From f6cff85c8d0ae4b32fbced558e831465d844e2ee Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Thu, 19 Sep 2024 14:31:13 +0400 Subject: [PATCH 01/24] [Console] Update Elasticsearch specification metamodel (#193239) ## Summary As mentioned in https://github.com/elastic/kibana/issues/163335, the Elasticsearch specification metamodel is copied into Kibana, not imported. I don't know how to fix the original issue, so this pull request only updates the file, one year later. ### Checklist - [x] I checked that `node scripts/generate_console_definitions.js --source ../elasticsearch-specification/ --emptyDest` still works. Co-authored-by: Elastic Machine --- .../src/types/specification_types.ts | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/kbn-generate-console-definitions/src/types/specification_types.ts b/packages/kbn-generate-console-definitions/src/types/specification_types.ts index a3816133827bf..fc2f694290801 100644 --- a/packages/kbn-generate-console-definitions/src/types/specification_types.ts +++ b/packages/kbn-generate-console-definitions/src/types/specification_types.ts @@ -92,7 +92,7 @@ export interface DictionaryOf { } /** - * A user defined value. To be used when bubbling a generic parameter up to the top-level interface is + * A user defined value. To be used when bubbling a generic parameter up to the top-level class is * inconvenient or impossible (e.g. for lists of user-defined values of possibly different types). * * Clients will allow providing a serializer/deserializer when reading/writing properties of this type, @@ -139,7 +139,7 @@ export interface Property { codegenName?: string; /** An optional set of aliases for `name` */ aliases?: string[]; - /** If the enclosing interface is a variants container, is this a property of the container and not a variant? */ + /** If the enclosing class is a variants container, is this a property of the container and not a variant? */ containerProperty?: boolean; /** If this property has a quirk that needs special attention, give a short explanation about it */ esQuirk?: string; @@ -181,7 +181,7 @@ export interface BaseType { specLocation: string; } -export type Variants = ExternalTag | InternalTag | Container; +export type Variants = ExternalTag | InternalTag | Container | Untagged; export interface VariantBase { /** @@ -208,6 +208,11 @@ export interface Container extends VariantBase { kind: 'container'; } +export interface Untagged extends VariantBase { + kind: 'untagged'; + untypedVariant: TypeName; +} + /** * Inherits clause (aka extends or implements) for an interface or request */ @@ -216,6 +221,12 @@ export interface Inherits { generics?: ValueOf[]; } +export interface Behavior { + type: TypeName; + generics?: ValueOf[]; + meta?: Record; +} + /** * An interface type */ @@ -232,7 +243,7 @@ export interface Interface extends BaseType { /** * Behaviors directly implemented by this interface */ - behaviors?: Inherits[]; + behaviors?: Behavior[]; /** * Behaviors attached to this interface, coming from the interface itself (see `behaviors`) @@ -271,12 +282,12 @@ export interface Request extends BaseType { // We can also pull path parameter descriptions on body properties they replace /** - * Body type. Most often a list of properties (that can extend those of the inherited interface, see above), except for a + * Body type. Most often a list of properties (that can extend those of the inherited class, see above), except for a * few specific cases that use other types such as bulk (array) or create (generic parameter). Or NoBody for requests * that don't have a body. */ body: Body; - behaviors?: Inherits[]; + behaviors?: Behavior[]; attachedBehaviors?: string[]; } @@ -287,7 +298,7 @@ export interface Response extends BaseType { kind: 'response'; generics?: TypeName[]; body: Body; - behaviors?: Inherits[]; + behaviors?: Behavior[]; attachedBehaviors?: string[]; exceptions?: ResponseException[]; } @@ -335,6 +346,7 @@ export interface EnumMember { description?: string; deprecation?: Deprecation; since?: string; + availability?: Availabilities; } /** @@ -358,8 +370,11 @@ export interface TypeAlias extends BaseType { type: ValueOf; /** generic parameters: either concrete types or open parameters from the enclosing type */ generics?: TypeName[]; - /** Only applicable to `union_of` aliases: identify typed_key unions (external) and variant inventories (internal) */ - variants?: InternalTag | ExternalTag; + /** + * Only applicable to `union_of` aliases: identify typed_key unions (external), variant inventories (internal) + * and untagged variants + */ + variants?: InternalTag | ExternalTag | Untagged; } // ------------------------------------------------------------------------------------------------ @@ -438,6 +453,14 @@ export interface UrlTemplate { } export interface Model { + _info?: { + title: string; + license: { + name: string; + url: string; + }; + }; + types: TypeDefinition[]; endpoints: Endpoint[]; } From 067d949d2c914b87fbbe88f469e005dcf59a0310 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 19 Sep 2024 14:21:44 +0300 Subject: [PATCH 02/24] [Security Solution] Make retry messages in `retryIfDeleteByQueryConflicts` loggable (#193117) ## Summary This makes retry messages in `retryIfDeleteByQueryConflicts()` helper function visible in CI logs. ## Details According to the [implementation](https://github.com/elastic/kibana/blob/main/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js#L71-L74) only error messages logged via `ToolingLog` piped to the CI log. It happens only when `mochaReporter.captureLogOutput` flag is set to `true`. According to the [schema](https://github.com/elastic/kibana/blob/eabb1022815a7c661a0e642c62d0a77ce338f9c9/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts#L175) `captureLogOutput` has `true` value in CI and there is no `DISABLE_CI_LOG_OUTPUT_CAPTURE` env variable set. It was [tested](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6978#01920936-b484-4dfb-a87b-ae40858c9ff5) in flaky test runner. Error messages a logged as expected. --- .../utils/retry_delete_by_query_conflicts.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts index de7e1afd163c9..e7710e2c21ad9 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/retry_delete_by_query_conflicts.ts @@ -29,13 +29,12 @@ export async function retryIfDeleteByQueryConflicts( const operationResult = await operation(); if (!operationResult.failures || operationResult.failures?.length === 0) { - logger.info(`${name} finished successfully`); return operationResult; } const failureCause = operationResult.failures.map((failure) => failure.cause).join(', '); - logger.warning(`Unable to delete by query ${name}. Caused by: "${failureCause}". Retrying ...`); + logger.error(`Unable to delete by query ${name}. Caused by: "${failureCause}". Retrying ...`); await waitBeforeNextRetry(retryDelay); } From 09374ad2ce5ba96b61a1d300264d2465f34e2326 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Thu, 19 Sep 2024 14:23:11 +0300 Subject: [PATCH 03/24] [Security Solution] Add a missing domain tag to Endpoint Exceptions API (#193019) **Addresses:** https://github.com/elastic/kibana/issues/183375 ## Summary This PR adds a missing domain tag to Endpoint Exceptions API. The rest API endpoints got their tags in https://github.com/elastic/kibana/pull/189621. --- ...ceptions_api_2023_10_31.bundled.schema.yaml | 18 ++++++++++++++++++ ...ceptions_api_2023_10_31.bundled.schema.yaml | 18 ++++++++++++++++++ .../scripts/openapi_bundle.js | 14 ++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index d14aeca79ff54..fc3b7c832a709 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/ess/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -52,6 +52,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Create an endpoint exception list + tags: + - Security Solution Endpoint Exceptions API /api/endpoint_list/items: delete: description: >- @@ -111,6 +113,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Delete an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API get: description: >- Get the details of an endpoint exception list item using the `id` or @@ -171,6 +175,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Get an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API post: description: >- Create an endpoint exception list item, and associate it with the @@ -250,6 +256,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Create an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API put: description: >- Update an endpoint exception list item using the `id` or `item_id` @@ -334,6 +342,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Update an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API /api/endpoint_list/items/_find: get: description: Get a list of all endpoint exception list items. @@ -439,6 +449,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Get endpoint exception list items + tags: + - Security Solution Endpoint Exceptions API components: schemas: EndpointList: @@ -867,3 +879,9 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + Endpoint Exceptions API allows you to manage detection rule endpoint + exceptions to prevent a rule from generating an alert from incoming events + even when the rule's other criteria are met. + name: Security Solution Endpoint Exceptions API diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml index b8e8feb07d3fb..d342e6f63794e 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/docs/openapi/serverless/security_solution_endpoint_exceptions_api_2023_10_31.bundled.schema.yaml @@ -52,6 +52,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Create an endpoint exception list + tags: + - Security Solution Endpoint Exceptions API /api/endpoint_list/items: delete: description: >- @@ -111,6 +113,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Delete an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API get: description: >- Get the details of an endpoint exception list item using the `id` or @@ -171,6 +175,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Get an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API post: description: >- Create an endpoint exception list item, and associate it with the @@ -250,6 +256,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Create an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API put: description: >- Update an endpoint exception list item using the `id` or `item_id` @@ -334,6 +342,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Update an endpoint exception list item + tags: + - Security Solution Endpoint Exceptions API /api/endpoint_list/items/_find: get: description: Get a list of all endpoint exception list items. @@ -439,6 +449,8 @@ paths: $ref: '#/components/schemas/SiemErrorResponse' description: Internal server error summary: Get endpoint exception list items + tags: + - Security Solution Endpoint Exceptions API components: schemas: EndpointList: @@ -867,3 +879,9 @@ components: type: http security: - BasicAuth: [] +tags: + - description: >- + Endpoint Exceptions API allows you to manage detection rule endpoint + exceptions to prevent a rule from generating an alert from incoming events + even when the rule's other criteria are met. + name: Security Solution Endpoint Exceptions API diff --git a/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js b/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js index 7d17eb56c3724..1c394ce1106ac 100644 --- a/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js +++ b/packages/kbn-securitysolution-endpoint-exceptions-common/scripts/openapi_bundle.js @@ -27,6 +27,13 @@ const ROOT = resolve(__dirname, '..'); title: 'Security Solution Endpoint Exceptions API (Elastic Cloud Serverless)', description: 'Endpoint Exceptions API allow you to manage Endpoint lists.', }, + tags: [ + { + name: 'Security Solution Endpoint Exceptions API', + description: + "Endpoint Exceptions API allows you to manage detection rule endpoint exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + }, + ], }, }, }); @@ -44,6 +51,13 @@ const ROOT = resolve(__dirname, '..'); title: 'Security Solution Endpoint Exceptions API (Elastic Cloud and self-hosted)', description: 'Endpoint Exceptions API allow you to manage Endpoint lists.', }, + tags: [ + { + name: 'Security Solution Endpoint Exceptions API', + description: + "Endpoint Exceptions API allows you to manage detection rule endpoint exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.", + }, + ], }, }, }); From 1c39fa2457df6fbb8cbf23149692c19deee618fc Mon Sep 17 00:00:00 2001 From: mohamedhamed-ahmed Date: Thu, 19 Sep 2024 12:52:30 +0100 Subject: [PATCH 04/24] [Logs Explorer] Fix namespace filter sync problem (#193307) closes https://github.com/elastic/kibana/issues/193304 --- .../src/services/control_panels.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/control_panels.ts b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/control_panels.ts index 138c72f84985b..9f761046b4d11 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/control_panels.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/services/control_panels.ts @@ -7,6 +7,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { DiscoverStateContainer } from '@kbn/discover-plugin/public'; +import deepEqual from 'fast-deep-equal'; import { mapValues, pick } from 'lodash'; import { InvokeCreator } from 'xstate'; import { @@ -39,8 +40,17 @@ export const subscribeControlGroup = discoverStateContainer.actions.fetchData(); }); + const inputSubscription = context.controlGroupAPI + .getInput$() + .subscribe(({ initialChildControlState: panels }) => { + if (!deepEqual(panels, context.controlPanels)) { + send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels }); + } + }); + return () => { filtersSubscription.unsubscribe(); + inputSubscription.unsubscribe(); }; }; From 6b372b7b45913a0171958a7795b4663ea865d137 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 19 Sep 2024 15:06:30 +0300 Subject: [PATCH 05/24] [ResponseOps][Cases] Fix a bug with cases telemetry where data from other spaces are not included (#193166) ## Summary The Find SO API supports the `namespaces` parameter where you can define the spaces that the SO client should search for. If you omit the `namespaces` parameter, the SO client will use the active space. This PR creates a wrapper around the SO client to add the `namespaces: ['*']` to all Find SO usages to count telemetry on all spaces. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../telemetry/collect_telemetry_data.ts | 2 +- .../plugins/cases/server/telemetry/index.ts | 17 +++--- .../server/telemetry/queries/alerts.test.ts | 11 +++- .../queries/case_system_action.test.ts | 50 ++++++++++++++++- .../telemetry/queries/case_system_action.ts | 1 + .../server/telemetry/queries/cases.test.ts | 21 +++++++- .../cases/server/telemetry/queries/cases.ts | 19 ++++--- .../server/telemetry/queries/comments.test.ts | 14 ++++- .../telemetry/queries/configuration.test.ts | 24 +++++++-- .../server/telemetry/queries/configuration.ts | 1 + .../telemetry/queries/connectors.test.ts | 12 ++++- .../server/telemetry/queries/connectors.ts | 1 + .../queries/{pushed.test.ts => push.test.ts} | 15 ++++-- .../telemetry/queries/{pushes.ts => push.ts} | 1 + .../telemetry/queries/user_actions.test.ts | 14 ++++- .../server/telemetry/queries/utils.test.ts | 23 ++++++-- .../cases/server/telemetry/queries/utils.ts | 5 +- .../telemetry_saved_objects_client.test.ts | 30 +++++++++++ .../telemetry_saved_objects_client.ts | 24 +++++++++ .../plugins/cases/server/telemetry/types.ts | 5 +- .../common/lib/api/index.ts | 2 + .../common/lib/api/telemetry.ts | 38 +++++++++++++ .../common/plugins/cases/kibana.jsonc | 1 + .../common/plugins/cases/server/plugin.ts | 2 + .../common/plugins/cases/server/routes.ts | 29 ++++++++++ .../common/plugins/cases/tsconfig.json | 1 + .../security_and_spaces/tests/common/index.ts | 5 ++ .../tests/common/telemetry.ts | 53 +++++++++++++++++++ 28 files changed, 379 insertions(+), 42 deletions(-) rename x-pack/plugins/cases/server/telemetry/queries/{pushed.test.ts => push.test.ts} (82%) rename x-pack/plugins/cases/server/telemetry/queries/{pushes.ts => push.ts} (98%) create mode 100644 x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts create mode 100644 x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts create mode 100644 x-pack/test/cases_api_integration/common/lib/api/telemetry.ts create mode 100644 x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts diff --git a/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts b/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts index cabb7743a540d..1bcf599f014fb 100644 --- a/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts +++ b/x-pack/plugins/cases/server/telemetry/collect_telemetry_data.ts @@ -11,7 +11,7 @@ import { getCasesSystemActionData } from './queries/case_system_action'; import { getUserCommentsTelemetryData } from './queries/comments'; import { getConfigurationTelemetryData } from './queries/configuration'; import { getConnectorsTelemetryData } from './queries/connectors'; -import { getPushedTelemetryData } from './queries/pushes'; +import { getPushedTelemetryData } from './queries/push'; import { getUserActionsTelemetryData } from './queries/user_actions'; import type { CasesTelemetry, CollectTelemetryDataParams } from './types'; diff --git a/x-pack/plugins/cases/server/telemetry/index.ts b/x-pack/plugins/cases/server/telemetry/index.ts index 5f10dcc6a3c72..c30d34d6c215c 100644 --- a/x-pack/plugins/cases/server/telemetry/index.ts +++ b/x-pack/plugins/cases/server/telemetry/index.ts @@ -5,12 +5,7 @@ * 2.0. */ -import type { - CoreSetup, - ISavedObjectsRepository, - Logger, - PluginInitializerContext, -} from '@kbn/core/server'; +import type { CoreSetup, Logger, PluginInitializerContext } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; @@ -25,6 +20,7 @@ import { } from '../../common/constants'; import type { CasesTelemetry } from './types'; import { casesSchema } from './schema'; +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; export { scheduleCasesTelemetryTask } from './schedule_telemetry_task'; @@ -42,13 +38,18 @@ export const createCasesTelemetry = ({ usageCollection, logger, }: CreateCasesTelemetryArgs) => { - const getInternalSavedObjectClient = async (): Promise => { + const getInternalSavedObjectClient = async (): Promise => { const [coreStart] = await core.getStartServices(); - return coreStart.savedObjects.createInternalRepository([ + const soClient = coreStart.savedObjects.createInternalRepository([ ...SAVED_OBJECT_TYPES, FILE_SO_TYPE, CASE_RULES_SAVED_OBJECT, ]); + + // Wrapping the internalRepository with the `TelemetrySavedObjectsClient` + // to ensure some best practices when collecting "all the telemetry" + // (i.e.: `.find` requests should query all spaces) + return new TelemetrySavedObjectsClient(soClient); }; taskManager.registerTaskDefinitions({ diff --git a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts index 0eaa99c57c0f3..11636b50ebd4e 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/alerts.test.ts @@ -7,12 +7,15 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getAlertsTelemetryData } from './alerts'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('alerts', () => { const logger = loggingSystemMock.createLogger(); describe('getAlertsTelemetryData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -35,7 +38,10 @@ describe('alerts', () => { }); it('it returns the correct res', async () => { - const res = await getAlertsTelemetryData({ savedObjectsClient, logger }); + const res = await getAlertsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -48,7 +54,7 @@ describe('alerts', () => { }); it('should call find with correct arguments', async () => { - await getAlertsTelemetryData({ savedObjectsClient, logger }); + await getAlertsTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -117,6 +123,7 @@ describe('alerts', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts index 6009d646431ed..0f121639e0f32 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.test.ts @@ -7,12 +7,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getCasesSystemActionData } from './case_system_action'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('casesSystemAction', () => { const logger = loggingSystemMock.createLogger(); describe('getCasesSystemActionData', () => { const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); beforeEach(() => { jest.clearAllMocks(); @@ -26,7 +28,10 @@ describe('casesSystemAction', () => { }); it('calculates the metrics correctly', async () => { - const res = await getCasesSystemActionData({ savedObjectsClient, logger }); + const res = await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ totalCasesCreated: 4, totalRules: 2 }); }); @@ -38,8 +43,49 @@ describe('casesSystemAction', () => { page: 1, }); - const res = await getCasesSystemActionData({ savedObjectsClient, logger }); + const res = await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); + expect(res).toEqual({ totalCasesCreated: 0, totalRules: 0 }); }); + + it('should call find with correct arguments', async () => { + savedObjectsClient.find.mockResolvedValue({ + total: 1, + saved_objects: [], + per_page: 1, + page: 1, + }); + + await getCasesSystemActionData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); + + expect(savedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "aggs": Object { + "counterSum": Object { + "sum": Object { + "field": "cases-rules.attributes.counter", + }, + }, + "totalRules": Object { + "cardinality": Object { + "field": "cases-rules.attributes.rules.id", + }, + }, + }, + "namespaces": Array [ + "*", + ], + "page": 1, + "perPage": 1, + "type": "cases-rules", + } + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts index 0e05006e3c437..6eda6b477611c 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/case_system_action.ts @@ -26,6 +26,7 @@ export const getCasesSystemActionData = async ({ cardinality: { field: `${CASE_RULES_SAVED_OBJECT}.attributes.rules.id` }, }, }, + namespaces: ['*'], }); return { diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts index 560997e8802be..fdfe39f940e9b 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/cases.test.ts @@ -15,6 +15,7 @@ import type { FileAttachmentAggregationResults, } from '../types'; import { getCasesTelemetryData } from './cases'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; const MOCK_FIND_TOTAL = 5; const SOLUTION_TOTAL = 1; @@ -23,6 +24,7 @@ describe('getCasesTelemetryData', () => { describe('getCasesTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); const mockFind = (aggs: object, so: SavedObjectsFindResponse['saved_objects'] = []) => { savedObjectsClient.find.mockResolvedValueOnce({ @@ -322,7 +324,10 @@ describe('getCasesTelemetryData', () => { }; }; - const res = await getCasesTelemetryData({ savedObjectsClient, logger }); + const res = await getCasesTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); const allAttachmentsTotal = 5; const allAttachmentsAverage = allAttachmentsTotal / MOCK_FIND_TOTAL; @@ -406,7 +411,7 @@ describe('getCasesTelemetryData', () => { it('should call find with correct arguments', async () => { mockResponse(); - await getCasesTelemetryData({ savedObjectsClient, logger }); + await getCasesTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(` Object { @@ -660,6 +665,9 @@ describe('getCasesTelemetryData', () => { }, }, }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "cases", @@ -974,6 +982,9 @@ describe('getCasesTelemetryData', () => { }, }, }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "cases-comments", @@ -1023,6 +1034,7 @@ describe('getCasesTelemetryData', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); expect(savedObjectsClient.find.mock.calls[3][0]).toEqual({ @@ -1068,6 +1080,7 @@ describe('getCasesTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); for (const [index, sortField] of ['created_at', 'updated_at', 'closed_at'].entries()) { @@ -1079,6 +1092,7 @@ describe('getCasesTelemetryData', () => { sortField, sortOrder: 'desc', type: 'cases', + namespaces: ['*'], }); } @@ -1172,6 +1186,9 @@ describe('getCasesTelemetryData', () => { "function": "is", "type": "function", }, + "namespaces": Array [ + "*", + ], "page": 0, "perPage": 0, "type": "file", diff --git a/x-pack/plugins/cases/server/telemetry/queries/cases.ts b/x-pack/plugins/cases/server/telemetry/queries/cases.ts index abd1979d752e8..81eefd6af1d1d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/cases.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/cases.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ISavedObjectsRepository, SavedObjectsFindResponse } from '@kbn/core/server'; +import type { SavedObjectsFindResponse } from '@kbn/core/server'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { fromKueryExpression } from '@kbn/es-query'; import { @@ -37,6 +37,7 @@ import { } from './utils'; import type { CasePersistedAttributes } from '../../common/types/case'; import { CasePersistedStatus } from '../../common/types/case'; +import type { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; export const getLatestCasesDates = async ({ savedObjectsClient, @@ -48,6 +49,7 @@ export const getLatestCasesDates = async ({ sortField, sortOrder: 'desc', type: CASE_SAVED_OBJECT, + namespaces: ['*'], }); const savedObjects = await Promise.all([ @@ -145,7 +147,7 @@ export const getCasesTelemetryData = async ({ }; const getCasesSavedObjectTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const caseByOwnerAggregationQuery = OWNERS.reduce( (aggQuery, owner) => ({ @@ -169,6 +171,7 @@ const getCasesSavedObjectTelemetry = async ( page: 0, perPage: 0, type: CASE_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...caseByOwnerAggregationQuery, ...getCountsAggregationQuery(CASE_SAVED_OBJECT), @@ -231,7 +234,7 @@ const getAssigneesAggregations = () => ({ }); const getCommentsSavedObjectTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const attachmentRegistries = () => ({ externalReferenceTypes: { @@ -275,6 +278,7 @@ const getCommentsSavedObjectTelemetry = async ( page: 0, perPage: 0, type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...attachmentsByOwnerAggregationQuery, ...attachmentRegistries(), @@ -288,7 +292,7 @@ const getCommentsSavedObjectTelemetry = async ( }; const getFilesTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { const averageSize = () => ({ averageSize: { @@ -332,17 +336,19 @@ const getFilesTelemetry = async ( perPage: 0, type: FILE_SO_TYPE, filter: filterCaseIdExists, + namespaces: ['*'], aggs: { ...filesByOwnerAggregationQuery, ...averageSize(), ...top20MimeTypes() }, }); }; const getAlertsTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { return savedObjectsClient.find({ page: 0, perPage: 0, type: CASE_COMMENT_SAVED_OBJECT, + namespaces: ['*'], filter: getOnlyAlertsCommentsFilter(), aggs: { ...getReferencesAggregationQuery({ @@ -355,12 +361,13 @@ const getAlertsTelemetry = async ( }; const getConnectorsTelemetry = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: TelemetrySavedObjectsClient ): Promise> => { return savedObjectsClient.find({ page: 0, perPage: 0, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], filter: getOnlyConnectorsFilter(), aggs: { ...getReferencesAggregationQuery({ diff --git a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts index 9eed9b4040992..d3104bd9a79ad 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/comments.test.ts @@ -7,11 +7,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { getUserCommentsTelemetryData } from './comments'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('comments', () => { describe('getUserCommentsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -34,7 +37,10 @@ describe('comments', () => { }); it('it returns the correct res', async () => { - const res = await getUserCommentsTelemetryData({ savedObjectsClient, logger }); + const res = await getUserCommentsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -47,7 +53,10 @@ describe('comments', () => { }); it('should call find with correct arguments', async () => { - await getUserCommentsTelemetryData({ savedObjectsClient, logger }); + await getUserCommentsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -116,6 +125,7 @@ describe('comments', () => { page: 0, perPage: 0, type: 'cases-comments', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts b/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts index 57c7c067a13cf..7e69c60980db1 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/configuration.test.ts @@ -8,11 +8,14 @@ import { loggingSystemMock, savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { CustomFieldTypes } from '../../../common/types/domain'; import { getConfigurationTelemetryData } from './configuration'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('configuration', () => { describe('getConfigurationTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -66,7 +69,10 @@ describe('configuration', () => { }); it('it returns the correct res', async () => { - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { @@ -82,7 +88,10 @@ describe('configuration', () => { }); it('should call find with correct arguments', async () => { - await getConfigurationTelemetryData({ savedObjectsClient, logger }); + await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { closureType: { @@ -95,6 +104,7 @@ describe('configuration', () => { page: 1, perPage: 5, type: 'cases-configure', + namespaces: ['*'], }); }); @@ -135,7 +145,10 @@ describe('configuration', () => { }, }); - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { @@ -205,7 +218,10 @@ describe('configuration', () => { }, }); - const res = await getConfigurationTelemetryData({ savedObjectsClient, logger }); + const res = await getConfigurationTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { closure: { diff --git a/x-pack/plugins/cases/server/telemetry/queries/configuration.ts b/x-pack/plugins/cases/server/telemetry/queries/configuration.ts index e3aff3216f5d5..6b736761207c8 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/configuration.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/configuration.ts @@ -28,6 +28,7 @@ export const getConfigurationTelemetryData = async ({ page: 1, perPage: 5, type: CASE_CONFIGURE_SAVED_OBJECT, + namespaces: ['*'], aggs: { closureType: { terms: { field: `${CASE_CONFIGURE_SAVED_OBJECT}.attributes.closure_type` }, diff --git a/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts b/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts index 684c77bac159a..03779f8714d8d 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/connectors.test.ts @@ -7,11 +7,13 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { getConnectorsTelemetryData } from './connectors'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('getConnectorsTelemetryData', () => { describe('getConnectorsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); const mockFind = (aggs: Record) => { savedObjectsClient.find.mockResolvedValueOnce({ @@ -42,7 +44,10 @@ describe('getConnectorsTelemetryData', () => { it('it returns the correct res', async () => { mockResponse(); - const res = await getConnectorsTelemetryData({ savedObjectsClient, logger }); + const res = await getConnectorsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { all: { @@ -71,7 +76,7 @@ describe('getConnectorsTelemetryData', () => { it('should call find with correct arguments', async () => { mockResponse(); - await getConnectorsTelemetryData({ savedObjectsClient, logger }); + await getConnectorsTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find.mock.calls[0][0]).toEqual({ aggs: { @@ -101,6 +106,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); expect(savedObjectsClient.find.mock.calls[1][0]).toEqual({ @@ -151,6 +157,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); for (const [index, connector] of [ @@ -205,6 +212,7 @@ describe('getConnectorsTelemetryData', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); } }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/connectors.ts b/x-pack/plugins/cases/server/telemetry/queries/connectors.ts index 0e8b12e1ed192..c3f254fadb4ce 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/connectors.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/connectors.ts @@ -37,6 +37,7 @@ export const getConnectorsTelemetryData = async ({ perPage: 0, filter, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...aggs, }, diff --git a/x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts b/x-pack/plugins/cases/server/telemetry/queries/push.test.ts similarity index 82% rename from x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts rename to x-pack/plugins/cases/server/telemetry/queries/push.test.ts index e25718f0feac9..1834c5f5d54c0 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/pushed.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/push.test.ts @@ -6,12 +6,15 @@ */ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; -import { getPushedTelemetryData } from './pushes'; +import { getPushedTelemetryData } from './push'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; -describe('pushes', () => { +describe('push', () => { describe('getPushedTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -27,7 +30,10 @@ describe('pushes', () => { }); it('it returns the correct res', async () => { - const res = await getPushedTelemetryData({ savedObjectsClient, logger }); + const res = await getPushedTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { maxOnACase: 1, @@ -37,7 +43,7 @@ describe('pushes', () => { }); it('should call find with correct arguments', async () => { - await getPushedTelemetryData({ savedObjectsClient, logger }); + await getPushedTelemetryData({ savedObjectsClient: telemetrySavedObjectsClient, logger }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { references: { @@ -86,6 +92,7 @@ describe('pushes', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/pushes.ts b/x-pack/plugins/cases/server/telemetry/queries/push.ts similarity index 98% rename from x-pack/plugins/cases/server/telemetry/queries/pushes.ts rename to x-pack/plugins/cases/server/telemetry/queries/push.ts index 0462a7ff0ef13..ea1127ae4520b 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/pushes.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/push.ts @@ -29,6 +29,7 @@ export const getPushedTelemetryData = async ({ perPage: 0, filter: pushFilter, type: CASE_USER_ACTION_SAVED_OBJECT, + namespaces: ['*'], aggs: { ...getMaxBucketOnCaseAggregationQuery(CASE_USER_ACTION_SAVED_OBJECT) }, }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts index c01c8d329c5b0..b6c45d8da3efc 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/user_actions.test.ts @@ -7,11 +7,14 @@ import { savedObjectsRepositoryMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { getUserActionsTelemetryData } from './user_actions'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('user_actions', () => { describe('getUserActionsTelemetryData', () => { const logger = loggingSystemMock.createLogger(); const savedObjectsClient = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -34,7 +37,10 @@ describe('user_actions', () => { }); it('it returns the correct res', async () => { - const res = await getUserActionsTelemetryData({ savedObjectsClient, logger }); + const res = await getUserActionsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(res).toEqual({ all: { total: 5, @@ -47,7 +53,10 @@ describe('user_actions', () => { }); it('should call find with correct arguments', async () => { - await getUserActionsTelemetryData({ savedObjectsClient, logger }); + await getUserActionsTelemetryData({ + savedObjectsClient: telemetrySavedObjectsClient, + logger, + }); expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -101,6 +110,7 @@ describe('user_actions', () => { page: 0, perPage: 0, type: 'cases-user-actions', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index bf975b84f46c5..6c66c5aab81c7 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -29,6 +29,7 @@ import { getReferencesAggregationQuery, getSolutionValues, } from './utils'; +import { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; describe('utils', () => { describe('getSolutionValues', () => { @@ -1017,7 +1018,12 @@ describe('utils', () => { }); it('returns the correct counts and max data', async () => { - const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + const res = await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); expect(res).toEqual({ all: { total: 5, @@ -1030,6 +1036,7 @@ describe('utils', () => { }); it('returns zero data if the response aggregation is not as expected', async () => { + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); savedObjectsClient.find.mockResolvedValue({ total: 5, saved_objects: [], @@ -1037,7 +1044,10 @@ describe('utils', () => { page: 1, }); - const res = await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const res = await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); expect(res).toEqual({ all: { total: 5, @@ -1050,7 +1060,13 @@ describe('utils', () => { }); it('should call find with correct arguments', async () => { - await getCountsAndMaxData({ savedObjectsClient, savedObjectType: 'test' }); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsClient); + + await getCountsAndMaxData({ + savedObjectsClient: telemetrySavedObjectsClient, + savedObjectType: 'test', + }); + expect(savedObjectsClient.find).toBeCalledWith({ aggs: { counts: { @@ -1104,6 +1120,7 @@ describe('utils', () => { page: 0, perPage: 0, type: 'test', + namespaces: ['*'], }); }); }); diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index ff785077d74ac..65b81e3362300 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -7,7 +7,6 @@ import { get } from 'lodash'; import type { KueryNode } from '@kbn/es-query'; -import type { ISavedObjectsRepository } from '@kbn/core/server'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -32,6 +31,7 @@ import type { import { buildFilter } from '../../client/utils'; import type { Owner } from '../../../common/constants/types'; import type { ConfigurationPersistedAttributes } from '../../common/types/configure'; +import type { TelemetrySavedObjectsClient } from '../telemetry_saved_objects_client'; export const getCountsAggregationQuery = (savedObjectType: string) => ({ counts: { @@ -126,7 +126,7 @@ export const getCountsAndMaxData = async ({ savedObjectType, filter, }: { - savedObjectsClient: ISavedObjectsRepository; + savedObjectsClient: TelemetrySavedObjectsClient; savedObjectType: string; filter?: KueryNode; }) => { @@ -138,6 +138,7 @@ export const getCountsAndMaxData = async ({ perPage: 0, filter, type: savedObjectType, + namespaces: ['*'], aggs: { ...getCountsAggregationQuery(savedObjectType), ...getMaxBucketOnCaseAggregationQuery(savedObjectType), diff --git a/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts new file mode 100644 index 0000000000000..bbe2d58a1ce9b --- /dev/null +++ b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; +import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; + +describe('TelemetrySavedObjectsClient', () => { + it("find requests are extended with `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + + await telemetrySavedObjectsClient.find({ type: 'my-test-type' }); + expect(savedObjectsRepository.find).toBeCalledWith({ type: 'my-test-type', namespaces: ['*'] }); + }); + + it("allow callers to overwrite the `namespaces:['*']`", async () => { + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const telemetrySavedObjectsClient = new TelemetrySavedObjectsClient(savedObjectsRepository); + + await telemetrySavedObjectsClient.find({ type: 'my-test-type', namespaces: ['some_space'] }); + expect(savedObjectsRepository.find).toBeCalledWith({ + type: 'my-test-type', + namespaces: ['some_space'], + }); + }); +}); diff --git a/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.ts new file mode 100644 index 0000000000000..42ae1fdd296d4 --- /dev/null +++ b/x-pack/plugins/cases/server/telemetry/telemetry_saved_objects_client.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsFindOptions, SavedObjectsFindResponse } from '@kbn/core/server'; +import { SavedObjectsClient } from '@kbn/core/server'; + +/** + * Extends the SavedObjectsClient to fit the telemetry fetching requirements (i.e.: find objects from all namespaces by default) + */ +export class TelemetrySavedObjectsClient extends SavedObjectsClient { + /** + * Find the SavedObjects matching the search query in all the Spaces by default + * @param options + */ + async find( + options: SavedObjectsFindOptions + ): Promise> { + return super.find({ namespaces: ['*'], ...options }); + } +} diff --git a/x-pack/plugins/cases/server/telemetry/types.ts b/x-pack/plugins/cases/server/telemetry/types.ts index 294efdbce1125..b4996da27f234 100644 --- a/x-pack/plugins/cases/server/telemetry/types.ts +++ b/x-pack/plugins/cases/server/telemetry/types.ts @@ -5,9 +5,10 @@ * 2.0. */ -import type { ISavedObjectsRepository, Logger } from '@kbn/core/server'; +import type { Logger } from '@kbn/core/server'; import type { MakeSchemaFrom } from '@kbn/usage-collection-plugin/server'; import type { Owner } from '../../common/constants/types'; +import type { TelemetrySavedObjectsClient } from './telemetry_saved_objects_client'; export type BucketKeyString = Omit & { key: string }; @@ -35,7 +36,7 @@ export interface ReferencesAggregation { } export interface CollectTelemetryDataParams { - savedObjectsClient: ISavedObjectsRepository; + savedObjectsClient: TelemetrySavedObjectsClient; logger: Logger; } diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts index cfb0596fa1ce9..ea0f66affdc35 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts @@ -61,6 +61,8 @@ export * from './user_profiles'; export * from './omit'; export * from './configuration'; export * from './files'; +export * from './telemetry'; + export { getSpaceUrlPrefix } from './helpers'; function toArray(input: T | T[]): T[] { diff --git a/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts b/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts new file mode 100644 index 0000000000000..785c059249030 --- /dev/null +++ b/x-pack/test/cases_api_integration/common/lib/api/telemetry.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type SuperTest from 'supertest'; +import { + ELASTIC_HTTP_VERSION_HEADER, + X_ELASTIC_INTERNAL_ORIGIN_REQUEST, +} from '@kbn/core-http-common'; +import { CasesTelemetry } from '@kbn/cases-plugin/server/telemetry/types'; +import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants'; + +interface CasesTelemetryPayload { + stats: { stack_stats: { kibana: { plugins: { cases: CasesTelemetry } } } }; +} + +export const getTelemetry = async (supertest: SuperTest.Agent): Promise => { + const { body } = await supertest + .post('/internal/telemetry/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .set(ELASTIC_HTTP_VERSION_HEADER, '2') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .send({ unencrypted: true, refreshCache: true }) + .expect(200); + + return body[0]; +}; + +export const runTelemetryTask = async (supertest: SuperTest.Agent) => { + await supertest + .post('/api/cases_fixture/telemetry/run_soon') + .set('kbn-xsrf', 'xxx') + .send({ taskId: CASES_TELEMETRY_TASK_NAME }) + .expect(200); +}; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc index 135db481efeef..91238eae39223 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc +++ b/x-pack/test/cases_api_integration/common/plugins/cases/kibana.jsonc @@ -11,6 +11,7 @@ "features", "cases", "files", + "taskManager" ], "optionalPlugins": [ "security", diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts index 488b56927450f..a10bf8ed1797e 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/plugin.ts @@ -13,6 +13,7 @@ import type { CasesServerStart, CasesServerSetup } from '@kbn/cases-plugin/serve import { FilesSetup } from '@kbn/files-plugin/server'; import { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server/plugin'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; +import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { getPersistableStateAttachment } from './attachments/persistable_state'; import { getExternalReferenceAttachment } from './attachments/external_reference'; import { registerRoutes } from './routes'; @@ -29,6 +30,7 @@ export interface FixtureStartDeps { security?: SecurityPluginStart; spaces?: SpacesPluginStart; cases: CasesServerStart; + taskManager: TaskManagerStartContract; } export class FixturePlugin implements Plugin { diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts index 11335c4d7adc7..10139f636c809 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts +++ b/x-pack/test/cases_api_integration/common/plugins/cases/server/routes.ts @@ -15,6 +15,7 @@ import type { } from '@kbn/cases-plugin/server/attachment_framework/types'; import { BulkCreateCasesRequest, CasesPatchRequest } from '@kbn/cases-plugin/common/types/api'; import { ActionExecutionSourceType } from '@kbn/actions-plugin/server/types'; +import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants'; import type { FixtureStartDeps } from './plugin'; const hashParts = (parts: string[]): string => { @@ -178,4 +179,32 @@ export const registerRoutes = (core: CoreSetup, logger: Logger } } ); + + router.post( + { + path: '/api/cases_fixture/telemetry/run_soon', + validate: { + body: schema.object({ + taskId: schema.string({ + validate: (telemetryTaskId: string) => { + if (CASES_TELEMETRY_TASK_NAME === telemetryTaskId) { + return; + } + + return 'invalid telemetry task id'; + }, + }), + }), + }, + }, + async (context, req, res) => { + const { taskId } = req.body; + try { + const [_, { taskManager }] = await core.getStartServices(); + return res.ok({ body: await taskManager.runSoon(taskId) }); + } catch (err) { + return res.ok({ body: { id: taskId, error: `${err}` } }); + } + } + ); }; diff --git a/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json b/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json index 0e0443d2930e9..72a20bd3f40d4 100644 --- a/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json +++ b/x-pack/test/cases_api_integration/common/plugins/cases/tsconfig.json @@ -18,6 +18,7 @@ "@kbn/features-plugin", "@kbn/spaces-plugin", "@kbn/security-plugin", + "@kbn/task-manager-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts index e731e0101bdc0..f9360e473080d 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/index.ts @@ -70,6 +70,11 @@ export default ({ loadTestFile }: FtrProviderContext): void => { */ loadTestFile(require.resolve('./cases/bulk_create_cases')); + /** + * Telemetry + */ + loadTestFile(require.resolve('./telemetry')); + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces // which causes errors in any tests after them that relies on those }); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts new file mode 100644 index 0000000000000..0c47e62fae79c --- /dev/null +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/telemetry.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { getPostCaseRequest } from '../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + getTelemetry, + runTelemetryTask, +} from '../../../common/lib/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { superUser } from '../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const retry = getService('retry'); + + describe('Cases telemetry', () => { + before(async () => { + await deleteAllCaseItems(es); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should count cases from all spaces', async () => { + await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: 'space2', + }); + + await runTelemetryTask(supertest); + + await retry.try(async () => { + const res = await getTelemetry(supertest); + expect(res.stats.stack_stats.kibana.plugins.cases.cases.all.total).toBe(2); + }); + }); + }); +}; From 0781e51fac89d688feb5a958418cb45b324da5f4 Mon Sep 17 00:00:00 2001 From: Alex Szabo Date: Thu, 19 Sep 2024 14:50:31 +0200 Subject: [PATCH 06/24] [fix] regenerate and add missing openapi-generated files (#193419) ## Summary Main seems to be broken because of a check. These are probably regenerated with a different shape since the js-yaml update: #190678 --- ...ity_solution_timeline_api_2023_10_31.bundled.schema.yaml | 6 +++--- ...ity_solution_timeline_api_2023_10_31.bundled.schema.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 36b20f661d3f0..5d55fac18c402 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -131,7 +131,7 @@ paths: type: string required: - note - description: 'The note to add or update, along with additional metadata.' + description: The note to add or update, along with additional metadata. required: true responses: '200': @@ -185,7 +185,7 @@ paths: required: - eventId - timelineId - description: 'The pinned event to add or update, along with additional metadata.' + description: The pinned event to add or update, along with additional metadata. required: true responses: '200': @@ -321,7 +321,7 @@ paths: - timelineId - version - timeline - description: 'The Timeline updates, along with the Timeline ID and version.' + description: The Timeline updates, along with the Timeline ID and version. required: true responses: '200': diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml index 5c676bec03ef5..d8536c1703ed7 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_timeline_api_2023_10_31.bundled.schema.yaml @@ -131,7 +131,7 @@ paths: type: string required: - note - description: 'The note to add or update, along with additional metadata.' + description: The note to add or update, along with additional metadata. required: true responses: '200': @@ -185,7 +185,7 @@ paths: required: - eventId - timelineId - description: 'The pinned event to add or update, along with additional metadata.' + description: The pinned event to add or update, along with additional metadata. required: true responses: '200': @@ -321,7 +321,7 @@ paths: - timelineId - version - timeline - description: 'The Timeline updates, along with the Timeline ID and version.' + description: The Timeline updates, along with the Timeline ID and version. required: true responses: '200': From 27f5da436b70da1a3743ee99c54d8159918b40de Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Thu, 19 Sep 2024 14:54:53 +0200 Subject: [PATCH 07/24] [SecuritySolutions] Create Entity Store 'entities/list' API (#192806) This PR introduces the following API routes for listing Entity Store "entities":
List Entities | GET /api/entity_store/entities/list -- | --
The PR includes the following: - The OpenAPI schemas for the route - The actual Kibana side endpoint - Add searchEntities function to the `EntityStoreDataClient` ### How to test 1. Add some host/user data * Easiest is to use [elastic/security-data-generator](https://github.com/elastic/security-documents-generator) 2. Make sure to add `entityStoreEnabled` under `xpack.securitySolution.enableExperimental` in your `kibana.dev.yml` 3. In kibana dev tools or your terminal, call the `INIT` route for either `user` or `host`. 4. You should now see 2 transforms in kibana. Make sure to re-trigger them if needed so they process the documents. 5. Call the new API, and it should return entities Implements https://github.com/elastic/security-team/issues/10517 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../ftr_security_serverless_configs.yml | 1 + .buildkite/ftr_security_stateful_configs.yml | 1 + .../entity_store/common.gen.ts | 6 + .../entity_store/common.schema.yaml | 19 +- .../entity_store/entities/common.gen.ts | 77 +++++ .../entity_store/entities/common.schema.yaml | 159 +++++++++ .../entities/list_entities.gen.ts | 44 +++ .../entities/list_entities.schema.yaml | 82 +++++ .../common/api/quickstart_client.gen.ts | 24 ++ .../entity_store/constants.ts | 15 + ...alytics_api_2023_10_31.bundled.schema.yaml | 240 ++++++++++++++ ...alytics_api_2023_10_31.bundled.schema.yaml | 240 ++++++++++++++ .../public/entity_analytics/api/api.ts | 30 ++ .../entity_store_data_client.test.ts.snap | 34 ++ .../entity_store/constants.ts | 2 + .../entity_store/definition.ts | 92 +++--- .../entity_store_data_client.mock.ts | 1 + .../entity_store_data_client.test.ts | 138 ++++++++ .../entity_store/entity_store_data_client.ts | 57 +++- .../entity_store/routes/entities/list.ts | 92 ++++++ .../routes/register_entity_store_routes.ts | 2 + .../entity_store/utils/utils.ts | 21 +- .../services/security_solution_api.gen.ts | 15 + .../security_solution/entity_store/data.json | 85 +++++ .../entity_store/mappings.json | 303 ++++++++++++++++++ .../configs/ess.config.ts | 28 ++ .../configs/serverless.config.ts | 24 ++ .../entities_list.ts | 71 ++++ .../trial_license_complete_tier/index.ts | 14 + 29 files changed, 1864 insertions(+), 53 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.gen.ts create mode 100644 x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.schema.yaml create mode 100644 x-pack/plugins/security_solution/common/entity_analytics/entity_store/constants.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/entities/list.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/entity_store/data.json create mode 100644 x-pack/test/functional/es_archives/security_solution/entity_store/mappings.json create mode 100644 x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/serverless.config.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts create mode 100644 x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts diff --git a/.buildkite/ftr_security_serverless_configs.yml b/.buildkite/ftr_security_serverless_configs.yml index 89ebb4aa12cd4..6d42c030b2d4f 100644 --- a/.buildkite/ftr_security_serverless_configs.yml +++ b/.buildkite/ftr_security_serverless_configs.yml @@ -81,6 +81,7 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/genai/knowledge_base/entries/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/serverless.config.ts + - x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/exception_lists_items/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/configs/serverless.config.ts - x-pack/test/security_solution_api_integration/test_suites/explore/hosts/trial_license_complete_tier/configs/serverless.config.ts diff --git a/.buildkite/ftr_security_stateful_configs.yml b/.buildkite/ftr_security_stateful_configs.yml index 77a8c57029096..a2390fa2bd27f 100644 --- a/.buildkite/ftr_security_stateful_configs.yml +++ b/.buildkite/ftr_security_stateful_configs.yml @@ -62,6 +62,7 @@ enabled: - x-pack/test/security_solution_api_integration/test_suites/detections_response/user_roles/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/basic_license_essentials_tier/configs/ess.config.ts + - x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/exception_lists_items/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/lists_and_exception_lists/lists_items/trial_license_complete_tier/configs/ess.config.ts - x-pack/test/security_solution_api_integration/test_suites/explore/hosts/trial_license_complete_tier/configs/ess.config.ts diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts index e5f8c631fcbae..75263643c2fc9 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts @@ -36,3 +36,9 @@ export const EngineDescriptor = z.object({ status: EngineStatus.optional(), filter: z.string().optional(), }); + +export type InspectQuery = z.infer; +export const InspectQuery = z.object({ + response: z.array(z.string()), + dsl: z.array(z.string()), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml index dc17ad6193ee5..505caac10d8df 100644 --- a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml @@ -6,7 +6,6 @@ info: paths: {} components: schemas: - EntityType: type: string enum: @@ -31,7 +30,21 @@ components: - installing - started - stopped - + IndexPattern: type: string - \ No newline at end of file + + InspectQuery: + type: object + properties: + response: + type: array + items: + type: string + dsl: + type: array + items: + type: string + required: + - dsl + - response diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts new file mode 100644 index 0000000000000..eb123b5a9da1f --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.gen.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Common Entities Schemas + * version: 1 + */ + +import { z } from '@kbn/zod'; + +export type UserEntity = z.infer; +export const UserEntity = z.object({ + user: z + .object({ + full_name: z.array(z.string()).optional(), + domain: z.array(z.string()).optional(), + roles: z.array(z.string()).optional(), + name: z.string(), + id: z.array(z.string()).optional(), + email: z.array(z.string()).optional(), + hash: z.array(z.string()).optional(), + }) + .optional(), + entity: z + .object({ + lastSeenTimestamp: z.string().datetime(), + schemaVersion: z.string(), + definitionVersion: z.string(), + displayName: z.string(), + identityFields: z.array(z.string()), + id: z.string(), + type: z.literal('node'), + firstSeenTimestamp: z.string().datetime(), + definitionId: z.string(), + }) + .optional(), +}); + +export type HostEntity = z.infer; +export const HostEntity = z.object({ + host: z + .object({ + hostname: z.array(z.string()).optional(), + domain: z.array(z.string()).optional(), + ip: z.array(z.string()).optional(), + name: z.string(), + id: z.array(z.string()).optional(), + type: z.array(z.string()).optional(), + mac: z.array(z.string()).optional(), + architecture: z.array(z.string()).optional(), + }) + .optional(), + entity: z + .object({ + lastSeenTimestamp: z.string().datetime(), + schemaVersion: z.string(), + definitionVersion: z.string(), + displayName: z.string(), + identityFields: z.array(z.string()), + id: z.string(), + type: z.literal('node'), + firstSeenTimestamp: z.string().datetime(), + definitionId: z.string(), + }) + .optional(), +}); + +export type Entity = z.infer; +export const Entity = z.union([UserEntity, HostEntity]); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml new file mode 100644 index 0000000000000..0f7f31792306c --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/common.schema.yaml @@ -0,0 +1,159 @@ +openapi: 3.0.0 +info: + title: Common Entities Schemas + description: Common Entities schemas for the Entity Store + version: '1' +paths: {} +components: + schemas: + UserEntity: + type: object + properties: + user: + type: object + properties: + full_name: + type: array + items: + type: string + domain: + type: array + items: + type: string + roles: + type: array + items: + type: string + name: + type: string + id: + type: array + items: + type: string + email: + type: array + items: + type: string + hash: + type: array + items: + type: string + required: + - name + entity: + type: object + properties: + lastSeenTimestamp: + type: string + format: date-time + schemaVersion: + type: string + definitionVersion: + type: string + displayName: + type: string + identityFields: + type: array + items: + type: string + id: + type: string + type: + type: string + enum: + - node + firstSeenTimestamp: + type: string + format: date-time + definitionId: + type: string + required: + - lastSeenTimestamp + - schemaVersion + - definitionVersion + - displayName + - identityFields + - id + - type + - firstSeenTimestamp + - definitionId + HostEntity: + type: object + properties: + host: + type: object + properties: + hostname: + type: array + items: + type: string + domain: + type: array + items: + type: string + ip: + type: array + items: + type: string + name: + type: string + id: + type: array + items: + type: string + type: + type: array + items: + type: string + mac: + type: array + items: + type: string + architecture: + type: array + items: + type: string + required: + - name + entity: + type: object + properties: + lastSeenTimestamp: + type: string + format: date-time + schemaVersion: + type: string + definitionVersion: + type: string + displayName: + type: string + identityFields: + type: array + items: + type: string + id: + type: string + type: + type: string + enum: + - node + firstSeenTimestamp: + type: string + format: date-time + definitionId: + type: string + required: + - lastSeenTimestamp + - schemaVersion + - definitionVersion + - displayName + - identityFields + - id + - type + - firstSeenTimestamp + - definitionId + + Entity: + oneOf: + - $ref: '#/components/schemas/UserEntity' + - $ref: '#/components/schemas/HostEntity' diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.gen.ts b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.gen.ts new file mode 100644 index 0000000000000..0c500f97986ed --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.gen.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Entities List Schema + * version: 2023-10-31 + */ + +import { z } from '@kbn/zod'; +import { ArrayFromString } from '@kbn/zod-helpers'; + +import { EntityType, InspectQuery } from '../common.gen'; +import { Entity } from './common.gen'; + +export type ListEntitiesRequestQuery = z.infer; +export const ListEntitiesRequestQuery = z.object({ + sort_field: z.string().optional(), + sort_order: z.enum(['asc', 'desc']).optional(), + page: z.coerce.number().int().min(1).optional(), + per_page: z.coerce.number().int().min(1).max(10000).optional(), + /** + * An ES query to filter by. + */ + filterQuery: z.string().optional(), + entities_types: ArrayFromString(EntityType), +}); +export type ListEntitiesRequestQueryInput = z.input; + +export type ListEntitiesResponse = z.infer; +export const ListEntitiesResponse = z.object({ + records: z.array(Entity), + page: z.number().int().min(1), + per_page: z.number().int().min(1).max(1000), + total: z.number().int().min(0), + inspect: InspectQuery.optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.schema.yaml b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.schema.yaml new file mode 100644 index 0000000000000..7664473dc61f4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/entity_analytics/entity_store/entities/list_entities.schema.yaml @@ -0,0 +1,82 @@ +# ⚠️ Updating this file? Also update the public API docs at https://github.com/elastic/security-docs/tree/main/docs/advanced-entity-analytics/api +openapi: 3.0.0 +info: + version: '2023-10-31' + title: Entities List Schema +paths: + /api/entity_store/entities/list: + get: + x-labels: [ess, serverless] + x-codegen-enabled: true + operationId: ListEntities + summary: List Entity Store Entities + description: List entities records, paging, sorting and filtering as needed. + parameters: + - name: sort_field + in: query + required: false + schema: + type: string + - name: sort_order + in: query + required: false + schema: + type: string + enum: + - asc + - desc + - name: page + in: query + required: false + schema: + type: integer + minimum: 1 + - name: per_page + in: query + required: false + schema: + type: integer + minimum: 1 + maximum: 10000 + - name: filterQuery + in: query + required: false + schema: + type: string + description: An ES query to filter by. + - name: entities_types + in: query + required: true + schema: + type: array + items: + $ref: '../common.schema.yaml#/components/schemas/EntityType' + responses: + '200': + description: Entities returned successfully + content: + application/json: + schema: + type: object + properties: + records: + type: array + items: + $ref: './common.schema.yaml#/components/schemas/Entity' + page: + type: integer + minimum: 1 + per_page: + type: integer + minimum: 1 + maximum: 1000 + total: + type: integer + minimum: 0 + inspect: + $ref: '../common.schema.yaml#/components/schemas/InspectQuery' + required: + - records + - page + - per_page + - total diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index f70d521692779..5c9d33858d0f7 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -269,6 +269,10 @@ import type { StopEntityStoreRequestParamsInput, StopEntityStoreResponse, } from './entity_analytics/entity_store/engine/stop.gen'; +import type { + ListEntitiesRequestQueryInput, + ListEntitiesResponse, +} from './entity_analytics/entity_store/entities/list_entities.gen'; import type { DisableRiskEngineResponse } from './entity_analytics/risk_engine/engine_disable_route.gen'; import type { EnableRiskEngineResponse } from './entity_analytics/risk_engine/engine_enable_route.gen'; import type { InitRiskEngineResponse } from './entity_analytics/risk_engine/engine_init_route.gen'; @@ -1474,6 +1478,23 @@ finalize it. }) .catch(catchAxiosErrorFormatAndThrow); } + /** + * List entities records, paging, sorting and filtering as needed. + */ + async listEntities(props: ListEntitiesProps) { + this.log.info(`${new Date().toISOString()} Calling API ListEntities`); + return this.kbnClient + .request({ + path: '/api/entity_store/entities/list', + headers: { + [ELASTIC_HTTP_VERSION_HEADER]: '2023-10-31', + }, + method: 'GET', + + query: props.query, + }) + .catch(catchAxiosErrorFormatAndThrow); + } async listEntityStoreEngines() { this.log.info(`${new Date().toISOString()} Calling API ListEntityStoreEngines`); return this.kbnClient @@ -2104,6 +2125,9 @@ export interface InstallPrepackedTimelinesProps { export interface InternalUploadAssetCriticalityRecordsProps { attachment: FormData; } +export interface ListEntitiesProps { + query: ListEntitiesRequestQueryInput; +} export interface PatchRuleProps { body: PatchRuleRequestBodyInput; } diff --git a/x-pack/plugins/security_solution/common/entity_analytics/entity_store/constants.ts b/x-pack/plugins/security_solution/common/entity_analytics/entity_store/constants.ts new file mode 100644 index 0000000000000..157ec6845e33a --- /dev/null +++ b/x-pack/plugins/security_solution/common/entity_analytics/entity_store/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Entity Store routes + */ + +export const ENTITY_STORE_URL = '/api/entity_store' as const; +export const ENTITIES_URL = `${ENTITY_STORE_URL}/entities` as const; + +export const LIST_ENTITIES_URL = `${ENTITIES_URL}/list` as const; diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index b1f7e444371f7..461d490305707 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -437,6 +437,82 @@ paths: summary: Stop the Entity Store engine tags: - Security Solution Entity Analytics API + /api/entity_store/entities/list: + get: + description: 'List entities records, paging, sorting and filtering as needed.' + operationId: ListEntities + parameters: + - in: query + name: sort_field + required: false + schema: + type: string + - in: query + name: sort_order + required: false + schema: + enum: + - asc + - desc + type: string + - in: query + name: page + required: false + schema: + minimum: 1 + type: integer + - in: query + name: per_page + required: false + schema: + maximum: 10000 + minimum: 1 + type: integer + - description: An ES query to filter by. + in: query + name: filterQuery + required: false + schema: + type: string + - in: query + name: entities_types + required: true + schema: + items: + $ref: '#/components/schemas/EntityType' + type: array + responses: + '200': + content: + application/json: + schema: + type: object + properties: + inspect: + $ref: '#/components/schemas/InspectQuery' + page: + minimum: 1 + type: integer + per_page: + maximum: 1000 + minimum: 1 + type: integer + records: + items: + $ref: '#/components/schemas/Entity' + type: array + total: + minimum: 0 + type: integer + required: + - records + - page + - per_page + - total + description: Entities returned successfully + summary: List Entity Store Entities + tags: + - Security Solution Entity Analytics API /api/risk_score/engine/schedule_now: post: operationId: ScheduleRiskEngineNow @@ -549,11 +625,90 @@ components: - started - stopped type: string + Entity: + oneOf: + - $ref: '#/components/schemas/UserEntity' + - $ref: '#/components/schemas/HostEntity' EntityType: enum: - user - host type: string + HostEntity: + type: object + properties: + entity: + type: object + properties: + definitionId: + type: string + definitionVersion: + type: string + displayName: + type: string + firstSeenTimestamp: + format: date-time + type: string + id: + type: string + identityFields: + items: + type: string + type: array + lastSeenTimestamp: + format: date-time + type: string + schemaVersion: + type: string + type: + enum: + - node + type: string + required: + - lastSeenTimestamp + - schemaVersion + - definitionVersion + - displayName + - identityFields + - id + - type + - firstSeenTimestamp + - definitionId + host: + type: object + properties: + architecture: + items: + type: string + type: array + domain: + items: + type: string + type: array + hostname: + items: + type: string + type: array + id: + items: + type: string + type: array + ip: + items: + type: string + type: array + mac: + items: + type: string + type: array + name: + type: string + type: + items: + type: string + type: array + required: + - name IdField: enum: - host.name @@ -561,6 +716,20 @@ components: type: string IndexPattern: type: string + InspectQuery: + type: object + properties: + dsl: + items: + type: string + type: array + response: + items: + type: string + type: array + required: + - dsl + - response RiskEngineScheduleNowErrorResponse: type: object properties: @@ -588,6 +757,77 @@ components: required: - status_code - message + UserEntity: + type: object + properties: + entity: + type: object + properties: + definitionId: + type: string + definitionVersion: + type: string + displayName: + type: string + firstSeenTimestamp: + format: date-time + type: string + id: + type: string + identityFields: + items: + type: string + type: array + lastSeenTimestamp: + format: date-time + type: string + schemaVersion: + type: string + type: + enum: + - node + type: string + required: + - lastSeenTimestamp + - schemaVersion + - definitionVersion + - displayName + - identityFields + - id + - type + - firstSeenTimestamp + - definitionId + user: + type: object + properties: + domain: + items: + type: string + type: array + email: + items: + type: string + type: array + full_name: + items: + type: string + type: array + hash: + items: + type: string + type: array + id: + items: + type: string + type: array + name: + type: string + roles: + items: + type: string + type: array + required: + - name securitySchemes: BasicAuth: scheme: basic diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 7506b8eb0d998..b3584e7efd08c 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -437,6 +437,82 @@ paths: summary: Stop the Entity Store engine tags: - Security Solution Entity Analytics API + /api/entity_store/entities/list: + get: + description: 'List entities records, paging, sorting and filtering as needed.' + operationId: ListEntities + parameters: + - in: query + name: sort_field + required: false + schema: + type: string + - in: query + name: sort_order + required: false + schema: + enum: + - asc + - desc + type: string + - in: query + name: page + required: false + schema: + minimum: 1 + type: integer + - in: query + name: per_page + required: false + schema: + maximum: 10000 + minimum: 1 + type: integer + - description: An ES query to filter by. + in: query + name: filterQuery + required: false + schema: + type: string + - in: query + name: entities_types + required: true + schema: + items: + $ref: '#/components/schemas/EntityType' + type: array + responses: + '200': + content: + application/json: + schema: + type: object + properties: + inspect: + $ref: '#/components/schemas/InspectQuery' + page: + minimum: 1 + type: integer + per_page: + maximum: 1000 + minimum: 1 + type: integer + records: + items: + $ref: '#/components/schemas/Entity' + type: array + total: + minimum: 0 + type: integer + required: + - records + - page + - per_page + - total + description: Entities returned successfully + summary: List Entity Store Entities + tags: + - Security Solution Entity Analytics API /api/risk_score/engine/schedule_now: post: operationId: ScheduleRiskEngineNow @@ -549,11 +625,90 @@ components: - started - stopped type: string + Entity: + oneOf: + - $ref: '#/components/schemas/UserEntity' + - $ref: '#/components/schemas/HostEntity' EntityType: enum: - user - host type: string + HostEntity: + type: object + properties: + entity: + type: object + properties: + definitionId: + type: string + definitionVersion: + type: string + displayName: + type: string + firstSeenTimestamp: + format: date-time + type: string + id: + type: string + identityFields: + items: + type: string + type: array + lastSeenTimestamp: + format: date-time + type: string + schemaVersion: + type: string + type: + enum: + - node + type: string + required: + - lastSeenTimestamp + - schemaVersion + - definitionVersion + - displayName + - identityFields + - id + - type + - firstSeenTimestamp + - definitionId + host: + type: object + properties: + architecture: + items: + type: string + type: array + domain: + items: + type: string + type: array + hostname: + items: + type: string + type: array + id: + items: + type: string + type: array + ip: + items: + type: string + type: array + mac: + items: + type: string + type: array + name: + type: string + type: + items: + type: string + type: array + required: + - name IdField: enum: - host.name @@ -561,6 +716,20 @@ components: type: string IndexPattern: type: string + InspectQuery: + type: object + properties: + dsl: + items: + type: string + type: array + response: + items: + type: string + type: array + required: + - dsl + - response RiskEngineScheduleNowErrorResponse: type: object properties: @@ -588,6 +757,77 @@ components: required: - status_code - message + UserEntity: + type: object + properties: + entity: + type: object + properties: + definitionId: + type: string + definitionVersion: + type: string + displayName: + type: string + firstSeenTimestamp: + format: date-time + type: string + id: + type: string + identityFields: + items: + type: string + type: array + lastSeenTimestamp: + format: date-time + type: string + schemaVersion: + type: string + type: + enum: + - node + type: string + required: + - lastSeenTimestamp + - schemaVersion + - definitionVersion + - displayName + - identityFields + - id + - type + - firstSeenTimestamp + - definitionId + user: + type: object + properties: + domain: + items: + type: string + type: array + email: + items: + type: string + type: array + full_name: + items: + type: string + type: array + hash: + items: + type: string + type: array + id: + items: + type: string + type: array + name: + type: string + roles: + items: + type: string + type: array + required: + - name securitySchemes: BasicAuth: scheme: basic diff --git a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts index 427a5633a7265..f958d20d7c96b 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts +++ b/x-pack/plugins/security_solution/public/entity_analytics/api/api.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { LIST_ENTITIES_URL } from '../../../common/entity_analytics/entity_store/constants'; import type { RiskEngineScheduleNowResponse } from '../../../common/api/entity_analytics/risk_engine/engine_schedule_now_route.gen'; import type { DisableRiskEngineResponse } from '../../../common/api/entity_analytics/risk_engine/engine_disable_route.gen'; import type { UploadAssetCriticalityRecordsResponse } from '../../../common/api/entity_analytics/asset_criticality/upload_asset_criticality_csv.gen'; @@ -44,6 +45,8 @@ import { import type { SnakeToCamelCase } from '../common/utils'; import { useKibana } from '../../common/lib/kibana/kibana_react'; import type { ReadRiskEngineSettingsResponse } from '../../../common/api/entity_analytics/risk_engine'; +import type { ListEntitiesResponse } from '../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; +import { type ListEntitiesRequestQuery } from '../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; export interface DeleteAssetCriticalityResponse { deleted: true; @@ -69,6 +72,30 @@ export const useEntityAnalyticsRoutes = () => { signal, }); + /** + * Fetches entities from the Entity Store + */ + const fetchEntitiesList = ({ + signal, + params, + }: { + signal?: AbortSignal; + params: FetchEntitiesListParams; + }) => + http.fetch(LIST_ENTITIES_URL, { + version: API_VERSIONS.public.v1, + method: 'GET', + query: { + entities_types: params.entitiesTypes, + sort_field: params.sortField, + sort_order: params.sortOrder, + page: params.page, + per_page: params.perPage, + filterQuery: params.filterQuery, + }, + signal, + }); + /** * Fetches risks engine status */ @@ -256,8 +283,11 @@ export const useEntityAnalyticsRoutes = () => { getRiskScoreIndexStatus, fetchRiskEngineSettings, calculateEntityRiskScore, + fetchEntitiesList, }; }, [http]); }; export type AssetCriticality = SnakeToCamelCase; + +export type FetchEntitiesListParams = SnakeToCamelCase; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap new file mode 100644 index 0000000000000..9bf156dc25efd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/__snapshots__/entity_store_data_client.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EntityStoreDataClient search entities returns inspect query params 1`] = ` +Object { + "dsl": Array [ + "{ + \\"index\\": [ + \\".entities.v1.latest.ea_host_entity_store\\" + ], + \\"body\\": { + \\"bool\\": { + \\"filter\\": [] + } + } +}", + ], + "response": Array [ + "{ + \\"took\\": 0, + \\"timed_out\\": false, + \\"_shards\\": { + \\"total\\": 0, + \\"successful\\": 0, + \\"skipped\\": 0, + \\"failed\\": 0 + }, + \\"hits\\": { + \\"total\\": 0, + \\"hits\\": [] + } +}", + ], +} +`; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts index ce5a61fa7e6c9..e2ddb69f60b0b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts @@ -19,3 +19,5 @@ export const ENGINE_STATUS: Record, EngineStatus> = { STARTED: 'started', STOPPED: 'stopped', }; + +export const MAX_SEARCH_RESPONSE_SIZE = 10_000; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts index 32859b9841e7f..391e8b16dd32d 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/definition.ts @@ -7,50 +7,52 @@ import { entityDefinitionSchema, type EntityDefinition } from '@kbn/entities-schema'; import { ENTITY_STORE_DEFAULT_SOURCE_INDICES } from './constants'; +import { getEntityDefinitionId } from './utils/utils'; -export const HOST_ENTITY_DEFINITION: EntityDefinition = entityDefinitionSchema.parse({ - id: 'ea_host_entity_store', - name: 'EA Host Store', - type: 'host', - indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, - identityFields: ['host.name'], - displayNameTemplate: '{{host.name}}', - metadata: [ - 'host.domain', - 'host.hostname', - 'host.id', - 'host.ip', - 'host.mac', - 'host.name', - 'host.type', - 'host.architecture', - ], - history: { - timestampField: '@timestamp', - interval: '1m', - }, - version: '1.0.0', -}); +export const buildHostEntityDefinition = (): EntityDefinition => + entityDefinitionSchema.parse({ + id: getEntityDefinitionId('host'), + name: 'EA Host Store', + type: 'host', + indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, + identityFields: ['host.name'], + displayNameTemplate: '{{host.name}}', + metadata: [ + 'host.domain', + 'host.hostname', + 'host.id', + 'host.ip', + 'host.mac', + 'host.name', + 'host.type', + 'host.architecture', + ], + history: { + timestampField: '@timestamp', + interval: '1m', + }, + version: '1.0.0', + }); -export const USER_ENTITY_DEFINITION: EntityDefinition = entityDefinitionSchema.parse({ - id: 'ea_user_entity_store', - name: 'EA User Store', - type: 'user', - indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, - identityFields: ['user.name'], - displayNameTemplate: '{{user.name}}', - metadata: [ - 'user.domain', - 'user.email', - 'user.full_name', - 'user.hash', - 'user.id', - 'user.name', - 'user.roles', - ], - history: { - timestampField: '@timestamp', - interval: '1m', - }, - version: '1.0.0', -}); +export const buildUserEntityDefinition = (): EntityDefinition => + entityDefinitionSchema.parse({ + id: getEntityDefinitionId('user'), + name: 'EA User Store', + indexPatterns: ENTITY_STORE_DEFAULT_SOURCE_INDICES, + identityFields: ['user.name'], + displayNameTemplate: '{{user.name}}', + metadata: [ + 'user.domain', + 'user.email', + 'user.full_name', + 'user.hash', + 'user.id', + 'user.name', + 'user.roles', + ], + history: { + timestampField: '@timestamp', + interval: '1m', + }, + version: '1.0.0', + }); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts index 095565343e130..4c5066e344182 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.mock.ts @@ -15,6 +15,7 @@ const createEntityStoreDataClientMock = () => get: jest.fn(), list: jest.fn(), delete: jest.fn(), + searchEntities: jest.fn(), } as unknown as jest.Mocked); export const entityStoreDataClientMock = { create: createEntityStoreDataClientMock }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts new file mode 100644 index 0000000000000..040b6e60eb695 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts @@ -0,0 +1,138 @@ +/* + * 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 { + loggingSystemMock, + elasticsearchServiceMock, + savedObjectsClientMock, +} from '@kbn/core/server/mocks'; +import { EntityStoreDataClient } from './entity_store_data_client'; +import { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; +import type { SortOrder } from '@elastic/elasticsearch/lib/api/types'; +import type { EntityType } from '../../../../common/api/entity_analytics/entity_store/common.gen'; + +describe('EntityStoreDataClient', () => { + const logger = loggingSystemMock.createLogger(); + const mockSavedObjectClient = savedObjectsClientMock.create(); + const esClientMock = elasticsearchServiceMock.createScopedClusterClient().asInternalUser; + const loggerMock = loggingSystemMock.createLogger(); + const dataClient = new EntityStoreDataClient({ + esClient: esClientMock, + logger: loggerMock, + namespace: 'default', + soClient: mockSavedObjectClient, + entityClient: new EntityClient({ + esClient: esClientMock, + soClient: mockSavedObjectClient, + logger, + }), + }); + + const defaultSearchParams = { + entityTypes: ['host'] as EntityType[], + page: 1, + perPage: 10, + sortField: 'hostName', + sortOrder: 'asc' as SortOrder, + }; + + describe('search entities', () => { + beforeEach(() => { + jest.resetAllMocks(); + esClientMock.search.mockResolvedValue({ + took: 0, + timed_out: false, + _shards: { + total: 0, + successful: 0, + skipped: 0, + failed: 0, + }, + hits: { + total: 0, + hits: [], + }, + }); + }); + + it('searches in the entities store indices', async () => { + await dataClient.searchEntities({ + ...defaultSearchParams, + entityTypes: ['host', 'user'], + }); + + expect(esClientMock.search).toHaveBeenCalledWith( + expect.objectContaining({ + index: [ + '.entities.v1.latest.ea_host_entity_store', + '.entities.v1.latest.ea_user_entity_store', + ], + }) + ); + }); + + it('should filter by filterQuery param', async () => { + await dataClient.searchEntities({ + ...defaultSearchParams, + filterQuery: '{"match_all":{}}', + }); + + expect(esClientMock.search).toHaveBeenCalledWith( + expect.objectContaining({ query: { bool: { filter: [{ match_all: {} }] } } }) + ); + }); + + it('should paginate', async () => { + await dataClient.searchEntities({ + ...defaultSearchParams, + page: 3, + perPage: 7, + }); + + expect(esClientMock.search).toHaveBeenCalledWith( + expect.objectContaining({ from: 14, size: 7 }) + ); + }); + + it('should sort', async () => { + await dataClient.searchEntities({ + ...defaultSearchParams, + sortField: '@timestamp', + sortOrder: 'asc', + }); + + expect(esClientMock.search).toHaveBeenCalledWith( + expect.objectContaining({ sort: [{ '@timestamp': 'asc' }] }) + ); + }); + + it('caps the size to the maximum query size', async () => { + await dataClient.searchEntities({ + ...defaultSearchParams, + perPage: 999_999, + }); + + const maxSize = 10_000; + + expect(esClientMock.search).toHaveBeenCalledWith(expect.objectContaining({ size: maxSize })); + }); + + it('ignores an index_not_found_exception if the entity index does not exist', async () => { + await dataClient.searchEntities(defaultSearchParams); + + expect(esClientMock.search).toHaveBeenCalledWith( + expect.objectContaining({ ignore_unavailable: true }) + ); + }); + + it('returns inspect query params', async () => { + const response = await dataClient.searchEntities(defaultSearchParams); + + expect(response.inspect).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index cb4d59139a25f..1d235531b2e21 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -8,6 +8,9 @@ import type { Logger, ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; +import type { SortOrder } from '@elastic/elasticsearch/lib/api/types'; +import type { Entity } from '../../../../common/api/entity_analytics/entity_store/entities/common.gen'; +import { createQueryFilterClauses } from '../../../utils/build_query'; import type { InitEntityStoreRequestBody, InitEntityStoreResponse, @@ -15,11 +18,12 @@ import type { import type { EngineDescriptor, EntityType, + InspectQuery, } from '../../../../common/api/entity_analytics/entity_store/common.gen'; import { entityEngineDescriptorTypeName } from './saved_object'; import { EngineDescriptorClient } from './saved_object/engine_descriptor'; -import { getEntityDefinition } from './utils/utils'; -import { ENGINE_STATUS } from './constants'; +import { getEntitiesIndexName, getEntityDefinition } from './utils/utils'; +import { ENGINE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants'; interface EntityStoreClientOpts { logger: Logger; @@ -29,6 +33,15 @@ interface EntityStoreClientOpts { soClient: SavedObjectsClientContract; } +interface SearchEntitiesParams { + entityTypes: EntityType[]; + filterQuery?: string; + page: number; + perPage: number; + sortField: string; + sortOrder: SortOrder; +} + export class EntityStoreDataClient { private engineClient: EngineDescriptorClient; constructor(private readonly options: EntityStoreClientOpts) { @@ -117,4 +130,44 @@ export class EntityStoreDataClient { return { deleted: true }; } + + public async searchEntities(params: SearchEntitiesParams): Promise<{ + records: Entity[]; + total: number; + inspect: InspectQuery; + }> { + const { page, perPage, sortField, sortOrder, filterQuery, entityTypes } = params; + + const index = entityTypes.map(getEntitiesIndexName); + const from = (page - 1) * perPage; + const sort = sortField ? [{ [sortField]: sortOrder }] : undefined; + + const filter = [...createQueryFilterClauses(filterQuery)]; + const query = { + bool: { + filter, + }, + }; + + const response = await this.options.esClient.search({ + index, + query, + size: Math.min(perPage, MAX_SEARCH_RESPONSE_SIZE), + from, + sort, + ignore_unavailable: true, + }); + const { hits } = response; + + const total = typeof hits.total === 'number' ? hits.total : hits.total?.value ?? 0; + + const records = hits.hits.map((hit) => hit._source as Entity); + + const inspect: InspectQuery = { + dsl: [JSON.stringify({ index, body: query }, null, 2)], + response: [JSON.stringify(response, null, 2)], + }; + + return { records, total, inspect }; + } } diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/entities/list.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/entities/list.ts new file mode 100644 index 0000000000000..6aaab39656e7d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/entities/list.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: List Entity Store engines + * version: 1 + */ + +import type { IKibanaResponse, Logger } from '@kbn/core/server'; +import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { buildRouteValidationWithZod } from '@kbn/zod-helpers'; +import { LIST_ENTITIES_URL } from '../../../../../../common/entity_analytics/entity_store/constants'; +import type { ListEntitiesResponse } from '../../../../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; +import { ListEntitiesRequestQuery } from '../../../../../../common/api/entity_analytics/entity_store/entities/list_entities.gen'; +import { APP_ID } from '../../../../../../common'; +import { API_VERSIONS } from '../../../../../../common/entity_analytics/constants'; + +import type { EntityAnalyticsRoutesDeps } from '../../../types'; + +export const listEntitiesRoute = (router: EntityAnalyticsRoutesDeps['router'], logger: Logger) => { + router.versioned + .get({ + access: 'public', + path: LIST_ENTITIES_URL, + options: { + tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: { + query: buildRouteValidationWithZod(ListEntitiesRequestQuery), + }, + }, + }, + + async (context, request, response): Promise> => { + const siemResponse = buildSiemResponse(response); + + try { + const { + page = 1, + per_page: perPage = 10, + sort_field: sortField = 'entity.lastSeenTimestamp', + sort_order: sortOrder = 'desc', + entities_types: entityTypes, + filterQuery, + } = request.query; + + const securitySolution = await context.securitySolution; + const entityStoreClient = securitySolution.getEntityStoreDataClient(); + const { records, total, inspect } = await entityStoreClient.searchEntities({ + entityTypes, + filterQuery, + page, + perPage, + sortField, + sortOrder, + }); + + return response.ok({ + body: { + records, + total, + page, + per_page: perPage, + inspect, + }, + }); + } catch (e) { + logger.error(e); + const error = transformError(e); + return siemResponse.error({ + statusCode: error.statusCode, + body: error.message, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts index b78316b02c91e..dfc9007486bf5 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/routes/register_entity_store_routes.ts @@ -7,6 +7,7 @@ import type { EntityAnalyticsRoutesDeps } from '../../types'; import { deleteEntityEngineRoute } from './delete'; +import { listEntitiesRoute } from './entities/list'; import { getEntityEngineRoute } from './get'; import { initEntityEngineRoute } from './init'; import { listEntityEnginesRoute } from './list'; @@ -20,4 +21,5 @@ export const registerEntityStoreRoutes = ({ router, logger }: EntityAnalyticsRou deleteEntityEngineRoute(router, logger); getEntityEngineRoute(router, logger); listEntityEnginesRoute(router, logger); + listEntitiesRoute(router, logger); }; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts index 864fdb2367eb5..ef6deec5899b7 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/entity_store/utils/utils.ts @@ -6,16 +6,22 @@ */ import type { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-server'; + +import { + ENTITY_LATEST, + ENTITY_SCHEMA_VERSION_V1, + entitiesIndexPattern, +} from '@kbn/entities-schema'; import type { EngineDescriptor, EntityType, } from '../../../../../common/api/entity_analytics/entity_store/common.gen'; -import { HOST_ENTITY_DEFINITION, USER_ENTITY_DEFINITION } from '../definition'; +import { buildHostEntityDefinition, buildUserEntityDefinition } from '../definition'; import { entityEngineDescriptorTypeName } from '../saved_object'; export const getEntityDefinition = (entityType: EntityType) => { - if (entityType === 'host') return HOST_ENTITY_DEFINITION; - if (entityType === 'user') return USER_ENTITY_DEFINITION; + if (entityType === 'host') return buildHostEntityDefinition(); + if (entityType === 'user') return buildUserEntityDefinition(); throw new Error(`Unsupported entity type: ${entityType}`); }; @@ -31,3 +37,12 @@ export const ensureEngineExists = export const getByEntityTypeQuery = (entityType: EntityType) => { return `${entityEngineDescriptorTypeName}.attributes.type: ${entityType}`; }; + +export const getEntitiesIndexName = (entityType: EntityType) => + entitiesIndexPattern({ + schemaVersion: ENTITY_SCHEMA_VERSION_V1, + dataset: ENTITY_LATEST, + definitionId: getEntityDefinitionId(entityType), + }); + +export const getEntityDefinitionId = (entityType: EntityType) => `ea_${entityType}_entity_store`; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 56be72871ea1c..5bf7ac87908d3 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -102,6 +102,7 @@ import { InitEntityStoreRequestBodyInput, } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/engine/init.gen'; import { InstallPrepackedTimelinesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/install_prepackaged_timelines/install_prepackaged_timelines_route.gen'; +import { ListEntitiesRequestQueryInput } from '@kbn/security-solution-plugin/common/api/entity_analytics/entity_store/entities/list_entities.gen'; import { PatchRuleRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/patch_rule/patch_rule_route.gen'; import { PatchTimelineRequestBodyInput } from '@kbn/security-solution-plugin/common/api/timeline/patch_timelines/patch_timeline_route.gen'; import { @@ -870,6 +871,17 @@ finalize it. .set(ELASTIC_HTTP_VERSION_HEADER, '1') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana'); }, + /** + * List entities records, paging, sorting and filtering as needed. + */ + listEntities(props: ListEntitiesProps) { + return supertest + .get('/api/entity_store/entities/list') + .set('kbn-xsrf', 'true') + .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') + .query(props.query); + }, listEntityStoreEngines() { return supertest .get('/api/entity_store/engines') @@ -1347,6 +1359,9 @@ export interface InitEntityStoreProps { export interface InstallPrepackedTimelinesProps { body: InstallPrepackedTimelinesRequestBodyInput; } +export interface ListEntitiesProps { + query: ListEntitiesRequestQueryInput; +} export interface PatchRuleProps { body: PatchRuleRequestBodyInput; } diff --git a/x-pack/test/functional/es_archives/security_solution/entity_store/data.json b/x-pack/test/functional/es_archives/security_solution/entity_store/data.json new file mode 100644 index 0000000000000..fdbf972691b84 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/entity_store/data.json @@ -0,0 +1,85 @@ +{ + "type": "doc", + "value": { + "id": "a4cf452c1e0375c3d4412cb550ad1783358468a3b3b777da4829d72c7d6fb74f", + "index": ".entities.v1.latest.ea_user_entity_store", + "source": { + "event": { + "ingested": "2024-09-11T11:26:49.706875Z" + }, + "user": { + "full_name": [], + "domain": [], + "roles": [], + "name": "hinamatsumoto", + "id": [], + "email": [], + "hash": [] + }, + "entity": { + "lastSeenTimestamp": "2024-09-11T11:24:15.588Z", + "schemaVersion": "v1", + "definitionVersion": "1.0.0", + "displayName": "hinamatsumoto", + "identityFields": [ + "user.name" + ], + "id": "LBQAgKHGmpup0Kg9nlKmeQ==", + "type": "node", + "firstSeenTimestamp": "2024-09-11T10:46:00.000Z", + "definitionId": "ea_user_entity_store" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "a2cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb71f", + "index": ".entities.v1.latest.ea_host_entity_store", + "source": { + "event": { + "ingested": "2024-09-11T11:26:49.641707Z" + }, + "host": { + "hostname": [ + "ali-ubuntu-server" + ], + "domain": [], + "ip": [ + "1050::6:600:300c:326c", + "192.168.1.10", + "1050::5:700:400d:427c", + "10.142.2.222" + ], + "name": "ali-ubuntu-server", + "id": [ + "new_host_id", + "b123c1d92f3821b748a7218b4e78125f" + ], + "type": [], + "mac": [ + "42-2b-ff-8e-ac-2f", + "51-3c-ff-9e-ac-2g" + ], + "architecture": [ + "x86_64" + ] + }, + "entity": { + "lastSeenTimestamp": "2024-09-11T11:24:15.591Z", + "schemaVersion": "v1", + "definitionVersion": "1.0.0", + "displayName": "ali-ubuntu-server", + "identityFields": [ + "host.name" + ], + "id": "ZXKm6GEcUJY6NHkMgPPmGQ==", + "type": "node", + "firstSeenTimestamp": "2024-09-11T10:46:00.000Z", + "definitionId": "ea_host_entity_store" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/security_solution/entity_store/mappings.json b/x-pack/test/functional/es_archives/security_solution/entity_store/mappings.json new file mode 100644 index 0000000000000..d532521bca5fb --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/entity_store/mappings.json @@ -0,0 +1,303 @@ +{ + "type": "index", + "value": { + "index": ".entities.v1.latest.ea_host_entity_store", + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "match_mapping_type": "string", + "mapping": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + { + "entity_metrics": { + "path_match": "entity.metrics.*", + "match_mapping_type": [ + "long", + "double" + ], + "mapping": { + "type": "{dynamic_type}" + } + } + } + ], + "properties": { + "entity": { + "properties": { + "definitionId": { + "type": "keyword", + "ignore_above": 1024 + }, + "definitionVersion": { + "type": "keyword", + "ignore_above": 1024 + }, + "displayName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "firstSeenTimestamp": { + "type": "date" + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "identityFields": { + "type": "keyword" + }, + "lastSeenTimestamp": { + "type": "date" + }, + "schemaVersion": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "event": { + "properties": { + "ingested": { + "type": "date" + } + } + }, + "host": { + "properties": { + "architecture": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "hostname": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "id": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "ip": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "mac": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "tags": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "index": ".entities.v1.latest.ea_user_entity_store", + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "match_mapping_type": "string", + "mapping": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + { + "entity_metrics": { + "path_match": "entity.metrics.*", + "match_mapping_type": [ + "long", + "double" + ], + "mapping": { + "type": "{dynamic_type}" + } + } + } + ], + "properties": { + "entity": { + "properties": { + "definitionId": { + "type": "keyword", + "ignore_above": 1024 + }, + "definitionVersion": { + "type": "keyword", + "ignore_above": 1024 + }, + "displayName": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "firstSeenTimestamp": { + "type": "date" + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "identityFields": { + "type": "keyword" + }, + "lastSeenTimestamp": { + "type": "date" + }, + "schemaVersion": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "event": { + "properties": { + "ingested": { + "type": "date" + } + } + }, + "labels": { + "type": "object" + }, + "tags": { + "type": "keyword", + "ignore_above": 1024 + }, + "user": { + "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "email": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "id": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024, + "fields": { + "text": { + "type": "text" + } + } + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts new file mode 100644 index 0000000000000..ba7a4c83e2ad7 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/ess.config.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile( + require.resolve('../../../../../config/ess/config.base.trial') + ); + + return { + ...functionalConfig.getAll(), + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['entityStoreEnabled'])}`, + ], + }, + testFiles: [require.resolve('..')], + junit: { + reportName: 'Entity Analytics - Entity Store Integration Tests - ESS Env - Trial License', + }, + }; +} diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/serverless.config.ts new file mode 100644 index 0000000000000..990bdd8778aeb --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/configs/serverless.config.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../../../../../config/serverless/config.base'; + +export default createTestConfig({ + kbnTestServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['entityStoreEnabled'])}`, + `--xpack.securitySolutionServerless.productTypes=${JSON.stringify([ + { product_line: 'security', product_tier: 'complete' }, + { product_line: 'endpoint', product_tier: 'complete' }, + { product_line: 'cloud', product_tier: 'complete' }, + ])}`, + ], + testFiles: [require.resolve('..')], + junit: { + reportName: + 'Entity Analytics - Entity Store Integration Tests - Serverless Env - Complete Tier', + }, +}); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts new file mode 100644 index 0000000000000..133f74a7a68c8 --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entities_list.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const securitySolutionApi = getService('securitySolutionApi'); + + describe('@ess @serverless @skipInServerlessMKI Entity store - Entities list API', () => { + describe('when the entity store is disable', () => { + it("should return response with success status when the index doesn't exist", async () => { + const { body } = await securitySolutionApi.listEntities({ + query: { entities_types: ['host'] }, + }); + + expect(body).toEqual( + expect.objectContaining({ + total: 0, + records: [], + }) + ); + }); + }); + + describe('when the entity store is enable', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/security_solution/entity_store'); + }); + + after(async () => { + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/entity_store' + ); + }); + + it('should return hosts in the entity store index', async () => { + const { body } = await securitySolutionApi.listEntities({ + query: { entities_types: ['host'] }, + }); + + expect(body.total).toEqual(1); + expect(body.records.length).toEqual(1); + }); + + it('should return users in the entity store index', async () => { + const { body } = await securitySolutionApi.listEntities({ + query: { entities_types: ['user'] }, + }); + + expect(body.total).toEqual(1); + expect(body.records.length).toEqual(1); + }); + + it('should return all entities in the entity store index', async () => { + const { body } = await securitySolutionApi.listEntities({ + query: { entities_types: ['user', 'host'] }, + }); + + expect(body.total).toEqual(2); + expect(body.records.length).toEqual(2); + }); + }); + }); +}; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts new file mode 100644 index 0000000000000..a043ea866d5eb --- /dev/null +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Entity Analytics - Entity Store', function () { + loadTestFile(require.resolve('./entities_list')); + }); +} From e524ed6a1aca0895833617b80c380713fac516bd Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:37:40 +1000 Subject: [PATCH 08/24] skip failing test suite (#193309) --- test/functional/apps/console/_misc_console_behavior.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_misc_console_behavior.ts b/test/functional/apps/console/_misc_console_behavior.ts index 68bcc6e7558cf..05cd34d1c2a7f 100644 --- a/test/functional/apps/console/_misc_console_behavior.ts +++ b/test/functional/apps/console/_misc_console_behavior.ts @@ -18,7 +18,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'console', 'header']); - describe('misc console behavior', function testMiscConsoleBehavior() { + // Failing: See https://github.com/elastic/kibana/issues/193309 + describe.skip('misc console behavior', function testMiscConsoleBehavior() { this.tags('includeFirefox'); before(async () => { await browser.setWindowSize(1200, 800); From 60176bcffdcbdb75b48823f4783923528797efe0 Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:45:41 +0100 Subject: [PATCH 09/24] [Security Solution][Detection Engine] log ES requests when running rule preview (#191107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **Status:** works only for **ES|QL and EQL** rule types When clicking on "Show Elasticsearch requests, ran during rule executions" preview would return logged Elasticsearch queries that can be used to debug/explore rule execution. Each rule execution accordion has time rule execution started and its duration. Upon opening accordion: it will display ES requests with their description and duration. **NOTE**: Only search requests are returned, not the requests that create actual alerts Feature flag: **loggingRequestsEnabled** On week Demo([internal link](https://drive.google.com/drive/folders/1l-cDhbiMxykNH6BzIxFAnLeibmV9a4Cz)) ### Video demo (older UI) https://github.com/user-attachments/assets/26f963da-c528-447c-9efd-350b4d42b52c ### Up to date UI #### UI control Screenshot 2024-09-11 at 12 39 07 #### List of executions and code blocks Screenshot 2024-09-11 at 12 38 23 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed 🎉 All tests passed! - [kibana-flaky-test-suite-runner#6909](https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6909) [✅] [Serverless] Security Solution Detection Engine - Cypress: 100/100 tests passed. [✅] Security Solution Detection Engine - Cypress: 100/100 tests passed. FTR tests - https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6918 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../rule_preview/rule_preview.gen.ts | 18 ++ .../rule_preview/rule_preview.schema.yaml | 23 ++ .../common/api/quickstart_client.gen.ts | 3 + .../common/experimental_features.ts | 5 + ...ections_api_2023_10_31.bundled.schema.yaml | 24 ++ ...ections_api_2023_10_31.bundled.schema.yaml | 24 ++ .../rule_preview/__mocks__/preview_logs.ts | 93 ++++++ .../components/rule_preview/index.test.tsx | 68 ++++- .../components/rule_preview/index.tsx | 48 ++- .../rule_preview/logged_requests.test.tsx | 68 +++++ .../rule_preview/logged_requests.tsx | 58 ++++ .../rule_preview/logged_requests_item.tsx | 79 +++++ .../rule_preview/optimized_accordion.test.tsx | 59 ++++ .../rule_preview/optimized_accordion.tsx | 39 +++ .../components/rule_preview/preview_logs.tsx | 20 +- .../components/rule_preview/translations.ts | 21 ++ .../rule_preview/use_accordion_styling.ts | 16 + .../rule_preview/use_preview_route.tsx | 3 + .../rule_preview/use_preview_rule.ts | 5 +- .../rule_management/api/api.test.ts | 15 + .../rule_management/api/api.ts | 2 + .../rule_management/logic/types.ts | 6 +- .../rule_preview/api/preview_rules/route.ts | 23 +- .../create_security_rule_type_wrapper.ts | 6 +- .../rule_types/eql/create_eql_alert_type.ts | 5 +- .../rule_types/eql/eql.test.ts | 8 +- .../detection_engine/rule_types/eql/eql.ts | 33 ++- .../detection_engine/rule_types/esql/esql.ts | 279 ++++++++++-------- .../rule_types/esql/fetch_source_documents.ts | 31 +- .../rule_types/translations.ts | 29 ++ .../lib/detection_engine/rule_types/types.ts | 9 +- .../rule_types/utils/logged_requests/index.ts | 10 + .../utils/logged_requests/log_eql.ts | 19 ++ .../utils/logged_requests/log_esql.ts | 15 + .../utils/logged_requests/log_query.ts | 35 +++ .../services/security_solution_api.gen.ts | 9 +- .../config/ess/config.base.ts | 1 + .../configs/serverless.config.ts | 5 +- .../execution_logic/eql.ts | 27 ++ .../execution_logic/esql.ts | 58 ++++ .../utils/rules/preview_rule.ts | 3 + .../test/security_solution_cypress/config.ts | 5 +- .../detection_engine/rule_edit/preview.cy.ts | 85 ++++++ .../cypress/screens/create_new_rule.ts | 16 + .../cypress/tasks/create_new_rule.ts | 20 ++ .../serverless_config.ts | 5 +- 46 files changed, 1268 insertions(+), 165 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts create mode 100644 x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts index 0e7fb75c2c4c2..ad9b6d9ea12c2 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.gen.ts @@ -15,6 +15,7 @@ */ import { z } from '@kbn/zod'; +import { BooleanFromString } from '@kbn/zod-helpers'; import { EqlRuleCreateProps, @@ -34,6 +35,13 @@ export const RulePreviewParams = z.object({ timeframeEnd: z.string().datetime(), }); +export type RulePreviewLoggedRequest = z.infer; +export const RulePreviewLoggedRequest = z.object({ + request: NonEmptyString, + description: NonEmptyString.optional(), + duration: z.number().int().optional(), +}); + export type RulePreviewLogs = z.infer; export const RulePreviewLogs = z.object({ errors: z.array(NonEmptyString), @@ -43,7 +51,17 @@ export const RulePreviewLogs = z.object({ */ duration: z.number().int(), startedAt: NonEmptyString.optional(), + requests: z.array(RulePreviewLoggedRequest).optional(), +}); + +export type RulePreviewRequestQuery = z.infer; +export const RulePreviewRequestQuery = z.object({ + /** + * Enables logging and returning in response ES queries, performed during rule execution + */ + enable_logged_requests: BooleanFromString.optional(), }); +export type RulePreviewRequestQueryInput = z.input; export type RulePreviewRequestBody = z.infer; export const RulePreviewRequestBody = z.discriminatedUnion('type', [ diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml index 1d65d6b4e037e..400b84e533a02 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_preview/rule_preview.schema.yaml @@ -11,6 +11,13 @@ paths: summary: Preview rule alerts generated on specified time range tags: - Rule preview API + parameters: + - name: enable_logged_requests + in: query + description: Enables logging and returning in response ES queries, performed during rule execution + required: false + schema: + type: boolean requestBody: description: An object containing tags to add or remove and alert ids the changes will be applied required: true @@ -94,6 +101,18 @@ components: format: date-time required: [invocationCount, timeframeEnd] + RulePreviewLoggedRequest: + type: object + properties: + request: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + description: + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + duration: + type: integer + required: + - request + RulePreviewLogs: type: object properties: @@ -110,6 +129,10 @@ components: description: Execution duration in milliseconds startedAt: $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' + requests: + type: array + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' required: - errors - warnings diff --git a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts index 5c9d33858d0f7..bb564dbe69b34 100644 --- a/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts +++ b/x-pack/plugins/security_solution/common/api/quickstart_client.gen.ts @@ -99,6 +99,7 @@ import type { GetRuleExecutionResultsResponse, } from './detection_engine/rule_monitoring/rule_execution_logs/get_rule_execution_results/get_rule_execution_results_route.gen'; import type { + RulePreviewRequestQueryInput, RulePreviewRequestBodyInput, RulePreviewResponse, } from './detection_engine/rule_preview/rule_preview.gen'; @@ -1763,6 +1764,7 @@ detection engine rules. }, method: 'POST', body: props.body, + query: props.query, }) .catch(catchAxiosErrorFormatAndThrow); } @@ -2160,6 +2162,7 @@ export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } export interface RulePreviewProps { + query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } export interface SearchAlertsProps { diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 4147404e940c1..030e00768349d 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -138,6 +138,11 @@ export const allowedExperimentalValues = Object.freeze({ */ esqlRulesDisabled: false, + /** + * enables logging requests during rule preview + */ + loggingRequestsEnabled: false, + /** * Enables Protection Updates tab in the Endpoint Policy Details page */ diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 0df64f51f37ce..b9c1ac658fd90 100644 --- a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -891,6 +891,15 @@ paths: /api/detection_engine/rules/preview: post: operationId: RulePreview + parameters: + - description: >- + Enables logging and returning in response ES queries, performed + during rule execution + in: query + name: enable_logged_requests + required: false + schema: + type: boolean requestBody: content: application/json: @@ -5178,6 +5187,17 @@ components: - $ref: '#/components/schemas/MachineLearningRulePatchProps' - $ref: '#/components/schemas/NewTermsRulePatchProps' - $ref: '#/components/schemas/EsqlRulePatchProps' + RulePreviewLoggedRequest: + type: object + properties: + description: + $ref: '#/components/schemas/NonEmptyString' + duration: + type: integer + request: + $ref: '#/components/schemas/NonEmptyString' + required: + - request RulePreviewLogs: type: object properties: @@ -5188,6 +5208,10 @@ components: items: $ref: '#/components/schemas/NonEmptyString' type: array + requests: + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' + type: array startedAt: $ref: '#/components/schemas/NonEmptyString' warnings: diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml index 4699de9a25228..d1de42913c4e0 100644 --- a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_detections_api_2023_10_31.bundled.schema.yaml @@ -476,6 +476,15 @@ paths: /api/detection_engine/rules/preview: post: operationId: RulePreview + parameters: + - description: >- + Enables logging and returning in response ES queries, performed + during rule execution + in: query + name: enable_logged_requests + required: false + schema: + type: boolean requestBody: content: application/json: @@ -4331,6 +4340,17 @@ components: - $ref: '#/components/schemas/MachineLearningRulePatchProps' - $ref: '#/components/schemas/NewTermsRulePatchProps' - $ref: '#/components/schemas/EsqlRulePatchProps' + RulePreviewLoggedRequest: + type: object + properties: + description: + $ref: '#/components/schemas/NonEmptyString' + duration: + type: integer + request: + $ref: '#/components/schemas/NonEmptyString' + required: + - request RulePreviewLogs: type: object properties: @@ -4341,6 +4361,10 @@ components: items: $ref: '#/components/schemas/NonEmptyString' type: array + requests: + items: + $ref: '#/components/schemas/RulePreviewLoggedRequest' + type: array startedAt: $ref: '#/components/schemas/NonEmptyString' warnings: diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts new file mode 100644 index 0000000000000..1e380d1bb4561 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/__mocks__/preview_logs.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RulePreviewLogs } from '../../../../../../common/api/detection_engine'; + +export const previewLogs: RulePreviewLogs[] = [ + { + errors: [], + warnings: [], + startedAt: '2024-09-05T15:43:46.972Z', + duration: 149, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T15:43:46.972Z",\n "gte": "2024-09-05T15:22:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 23, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "yB7awpEBluhaSO8ejVKZ",\n "yR7awpEBluhaSO8ejVKZ",\n "yh7awpEBluhaSO8ejVKZ",\n "yx7awpEBluhaSO8ejVKZ",\n "zB7awpEBluhaSO8ejVKZ",\n "zR7awpEBluhaSO8ejVKZ",\n "zh7awpEBluhaSO8ejVKZ",\n "zx7awpEBluhaSO8ejVKZ",\n "0B7awpEBluhaSO8ejVKZ",\n "0R7awpEBluhaSO8ejVKZ",\n "0h7awpEBluhaSO8ejVKZ",\n "0x7awpEBluhaSO8ejVKZ",\n "1B7awpEBluhaSO8ejVKZ",\n "1R7awpEBluhaSO8ejVKZ",\n "1h7awpEBluhaSO8ejVKZ",\n "1x7awpEBluhaSO8ejVKZ",\n "2B7awpEBluhaSO8ejVKZ",\n "2R7awpEBluhaSO8ejVKZ",\n "2h7awpEBluhaSO8ejVKZ",\n "2x7awpEBluhaSO8ejVKZ",\n "3B7awpEBluhaSO8ejVKZ",\n "3R7awpEBluhaSO8ejVKZ",\n "3h7awpEBluhaSO8ejVKZ",\n "3x7awpEBluhaSO8ejVKZ",\n "4B7awpEBluhaSO8ejVKZ",\n "4R7awpEBluhaSO8ejVKZ",\n "4h7awpEBluhaSO8ejVKZ",\n "4x7awpEBluhaSO8ejVKZ",\n "5B7awpEBluhaSO8ejVKZ",\n "5R7awpEBluhaSO8ejVKZ",\n "5h7awpEBluhaSO8ejVKZ",\n "5x7awpEBluhaSO8ejVKZ",\n "6B7awpEBluhaSO8ejVKZ",\n "6R7awpEBluhaSO8ejVKZ",\n "6h7awpEBluhaSO8ejVKZ",\n "6x7awpEBluhaSO8ejVKZ",\n "7B7awpEBluhaSO8ejVKZ",\n "7R7awpEBluhaSO8ejVKZ",\n "7h7awpEBluhaSO8ejVKZ",\n "7x7awpEBluhaSO8ejVKZ",\n "8B7awpEBluhaSO8ejVKZ",\n "8R7awpEBluhaSO8ejVKZ",\n "8h7awpEBluhaSO8ejVKZ",\n "8x7awpEBluhaSO8ejVKZ",\n "9B7awpEBluhaSO8ejVKZ",\n "9R7awpEBluhaSO8ejVKZ",\n "9h7awpEBluhaSO8ejVKZ",\n "9x7awpEBluhaSO8ejVKZ",\n "-B7awpEBluhaSO8ejVKZ",\n "-R7awpEBluhaSO8ejVKZ",\n "-h7awpEBluhaSO8ejVKZ",\n "-x7awpEBluhaSO8ejVKZ",\n "_B7awpEBluhaSO8ejVKZ",\n "_R7awpEBluhaSO8ejVKZ",\n "_h7awpEBluhaSO8ejVKZ",\n "_x7awpEBluhaSO8ejVKZ",\n "AB7awpEBluhaSO8ejVOZ",\n "AR7awpEBluhaSO8ejVOZ",\n "Ah7awpEBluhaSO8ejVOZ",\n "Ax7awpEBluhaSO8ejVOZ",\n "BB7awpEBluhaSO8ejVOZ",\n "BR7awpEBluhaSO8ejVOZ",\n "Bh7awpEBluhaSO8ejVOZ",\n "Bx7awpEBluhaSO8ejVOZ",\n "CB7awpEBluhaSO8ejVOZ",\n "CR7awpEBluhaSO8ejVOZ",\n "Ch7awpEBluhaSO8ejVOZ",\n "Cx7awpEBluhaSO8ejVOZ",\n "DB7awpEBluhaSO8ejVOZ",\n "DR7awpEBluhaSO8ejVOZ",\n "Dh7awpEBluhaSO8ejVOZ",\n "Dx7awpEBluhaSO8ejVOZ",\n "EB7awpEBluhaSO8ejVOZ",\n "ER7awpEBluhaSO8ejVOZ",\n "Eh7awpEBluhaSO8ejVOZ",\n "Ex7awpEBluhaSO8ejVOZ",\n "FB7awpEBluhaSO8ejVOZ",\n "FR7awpEBluhaSO8ejVOZ",\n "Fh7awpEBluhaSO8ejVOZ",\n "Fx7awpEBluhaSO8ejVOZ",\n "GB7awpEBluhaSO8ejVOZ",\n "GR7awpEBluhaSO8ejVOZ",\n "Gh7awpEBluhaSO8ejVOZ",\n "Gx7awpEBluhaSO8ejVOZ",\n "HB7awpEBluhaSO8ejVOZ",\n "HR7awpEBluhaSO8ejVOZ",\n "Hh7awpEBluhaSO8ejVOZ",\n "Hx7awpEBluhaSO8ejVOZ",\n "IB7awpEBluhaSO8ejVOZ",\n "IR7awpEBluhaSO8ejVOZ",\n "Ih7awpEBluhaSO8ejVOZ",\n "Ix7awpEBluhaSO8ejVOZ",\n "JB7awpEBluhaSO8ejVOZ",\n "JR7awpEBluhaSO8ejVOZ",\n "Jh7awpEBluhaSO8ejVOZ",\n "Jx7awpEBluhaSO8ejVOZ",\n "KB7awpEBluhaSO8ejVOZ",\n "KR7awpEBluhaSO8ejVOZ",\n "Kh7awpEBluhaSO8ejVOZ",\n "Kx7awpEBluhaSO8ejVOZ",\n "LB7awpEBluhaSO8ejVOZ"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 8, + }, + ], + }, + { + errors: [], + warnings: [], + startedAt: '2024-09-05T16:03:46.972Z', + duration: 269, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 30, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "yB7awpEBluhaSO8ejVKZ",\n "yR7awpEBluhaSO8ejVKZ",\n "yh7awpEBluhaSO8ejVKZ",\n "yx7awpEBluhaSO8ejVKZ",\n "zB7awpEBluhaSO8ejVKZ",\n "zR7awpEBluhaSO8ejVKZ",\n "zh7awpEBluhaSO8ejVKZ",\n "zx7awpEBluhaSO8ejVKZ",\n "0B7awpEBluhaSO8ejVKZ",\n "0R7awpEBluhaSO8ejVKZ",\n "0h7awpEBluhaSO8ejVKZ",\n "0x7awpEBluhaSO8ejVKZ",\n "1B7awpEBluhaSO8ejVKZ",\n "1R7awpEBluhaSO8ejVKZ",\n "1h7awpEBluhaSO8ejVKZ",\n "1x7awpEBluhaSO8ejVKZ",\n "2B7awpEBluhaSO8ejVKZ",\n "2R7awpEBluhaSO8ejVKZ",\n "2h7awpEBluhaSO8ejVKZ",\n "2x7awpEBluhaSO8ejVKZ",\n "3B7awpEBluhaSO8ejVKZ",\n "3R7awpEBluhaSO8ejVKZ",\n "3h7awpEBluhaSO8ejVKZ",\n "3x7awpEBluhaSO8ejVKZ",\n "4B7awpEBluhaSO8ejVKZ",\n "4R7awpEBluhaSO8ejVKZ",\n "4h7awpEBluhaSO8ejVKZ",\n "4x7awpEBluhaSO8ejVKZ",\n "5B7awpEBluhaSO8ejVKZ",\n "5R7awpEBluhaSO8ejVKZ",\n "5h7awpEBluhaSO8ejVKZ",\n "5x7awpEBluhaSO8ejVKZ",\n "6B7awpEBluhaSO8ejVKZ",\n "6R7awpEBluhaSO8ejVKZ",\n "6h7awpEBluhaSO8ejVKZ",\n "6x7awpEBluhaSO8ejVKZ",\n "7B7awpEBluhaSO8ejVKZ",\n "7R7awpEBluhaSO8ejVKZ",\n "7h7awpEBluhaSO8ejVKZ",\n "7x7awpEBluhaSO8ejVKZ",\n "8B7awpEBluhaSO8ejVKZ",\n "8R7awpEBluhaSO8ejVKZ",\n "8h7awpEBluhaSO8ejVKZ",\n "8x7awpEBluhaSO8ejVKZ",\n "9B7awpEBluhaSO8ejVKZ",\n "9R7awpEBluhaSO8ejVKZ",\n "9h7awpEBluhaSO8ejVKZ",\n "9x7awpEBluhaSO8ejVKZ",\n "-B7awpEBluhaSO8ejVKZ",\n "-R7awpEBluhaSO8ejVKZ",\n "-h7awpEBluhaSO8ejVKZ",\n "-x7awpEBluhaSO8ejVKZ",\n "_B7awpEBluhaSO8ejVKZ",\n "_R7awpEBluhaSO8ejVKZ",\n "_h7awpEBluhaSO8ejVKZ",\n "_x7awpEBluhaSO8ejVKZ",\n "AB7awpEBluhaSO8ejVOZ",\n "AR7awpEBluhaSO8ejVOZ",\n "Ah7awpEBluhaSO8ejVOZ",\n "Ax7awpEBluhaSO8ejVOZ",\n "BB7awpEBluhaSO8ejVOZ",\n "BR7awpEBluhaSO8ejVOZ",\n "Bh7awpEBluhaSO8ejVOZ",\n "Bx7awpEBluhaSO8ejVOZ",\n "CB7awpEBluhaSO8ejVOZ",\n "CR7awpEBluhaSO8ejVOZ",\n "Ch7awpEBluhaSO8ejVOZ",\n "Cx7awpEBluhaSO8ejVOZ",\n "DB7awpEBluhaSO8ejVOZ",\n "DR7awpEBluhaSO8ejVOZ",\n "Dh7awpEBluhaSO8ejVOZ",\n "Dx7awpEBluhaSO8ejVOZ",\n "EB7awpEBluhaSO8ejVOZ",\n "ER7awpEBluhaSO8ejVOZ",\n "Eh7awpEBluhaSO8ejVOZ",\n "Ex7awpEBluhaSO8ejVOZ",\n "FB7awpEBluhaSO8ejVOZ",\n "FR7awpEBluhaSO8ejVOZ",\n "Fh7awpEBluhaSO8ejVOZ",\n "Fx7awpEBluhaSO8ejVOZ",\n "GB7awpEBluhaSO8ejVOZ",\n "GR7awpEBluhaSO8ejVOZ",\n "Gh7awpEBluhaSO8ejVOZ",\n "Gx7awpEBluhaSO8ejVOZ",\n "HB7awpEBluhaSO8ejVOZ",\n "HR7awpEBluhaSO8ejVOZ",\n "Hh7awpEBluhaSO8ejVOZ",\n "Hx7awpEBluhaSO8ejVOZ",\n "IB7awpEBluhaSO8ejVOZ",\n "IR7awpEBluhaSO8ejVOZ",\n "Ih7awpEBluhaSO8ejVOZ",\n "Ix7awpEBluhaSO8ejVOZ",\n "JB7awpEBluhaSO8ejVOZ",\n "JR7awpEBluhaSO8ejVOZ",\n "Jh7awpEBluhaSO8ejVOZ",\n "Jx7awpEBluhaSO8ejVOZ",\n "KB7awpEBluhaSO8ejVOZ",\n "KR7awpEBluhaSO8ejVOZ",\n "Kh7awpEBluhaSO8ejVOZ",\n "Kx7awpEBluhaSO8ejVOZ",\n "LB7awpEBluhaSO8ejVOZ"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 6, + }, + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 201",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "LB7awpEBluhaSO8ejVOZ",\n "LR7awpEBluhaSO8ejVOZ",\n "Lh7awpEBluhaSO8ejVOZ",\n "Lx7awpEBluhaSO8ejVOZ",\n "MB7awpEBluhaSO8ejVOZ",\n "MR7awpEBluhaSO8ejVOZ",\n "Mh7awpEBluhaSO8ejVOZ",\n "Mx7awpEBluhaSO8ejVOZ",\n "NB7awpEBluhaSO8ejVOZ",\n "NR7awpEBluhaSO8ejVOZ",\n "Nh7awpEBluhaSO8ejVOZ",\n "Nx7awpEBluhaSO8ejVOZ",\n "OB7awpEBluhaSO8ejVOZ",\n "OR7awpEBluhaSO8ejVOZ",\n "Oh7awpEBluhaSO8ejVOZ",\n "Ox7awpEBluhaSO8ejVOZ",\n "PB7awpEBluhaSO8ejVOZ",\n "PR7awpEBluhaSO8ejVOZ",\n "Ph7awpEBluhaSO8ejVOZ",\n "Px7awpEBluhaSO8ejVOZ",\n "QB7awpEBluhaSO8ejVOZ",\n "QR7awpEBluhaSO8ejVOZ",\n "Qh7awpEBluhaSO8ejVOZ",\n "Qx7awpEBluhaSO8ejVOZ",\n "RB7awpEBluhaSO8ejVOZ",\n "RR7awpEBluhaSO8ejVOZ",\n "Rh7awpEBluhaSO8ejVOZ",\n "Rx7awpEBluhaSO8ejVOZ",\n "SB7awpEBluhaSO8ejVOZ",\n "SR7awpEBluhaSO8ejVOZ",\n "Sx7awpEBluhaSO8ewFOg",\n "TB7awpEBluhaSO8ewFOg",\n "TR7awpEBluhaSO8ewFOg",\n "Th7awpEBluhaSO8ewFOg",\n "Tx7awpEBluhaSO8ewFOg",\n "UB7awpEBluhaSO8ewFOg",\n "UR7awpEBluhaSO8ewFOg",\n "Uh7awpEBluhaSO8ewFOh",\n "Ux7awpEBluhaSO8ewFOh",\n "VB7awpEBluhaSO8ewFOh",\n "VR7awpEBluhaSO8ewFOh",\n "Vh7awpEBluhaSO8ewFOh",\n "Vx7awpEBluhaSO8ewFOh",\n "WB7awpEBluhaSO8ewFOh",\n "WR7awpEBluhaSO8ewFOh",\n "Wh7awpEBluhaSO8ewFOh",\n "Wx7awpEBluhaSO8ewFOh",\n "XB7awpEBluhaSO8ewFOh",\n "XR7awpEBluhaSO8ewFOh",\n "Xh7awpEBluhaSO8ewFOh",\n "Xx7awpEBluhaSO8ewFOh",\n "YB7awpEBluhaSO8ewFOh",\n "YR7awpEBluhaSO8ewFOh",\n "Yh7awpEBluhaSO8ewFOh",\n "Yx7awpEBluhaSO8ewFOh",\n "ZB7awpEBluhaSO8ewFOh",\n "ZR7awpEBluhaSO8ewFOh",\n "Zh7awpEBluhaSO8ewFOh",\n "Zx7awpEBluhaSO8ewFOh",\n "aB7awpEBluhaSO8ewFOh",\n "aR7awpEBluhaSO8ewFOh",\n "ah7awpEBluhaSO8ewFOh",\n "ax7awpEBluhaSO8ewFOh",\n "bB7awpEBluhaSO8ewFOh",\n "bR7awpEBluhaSO8ewFOh",\n "bh7awpEBluhaSO8ewFOh",\n "bx7awpEBluhaSO8ewFOh",\n "cB7awpEBluhaSO8ewFOh",\n "cR7awpEBluhaSO8ewFOh",\n "ch7awpEBluhaSO8ewFOh",\n "cx7awpEBluhaSO8ewFOh",\n "dB7awpEBluhaSO8ewFOh",\n "dR7awpEBluhaSO8ewFOh",\n "dh7awpEBluhaSO8ewFOh",\n "dx7awpEBluhaSO8ewFOh",\n "eB7awpEBluhaSO8ewFOh",\n "eR7awpEBluhaSO8ewFOh",\n "eh7awpEBluhaSO8ewFOh",\n "ex7awpEBluhaSO8ewFOh",\n "fB7awpEBluhaSO8ewFOh",\n "fR7awpEBluhaSO8ewFOh",\n "fh7awpEBluhaSO8ewFOh",\n "fx7awpEBluhaSO8ewFOh",\n "gB7awpEBluhaSO8ewFOh",\n "gR7awpEBluhaSO8ewFOh",\n "gh7awpEBluhaSO8ewFOh",\n "gx7awpEBluhaSO8ewFOh",\n "hB7awpEBluhaSO8ewFOh",\n "hR7awpEBluhaSO8ewFOh",\n "hh7awpEBluhaSO8ewFOh",\n "hx7awpEBluhaSO8ewFOh",\n "iB7awpEBluhaSO8ewFOh",\n "iR7awpEBluhaSO8ewFOh",\n "ih7awpEBluhaSO8ewFOh",\n "ix7awpEBluhaSO8ewFOh",\n "jB7awpEBluhaSO8ewFOh",\n "jR7awpEBluhaSO8ewFOh",\n "jh7awpEBluhaSO8ewFOh",\n "jx7awpEBluhaSO8ewFOh",\n "kB7awpEBluhaSO8ewFOh",\n "kR7awpEBluhaSO8ewFOh"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 8, + }, + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 301",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:03:46.972Z",\n "gte": "2024-09-05T15:42:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "kR7awpEBluhaSO8ewFOh",\n "kh7awpEBluhaSO8ewFOh",\n "kx7awpEBluhaSO8ewFOh",\n "lB7awpEBluhaSO8ewFOh",\n "lR7awpEBluhaSO8ewFOh",\n "lh7awpEBluhaSO8ewFOh",\n "lx7awpEBluhaSO8ewFOh",\n "mB7awpEBluhaSO8ewFOh",\n "mR7awpEBluhaSO8ewFOh",\n "mh7awpEBluhaSO8ewFOh",\n "mx7awpEBluhaSO8ewFOh",\n "nB7awpEBluhaSO8ewFOh",\n "nR7awpEBluhaSO8ewFOh",\n "nh7awpEBluhaSO8ewFOh",\n "nx7awpEBluhaSO8ewFOh",\n "oB7awpEBluhaSO8ewFOh",\n "oR7awpEBluhaSO8ewFOh",\n "oh7awpEBluhaSO8ewFOh",\n "ox7awpEBluhaSO8ewFOh",\n "pB7awpEBluhaSO8ewFOh",\n "pR7awpEBluhaSO8ewFOh",\n "ph7awpEBluhaSO8ewFOh",\n "px7awpEBluhaSO8ewFOh",\n "qB7awpEBluhaSO8ewFOh",\n "qR7awpEBluhaSO8ewFOh",\n "qh7awpEBluhaSO8ewFOh",\n "qx7awpEBluhaSO8ewFOh",\n "rB7awpEBluhaSO8ewFOh",\n "rR7awpEBluhaSO8ewFOh",\n "rh7awpEBluhaSO8ewFOh",\n "rx7awpEBluhaSO8ewFOh",\n "sB7awpEBluhaSO8ewFOh",\n "sR7awpEBluhaSO8ewFOh",\n "sh7awpEBluhaSO8ewFOh",\n "sx7awpEBluhaSO8ewFOh",\n "tB7awpEBluhaSO8ewFOh",\n "tR7awpEBluhaSO8ewFOh",\n "th7awpEBluhaSO8ewFOh",\n "tx7awpEBluhaSO8ewFOh",\n "uB7awpEBluhaSO8ewFOh",\n "uR7awpEBluhaSO8ewFOh",\n "uh7awpEBluhaSO8ewFOh",\n "ux7awpEBluhaSO8ewFOh",\n "vB7awpEBluhaSO8ewFOh",\n "vR7awpEBluhaSO8ewFOh",\n "vh7awpEBluhaSO8ewFOh",\n "vx7awpEBluhaSO8ewFOh",\n "wB7awpEBluhaSO8ewFOh",\n "wR7awpEBluhaSO8ewFOh",\n "wh7awpEBluhaSO8ewFOh",\n "wx7awpEBluhaSO8ewFOh",\n "xB7awpEBluhaSO8ewFOh",\n "xR7awpEBluhaSO8ewFOh",\n "xh7awpEBluhaSO8ewFOh",\n "xx7awpEBluhaSO8ewFOh",\n "yB7awpEBluhaSO8ewFOh",\n "yR7awpEBluhaSO8ewFOh",\n "yh7awpEBluhaSO8ewFOh",\n "yx7awpEBluhaSO8ewFOh",\n "zB7awpEBluhaSO8ewFOh",\n "zR7awpEBluhaSO8ewFOh",\n "zh7awpEBluhaSO8ewFOh",\n "zx7awpEBluhaSO8ewFOh",\n "0B7awpEBluhaSO8ewFOh",\n "0R7awpEBluhaSO8ewFOh",\n "0h7awpEBluhaSO8ewFOh",\n "0x7awpEBluhaSO8ewFOh",\n "1B7awpEBluhaSO8ewFOh",\n "1R7awpEBluhaSO8ewFOh",\n "1h7awpEBluhaSO8ewFOh",\n "1x7awpEBluhaSO8ewFOh",\n "2B7awpEBluhaSO8ewFOh",\n "2R7awpEBluhaSO8ewFOh",\n "2h7awpEBluhaSO8ewFOh",\n "2x7awpEBluhaSO8ewFOh",\n "3B7awpEBluhaSO8ewFOh",\n "3R7awpEBluhaSO8ewFOh",\n "3h7awpEBluhaSO8ewFOh",\n "3x7awpEBluhaSO8ewFOh",\n "4B7awpEBluhaSO8ewFOh",\n "4R7awpEBluhaSO8ewFOh",\n "4h7awpEBluhaSO8ewFOh",\n "4x7awpEBluhaSO8ewFOh",\n "5B7awpEBluhaSO8ewFOh",\n "5R7awpEBluhaSO8ewFOh",\n "5h7awpEBluhaSO8ewFOh",\n "6h7awpEBluhaSO8e51Pb",\n "6x7awpEBluhaSO8e51Pb",\n "7B7awpEBluhaSO8e51Pb",\n "7R7awpEBluhaSO8e51Pb",\n "7h7awpEBluhaSO8e51Pb",\n "7x7awpEBluhaSO8e51Pb",\n "8B7awpEBluhaSO8e51Pb",\n "8R7awpEBluhaSO8e51Pb",\n "8h7awpEBluhaSO8e51Pb",\n "8x7awpEBluhaSO8e51Pb",\n "9B7awpEBluhaSO8e51Pb",\n "9R7awpEBluhaSO8e51Pb",\n "9h7awpEBluhaSO8e51Pb",\n "9x7awpEBluhaSO8e51Pb",\n "-B7awpEBluhaSO8e51Pb"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 7, + }, + ], + }, + { + errors: [], + warnings: [], + startedAt: '2024-09-05T16:23:46.972Z', + duration: 103, + requests: [ + { + request: + 'POST _query\n{\n "query": "FROM packetbeat-8.14.2 metadata _id, _version, _index | limit 101",\n "filter": {\n "bool": {\n "filter": [\n {\n "range": {\n "@timestamp": {\n "lte": "2024-09-05T16:23:46.972Z",\n "gte": "2024-09-05T16:02:46.972Z",\n "format": "strict_date_optional_time"\n }\n }\n },\n {\n "bool": {\n "must": [],\n "filter": [],\n "should": [],\n "must_not": []\n }\n }\n ]\n }\n }\n}', + description: 'ES|QL request to find all matches', + duration: 19, + }, + { + request: + 'POST /packetbeat-8.14.2/_search?ignore_unavailable=true\n{\n "query": {\n "bool": {\n "filter": {\n "ids": {\n "values": [\n "_B7_wpEBluhaSO8enqFT",\n "_R7_wpEBluhaSO8enqFT",\n "_h7_wpEBluhaSO8enqFT",\n "_x7_wpEBluhaSO8enqFT",\n "AB7_wpEBluhaSO8enqJT",\n "AR7_wpEBluhaSO8enqJT",\n "Ah7_wpEBluhaSO8enqJT",\n "Ax7_wpEBluhaSO8enqJT",\n "BB7_wpEBluhaSO8enqJT",\n "BR7_wpEBluhaSO8enqJT",\n "Bh7_wpEBluhaSO8enqJT",\n "Bx7_wpEBluhaSO8enqJT",\n "CB7_wpEBluhaSO8enqJT",\n "CR7_wpEBluhaSO8enqJT",\n "Ch7_wpEBluhaSO8enqJT",\n "Cx7_wpEBluhaSO8enqJT",\n "DB7_wpEBluhaSO8enqJT",\n "DR7_wpEBluhaSO8enqJT",\n "Dh7_wpEBluhaSO8enqJT",\n "Dx7_wpEBluhaSO8enqJT",\n "EB7_wpEBluhaSO8enqJT",\n "ER7_wpEBluhaSO8enqJT",\n "Eh7_wpEBluhaSO8enqJT",\n "Ex7_wpEBluhaSO8enqJT",\n "FB7_wpEBluhaSO8enqJT",\n "FR7_wpEBluhaSO8enqJT",\n "Fh7_wpEBluhaSO8enqJT",\n "Fx7_wpEBluhaSO8enqJT",\n "GB7_wpEBluhaSO8enqJT",\n "GR7_wpEBluhaSO8enqJT",\n "Gh7_wpEBluhaSO8enqJT",\n "Gx7_wpEBluhaSO8enqJT",\n "tR7wwpEBluhaSO8efnLO",\n "th7wwpEBluhaSO8efnLO",\n "tx7wwpEBluhaSO8efnLO",\n "uB7wwpEBluhaSO8efnLO",\n "uR7wwpEBluhaSO8efnLO",\n "uh7wwpEBluhaSO8efnLO",\n "ux7wwpEBluhaSO8efnLO",\n "vB7wwpEBluhaSO8efnLO",\n "vR7wwpEBluhaSO8efnLO",\n "vh7wwpEBluhaSO8efnLO",\n "vx7wwpEBluhaSO8efnLO",\n "wB7wwpEBluhaSO8efnLO",\n "wR7wwpEBluhaSO8efnLO",\n "wh7wwpEBluhaSO8efnLO",\n "wx7wwpEBluhaSO8efnLO",\n "xB7wwpEBluhaSO8efnLO",\n "xR7wwpEBluhaSO8efnLO",\n "xh7wwpEBluhaSO8efnLO",\n "xx7wwpEBluhaSO8efnLO",\n "yB7wwpEBluhaSO8efnLO",\n "yR7wwpEBluhaSO8efnLO",\n "yh7wwpEBluhaSO8efnLO",\n "yx7wwpEBluhaSO8efnLO",\n "zB7wwpEBluhaSO8efnLO",\n "zR7wwpEBluhaSO8efnLO",\n "zh7wwpEBluhaSO8efnLO",\n "zx7wwpEBluhaSO8efnLO",\n "0B7wwpEBluhaSO8efnLO",\n "0R7wwpEBluhaSO8efnLO",\n "0h7wwpEBluhaSO8efnLO",\n "0x7wwpEBluhaSO8efnLO",\n "1B7wwpEBluhaSO8efnLO",\n "1B7twpEBluhaSO8eu1-P",\n "1R7twpEBluhaSO8eu1-P",\n "1h7twpEBluhaSO8eu1-P",\n "1x7twpEBluhaSO8eu1-P",\n "2B7twpEBluhaSO8eu1-P",\n "2R7twpEBluhaSO8eu1-P",\n "2h7twpEBluhaSO8eu1-P",\n "2x7twpEBluhaSO8eu1-P",\n "3B7twpEBluhaSO8eu1-P",\n "3R7twpEBluhaSO8eu1-P",\n "3h7twpEBluhaSO8eu1-P",\n "3x7twpEBluhaSO8eu1-P",\n "4B7twpEBluhaSO8eu1-P",\n "4R7twpEBluhaSO8eu1-P",\n "4h7twpEBluhaSO8eu1-P",\n "4x7twpEBluhaSO8eu1-P",\n "5B7twpEBluhaSO8eu1-P",\n "5R7twpEBluhaSO8eu1-P",\n "5h7twpEBluhaSO8eu1-P",\n "5x7twpEBluhaSO8eu1-P",\n "6B7twpEBluhaSO8eu1-P",\n "6R7twpEBluhaSO8eu1-P",\n "6h7twpEBluhaSO8eu1-P",\n "6x7twpEBluhaSO8eu1-P",\n "7B7twpEBluhaSO8eu1-P",\n "7R7twpEBluhaSO8eu1-P",\n "7h7twpEBluhaSO8eu1-P",\n "7x7twpEBluhaSO8eu1-P",\n "8B7twpEBluhaSO8eu1-P",\n "8R7twpEBluhaSO8eu1-P",\n "8h7twpEBluhaSO8eu1-P",\n "8x7twpEBluhaSO8eu1-P",\n "HB7_wpEBluhaSO8enqJT",\n "HR7_wpEBluhaSO8enqJT",\n "Hh7_wpEBluhaSO8enqJT",\n "Hx7_wpEBluhaSO8enqJT",\n "IB7_wpEBluhaSO8enqJT"\n ]\n }\n }\n }\n },\n "_source": false,\n "fields": [\n "*"\n ]\n}', + description: 'Retrieve source documents when ES|QL query is not aggregable', + duration: 5, + }, + ], + }, +]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx index 69eebec3452d5..4ebb460177476 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.test.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import type { DataViewBase } from '@kbn/es-query'; import { fields } from '@kbn/data-plugin/common/mocks'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { TestProviders } from '../../../../common/mock'; import type { RulePreviewProps } from '.'; @@ -22,6 +23,7 @@ import { stepDefineDefaultValue, } from '../../../../detections/pages/detection_engine/rules/utils'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_preview_route'); @@ -34,6 +36,21 @@ jest.mock('../../../../common/containers/use_global_time', () => ({ }), })); jest.mock('./use_preview_invocation_count'); +jest.mock('../../../../common/hooks/use_experimental_features', () => ({ + useIsExperimentalFeatureEnabled: jest.fn(), +})); + +const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +// rule types that do not support logged requests +const doNotSupportLoggedRequests: Type[] = [ + 'threshold', + 'threat_match', + 'machine_learning', + 'query', + 'new_terms', +]; + +const supportLoggedRequests: Type[] = ['esql', 'eql']; const getMockIndexPattern = (): DataViewBase => ({ fields, @@ -97,6 +114,8 @@ describe('PreviewQuery', () => { }); (usePreviewInvocationCount as jest.Mock).mockReturnValue({ invocationCount: 500 }); + + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); }); afterEach(() => { @@ -137,4 +156,51 @@ describe('PreviewQuery', () => { expect(await wrapper.findByTestId('previewInvocationCountWarning')).toBeTruthy(); }); + + supportLoggedRequests.forEach((ruleType) => { + test(`renders "Show Elasticsearch requests" for ${ruleType} rule type`, () => { + render( + + + + ); + + expect(screen.getByTestId('show-elasticsearch-requests')).toBeInTheDocument(); + }); + }); + + supportLoggedRequests.forEach((ruleType) => { + test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type when feature is disabled`, () => { + useIsExperimentalFeatureEnabledMock.mockReturnValue(false); + + render( + + + + ); + + expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); + }); + }); + + doNotSupportLoggedRequests.forEach((ruleType) => { + test(`does not render "Show Elasticsearch requests" for ${ruleType} rule type`, () => { + render( + + + + ); + + expect(screen.queryByTestId('show-elasticsearch-requests')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx index 8deb2ebd41863..2a86600d94e7a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/index.tsx @@ -18,9 +18,12 @@ import { EuiText, EuiTitle, EuiFormRow, + EuiCheckbox, } from '@elastic/eui'; import moment from 'moment'; import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; + import { isEqual } from 'lodash'; import * as i18n from './translations'; import { usePreviewRoute } from './use_preview_route'; @@ -37,9 +40,12 @@ import type { TimeframePreviewOptions, } from '../../../../detections/pages/detection_engine/rules/types'; import { usePreviewInvocationCount } from './use_preview_invocation_count'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; export const REASONABLE_INVOCATION_COUNT = 200; +const RULE_TYPES_SUPPORTING_LOGGED_REQUESTS: Type[] = ['esql', 'eql']; + const timeRanges = [ { start: 'now/d', end: 'now', label: 'Today' }, { start: 'now/w', end: 'now', label: 'This week' }, @@ -64,6 +70,7 @@ interface RulePreviewState { aboutRuleData?: AboutStepRule; scheduleRuleData?: ScheduleStepRule; timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; } const refreshedTimeframe = (startDate: string, endDate: string) => { @@ -83,6 +90,8 @@ const RulePreviewComponent: React.FC = ({ const { indexPattern, ruleType } = defineRuleData; const { spaces } = useKibana().services; + const isLoggingRequestsFeatureEnabled = useIsExperimentalFeatureEnabled('loggingRequestsEnabled'); + const [spaceId, setSpaceId] = useState(''); useEffect(() => { if (spaces) { @@ -98,6 +107,8 @@ const RulePreviewComponent: React.FC = ({ const [timeframeStart, setTimeframeStart] = useState(moment().subtract(1, 'hour')); const [timeframeEnd, setTimeframeEnd] = useState(moment()); + const [showElasticsearchRequests, setShowElasticsearchRequests] = useState(false); + const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); useEffect(() => { @@ -140,6 +151,7 @@ const RulePreviewComponent: React.FC = ({ scheduleRuleData: previewData.scheduleRuleData, exceptionsList, timeframeOptions: previewData.timeframeOptions, + enableLoggedRequests: previewData.enableLoggedRequests, }); const { startTransaction } = useStartTransaction(); @@ -185,9 +197,18 @@ const RulePreviewComponent: React.FC = ({ interval: scheduleRuleData.interval, lookback: scheduleRuleData.from, }, + enableLoggedRequests: showElasticsearchRequests, }); setIsRefreshing(true); - }, [aboutRuleData, defineRuleData, endDate, scheduleRuleData, startDate, startTransaction]); + }, [ + aboutRuleData, + defineRuleData, + endDate, + scheduleRuleData, + startDate, + startTransaction, + showElasticsearchRequests, + ]); const isDirty = useMemo( () => @@ -261,6 +282,24 @@ const RulePreviewComponent: React.FC = ({ + {isLoggingRequestsFeatureEnabled && + RULE_TYPES_SUPPORTING_LOGGED_REQUESTS.includes(ruleType) ? ( + + + + { + setShowElasticsearchRequests(!showElasticsearchRequests); + }} + /> + + + + ) : null} {isPreviewRequestInProgress && } {!isPreviewRequestInProgress && previewId && spaceId && ( @@ -273,7 +312,12 @@ const RulePreviewComponent: React.FC = ({ timeframeOptions={previewData.timeframeOptions} /> )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx new file mode 100644 index 0000000000000..c0cf8870c162a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { TestProviders } from '../../../../common/mock/test_providers'; +import { LoggedRequests } from './logged_requests'; + +import { previewLogs } from './__mocks__/preview_logs'; + +describe('LoggedRequests', () => { + it('should not render component if logs are empty', () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeNull(); + }); + + it('should open accordion on click and render list of request items', async () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Preview logged requests')); + + expect(screen.getAllByTestId('preview-logged-requests-item-accordion')).toHaveLength(3); + }); + + it('should render code content on logged request item accordion click', async () => { + render(, { wrapper: TestProviders }); + + expect(screen.queryByTestId('preview-logged-requests-accordion')).toBeInTheDocument(); + + await userEvent.click(screen.getByText('Preview logged requests')); + + // picking up second rule execution + const loggedRequestsItem = screen.getAllByTestId('preview-logged-requests-item-accordion')[1]; + + expect(loggedRequestsItem).toHaveTextContent('Rule execution started at'); + expect(loggedRequestsItem).toHaveTextContent('[269ms]'); + + await userEvent.click(loggedRequestsItem.querySelector('button') as HTMLElement); + + expect(screen.getAllByTestId('preview-logged-request-description')).toHaveLength(6); + expect(screen.getAllByTestId('preview-logged-request-code-block')).toHaveLength(6); + + expect(screen.getAllByTestId('preview-logged-request-description')[0]).toHaveTextContent( + 'ES|QL request to find all matches [30ms]' + ); + + expect(screen.getAllByTestId('preview-logged-request-code-block')[0]).toHaveTextContent( + /FROM packetbeat-8\.14\.2 metadata _id, _version, _index \| limit 101/ + ); + + expect(screen.getAllByTestId('preview-logged-request-description')[1]).toHaveTextContent( + 'Retrieve source documents when ES|QL query is not aggregable' + ); + + expect(screen.getAllByTestId('preview-logged-request-code-block')[1]).toHaveTextContent( + /POST \/packetbeat-8\.14\.2\/_search\?ignore_unavailable=true/ + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx new file mode 100644 index 0000000000000..d7b62c6f08c69 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC } from 'react'; +import React, { useMemo } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { css } from '@emotion/css'; + +import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { OptimizedAccordion } from './optimized_accordion'; +import { LoggedRequestsItem } from './logged_requests_item'; +import { useAccordionStyling } from './use_accordion_styling'; + +const LoggedRequestsComponent: FC<{ logs: RulePreviewLogs[] }> = ({ logs }) => { + const cssStyles = useAccordionStyling(); + + const AccordionContent = useMemo( + () => ( + <> + + {logs.map((log) => ( + + + + ))} + + ), + [logs] + ); + + if (logs.length === 0) { + return null; + } + + return ( + <> + + {AccordionContent} + + + ); +}; + +export const LoggedRequests = React.memo(LoggedRequestsComponent); +LoggedRequests.displayName = 'LoggedRequests'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx new file mode 100644 index 0000000000000..2f2e7d74bf826 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/logged_requests_item.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC, PropsWithChildren } from 'react'; +import React from 'react'; +import { css } from '@emotion/css'; + +import { EuiSpacer, EuiCodeBlock, useEuiPaddingSize, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; +import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; +import { OptimizedAccordion } from './optimized_accordion'; +import { useAccordionStyling } from './use_accordion_styling'; + +const LoggedRequestsItemComponent: FC> = ({ + startedAt, + duration, + requests, +}) => { + const paddingLarge = useEuiPaddingSize('l'); + const cssStyles = useAccordionStyling(); + + return ( + + {startedAt ? ( + }} + /> + ) : ( + i18n.LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON + )} + {`[${duration}ms]`} + + } + id={`ruleExecution-${startedAt}`} + css={css` + margin-left: ${paddingLarge}; + ${cssStyles} + `} + > + {(requests ?? []).map((request, key) => ( + + + + {request?.description ?? null} {request?.duration ? `[${request.duration}ms]` : null} + + + + {request.request} + + + ))} + + ); +}; + +export const LoggedRequestsItem = React.memo(LoggedRequestsItemComponent); +LoggedRequestsItem.displayName = 'LoggedRequestsItem'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx new file mode 100644 index 0000000000000..bab5751aa6ddf --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { OptimizedAccordion } from './optimized_accordion'; + +describe('OptimizedAccordion', () => { + it('should not render children content if accordion initially closed', () => { + render( + + {'content'} + + ); + + expect(screen.queryByText('content')).toBeNull(); + }); + it('should render children content if accordion initially opened', () => { + render( + + {'content'} + + ); + + expect(screen.getByText('content')).toBeInTheDocument(); + }); + it('should render children content when accordion opened', async () => { + render( + + {'content'} + + ); + + const toggleButton = screen.getByText('accordion button'); + await userEvent.click(toggleButton); + + expect(screen.getByText('content')).toBeVisible(); + }); + it('should not destroy children content when accordion closed', async () => { + render( + + {'content'} + + ); + + const toggleButton = screen.getByText('accordion button'); + await userEvent.click(toggleButton); + + expect(screen.getByText('content')).toBeVisible(); + + await userEvent.click(toggleButton); + expect(screen.getByText('content')).not.toBeVisible(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx new file mode 100644 index 0000000000000..5d3ad0ab874be --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/optimized_accordion.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FC } from 'react'; + +import React, { useState } from 'react'; + +import type { EuiAccordionProps } from '@elastic/eui'; +import { EuiAccordion } from '@elastic/eui'; + +/** + * component does not render children before it was opened + * once children rendered for the first time, they won't be re-rendered on subsequent accordion toggling + */ +const OptimizedAccordionComponent: FC = ({ children, ...props }) => { + const [trigger, setTrigger] = useState<'closed' | 'open'>('closed'); + const [isRendered, setIsRendered] = useState(false); + + const onToggle = (isOpen: boolean) => { + const newState = isOpen ? 'open' : 'closed'; + if (isOpen) { + setIsRendered(true); + } + setTrigger(newState); + }; + + return ( + + {isRendered || props.forceState === 'open' ? children : null} + + ); +}; + +export const OptimizedAccordion = React.memo(OptimizedAccordionComponent); +OptimizedAccordion.displayName = 'OptimizedAccordion'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx index 912df2453cc46..196408cbc1371 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/preview_logs.tsx @@ -7,14 +7,19 @@ import type { FC, PropsWithChildren } from 'react'; import React, { Fragment, useMemo } from 'react'; +import { css } from '@emotion/css'; import { EuiCallOut, EuiText, EuiSpacer, EuiAccordion } from '@elastic/eui'; + import type { RulePreviewLogs } from '../../../../../common/api/detection_engine'; import * as i18n from './translations'; +import { LoggedRequests } from './logged_requests'; +import { useAccordionStyling } from './use_accordion_styling'; interface PreviewLogsProps { logs: RulePreviewLogs[]; hasNoiseWarning: boolean; isAborted: boolean; + showElasticsearchRequests: boolean; } interface SortedLogs { @@ -43,7 +48,12 @@ const addLogs = ( allLogs: SortedLogs[] ) => (logs.length ? [{ startedAt, logs, duration }, ...allLogs] : allLogs); -const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarning, isAborted }) => { +const PreviewLogsComponent: React.FC = ({ + logs, + hasNoiseWarning, + isAborted, + showElasticsearchRequests, +}) => { const sortedLogs = useMemo( () => logs.reduce<{ @@ -66,6 +76,7 @@ const PreviewLogsComponent: React.FC = ({ logs, hasNoiseWarnin {isAborted ? : null} + {showElasticsearchRequests ? : null} ); }; @@ -74,6 +85,8 @@ export const PreviewLogs = React.memo(PreviewLogsComponent); PreviewLogs.displayName = 'PreviewLogs'; const LogAccordion: FC> = ({ logs, isError, children }) => { + const cssStyles = useAccordionStyling(); + const firstLog = logs[0]; if (!(children || firstLog)) return null; @@ -96,6 +109,10 @@ const LogAccordion: FC> = ({ logs, isError, buttonContent={ isError ? i18n.QUERY_PREVIEW_SEE_ALL_ERRORS : i18n.QUERY_PREVIEW_SEE_ALL_WARNINGS } + borders="horizontal" + css={css` + ${cssStyles} + `} > {restOfLogs.map((log, key) => ( > = ({ logs, isError, ))} ) : null} - ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts index 4ab9328ee806b..0b25071a76830 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/translations.ts @@ -158,6 +158,27 @@ export const VIEW_DETAILS = i18n.translate( } ); +export const ENABLED_LOGGED_REQUESTS_CHECKBOX = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.enabledLoggedRequestsLabel', + { + defaultMessage: 'Show Elasticsearch requests, ran during rule executions', + } +); + +export const LOGGED_REQUESTS_ACCORDION_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.loggedRequestsAccordionButtonLabel', + { + defaultMessage: 'Preview logged requests', + } +); + +export const LOGGED_REQUEST_ITEM_ACCORDION_UNKNOWN_TIME_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.loggedRequestItemAccordionUnknownTimeButtonLabel', + { + defaultMessage: 'Preview logged requests', + } +); + export const VIEW_DETAILS_FOR_ROW = ({ ariaRowindex, columnValues, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts new file mode 100644 index 0000000000000..be05d90836c94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_accordion_styling.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEuiPaddingSize } from '@elastic/eui'; + +export const useAccordionStyling = () => { + const paddingLarge = useEuiPaddingSize('l'); + const paddingSmall = useEuiPaddingSize('s'); + + return `padding-bottom: ${paddingLarge}; + padding-top: ${paddingSmall};`; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx index 25952956b2538..5684819106ada 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_route.tsx @@ -24,6 +24,7 @@ interface PreviewRouteParams { scheduleRuleData?: ScheduleStepRule; exceptionsList?: List[]; timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; } export const usePreviewRoute = ({ @@ -32,6 +33,7 @@ export const usePreviewRoute = ({ scheduleRuleData, exceptionsList, timeframeOptions, + enableLoggedRequests, }: PreviewRouteParams) => { const [isRequestTriggered, setIsRequestTriggered] = useState(false); @@ -41,6 +43,7 @@ export const usePreviewRoute = ({ const { isLoading, response, rule, setRule } = usePreviewRule({ timeframeOptions, + enableLoggedRequests, }); const [logs, setLogs] = useState(response.logs ?? []); const [isAborted, setIsAborted] = useState(!!response.isAborted); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts index 9fb7417bca036..05c3b9fe10299 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/use_preview_rule.ts @@ -27,8 +27,10 @@ const emptyPreviewRule: RulePreviewResponse = { export const usePreviewRule = ({ timeframeOptions, + enableLoggedRequests, }: { timeframeOptions: TimeframePreviewOptions; + enableLoggedRequests?: boolean; }) => { const [rule, setRule] = useState(null); const [response, setResponse] = useState(emptyPreviewRule); @@ -66,6 +68,7 @@ export const usePreviewRule = ({ invocationCount, timeframeEnd, }, + enableLoggedRequests, signal: abortCtrl.signal, }); if (isSubscribed) { @@ -87,7 +90,7 @@ export const usePreviewRule = ({ isSubscribed = false; abortCtrl.abort(); }; - }, [rule, addError, invocationCount, from, interval, timeframeEnd]); + }, [rule, addError, invocationCount, from, interval, timeframeEnd, enableLoggedRequests]); return { isLoading, response, rule, setRule }; }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts index 5180f9fb891f0..d10bb4bb03e08 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.test.ts @@ -117,6 +117,21 @@ describe('Detections Rules API', () => { expect.objectContaining({ body: '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1","invocationCount":1,"timeframeEnd":"2015-03-12 05:17:10"}', method: 'POST', + query: undefined, + }) + ); + }); + + test('sends enable_logged_requests in URL query', async () => { + const payload = getCreateRulesSchemaMock(); + await previewRule({ + rule: { ...payload, invocationCount: 1, timeframeEnd: '2015-03-12 05:17:10' }, + enableLoggedRequests: true, + }); + expect(fetchMock).toHaveBeenCalledWith( + '/api/detection_engine/rules/preview', + expect.objectContaining({ + query: { enable_logged_requests: true }, }) ); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts index 4254b58234400..c86606d0d8137 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/api/api.ts @@ -150,6 +150,7 @@ export const patchRule = async ({ */ export const previewRule = async ({ rule, + enableLoggedRequests, signal, }: PreviewRulesProps): Promise => KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_PREVIEW, { @@ -157,6 +158,7 @@ export const previewRule = async ({ version: '2023-10-31', body: JSON.stringify(rule), signal, + query: enableLoggedRequests ? { enable_logged_requests: enableLoggedRequests } : undefined, }); /** diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index 526d0a00389d7..e12442c97aa4c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -32,7 +32,11 @@ export interface CreateRulesProps { } export interface PreviewRulesProps { - rule: RuleCreateProps & { invocationCount: number; timeframeEnd: string }; + rule: RuleCreateProps & { + invocationCount: number; + timeframeEnd: string; + }; + enableLoggedRequests?: boolean; signal?: AbortSignal; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts index 52f4d3739b1e2..50542592aa1d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_preview/api/preview_rules/route.ts @@ -31,7 +31,11 @@ import type { RulePreviewResponse, RulePreviewLogs, } from '../../../../../../common/api/detection_engine'; -import { RulePreviewRequestBody } from '../../../../../../common/api/detection_engine'; +import { + RulePreviewRequestBody, + RulePreviewRequestQuery, +} from '../../../../../../common/api/detection_engine'; +import type { RulePreviewLoggedRequest } from '../../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { StartPlugins, SetupPlugins } from '../../../../../plugin'; import { buildSiemResponse } from '../../../routes/utils'; @@ -92,7 +96,12 @@ export const previewRulesRoute = ( .addVersion( { version: '2023-10-31', - validate: { request: { body: buildRouteValidationWithZod(RulePreviewRequestBody) } }, + validate: { + request: { + body: buildRouteValidationWithZod(RulePreviewRequestBody), + query: buildRouteValidationWithZod(RulePreviewRequestQuery), + }, + }, }, async (context, request, response): Promise> => { const siemResponse = buildSiemResponse(response); @@ -143,7 +152,9 @@ export const previewRulesRoute = ( const username = security?.authc.getCurrentUser(request)?.username; const loggedStatusChanges: Array = []; const previewRuleExecutionLogger = createPreviewRuleExecutionLogger(loggedStatusChanges); - const runState: Record = {}; + const runState: Record = { + isLoggedRequestsEnabled: request.query.enable_logged_requests, + }; const logs: RulePreviewLogs[] = []; let isAborted = false; @@ -224,6 +235,7 @@ export const previewRulesRoute = ( } ) => { let statePreview = runState as TState; + let loggedRequests = []; const abortController = new AbortController(); setTimeout(() => { @@ -268,7 +280,7 @@ export const previewRulesRoute = ( while (invocationCount > 0 && !isAborted) { invocationStartTime = moment(); - ({ state: statePreview } = (await executor({ + ({ state: statePreview, loggedRequests } = (await executor({ executionId: uuidv4(), params, previousStartedAt, @@ -302,7 +314,7 @@ export const previewRulesRoute = ( const date = startedAt.toISOString(); return { dateStart: date, dateEnd: date }; }, - })) as { state: TState }); + })) as { state: TState; loggedRequests: RulePreviewLoggedRequest[] }); const errors = loggedStatusChanges .filter((item) => item.newStatus === RuleExecutionStatusEnum.failed) @@ -317,6 +329,7 @@ export const previewRulesRoute = ( warnings, startedAt: startedAt.toDate().toISOString(), duration: moment().diff(invocationStartTime, 'milliseconds'), + ...(loggedRequests ? { requests: loggedRequests } : {}), }); loggedStatusChanges.length = 0; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 1e5e70a37ae5f..f25a8429089b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -467,6 +467,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = warning: warningMessages.length > 0, warningMessages, userError: runResult.userError, + ...(runResult.loggedRequests ? { loggedRequests: runResult.loggedRequests } : {}), }; runState = runResult.state; } @@ -571,7 +572,10 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); } - return { state: result.state }; + return { + state: result.state, + ...(result.loggedRequests ? { loggedRequests: result.loggedRequests } : {}), + }; }); }, alerts: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index ca16b38404e48..9de8641d7b17c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -111,7 +111,7 @@ export const createEqlAlertType = ( alertSuppression: completeRule.ruleParams.alertSuppression, licensing, }); - const result = await eqlExecutor({ + const { result, loggedRequests } = await eqlExecutor({ completeRule, tuple, inputIndex, @@ -131,9 +131,10 @@ export const createEqlAlertType = ( alertWithSuppression, isAlertSuppressionActive: isNonSeqAlertSuppressionActive, experimentalFeatures, + state, scheduleNotificationResponseActionsService, }); - return { ...result, state }; + return { ...result, state, ...(loggedRequests ? { loggedRequests } : {}) }; }, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts index 9ef9faeb9de3a..4f5aa7d322c9e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.test.ts @@ -54,7 +54,7 @@ describe('eql_executor', () => { describe('eqlExecutor', () => { describe('warning scenarios', () => { it('warns when exception list for eql rule contains value list exceptions', async () => { - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, @@ -105,7 +105,7 @@ describe('eql_executor', () => { }, }); - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: ruleWithSequenceAndSuppression, @@ -140,7 +140,7 @@ describe('eql_executor', () => { message: 'verification_exception\n\tRoot causes:\n\t\tverification_exception: Found 1 problem\nline 1:1: Unknown column [event.category]', }); - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, @@ -165,7 +165,7 @@ describe('eql_executor', () => { }); it('should handle scheduleNotificationResponseActionsService call', async () => { - const result = await eqlExecutor({ + const { result } = await eqlExecutor({ inputIndex: DEFAULT_INDEX_PATTERN, runtimeMappings: {}, completeRule: eqlCompleteRule, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts index 3379d0a0c6867..47e298392d7d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/eql.ts @@ -46,6 +46,9 @@ import type { import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; import { bulkCreateSuppressedAlertsInMemory } from '../utils/bulk_create_suppressed_alerts_in_memory'; import { getDataTierFilter } from '../utils/get_data_tier_filter'; +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; +import { logEqlRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; interface EqlExecutorParams { inputIndex: string[]; @@ -67,6 +70,7 @@ interface EqlExecutorParams { alertWithSuppression: SuppressedAlertService; isAlertSuppressionActive: boolean; experimentalFeatures: ExperimentalFeatures; + state?: Record; scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; } @@ -90,10 +94,17 @@ export const eqlExecutor = async ({ alertWithSuppression, isAlertSuppressionActive, experimentalFeatures, + state, scheduleNotificationResponseActionsService, -}: EqlExecutorParams): Promise => { +}: EqlExecutorParams): Promise<{ + result: SearchAfterAndBulkCreateReturnType; + loggedRequests?: RulePreviewLoggedRequest[]; +}> => { const ruleParams = completeRule.ruleParams; + const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; + const loggedRequests: RulePreviewLoggedRequest[] = []; + // eslint-disable-next-line complexity return withSecuritySpan('eqlExecutor', async () => { const result = createSearchAfterReturnType(); @@ -125,13 +136,24 @@ export const eqlExecutor = async ({ const eqlSignalSearchStart = performance.now(); try { + if (isLoggedRequestsEnabled) { + loggedRequests.push({ + request: logEqlRequest(request), + description: i18n.EQL_SEARCH_REQUEST_DESCRIPTION, + }); + } + const response = await services.scopedClusterClient.asCurrentUser.eql.search( request ); const eqlSignalSearchEnd = performance.now(); - const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); - result.searchAfterTimes = [eqlSearchDuration]; + const eqlSearchDuration = eqlSignalSearchEnd - eqlSignalSearchStart; + result.searchAfterTimes = [makeFloatString(eqlSearchDuration)]; + + if (isLoggedRequestsEnabled && loggedRequests[0]) { + loggedRequests[0].duration = Math.round(eqlSearchDuration); + } let newSignals: Array> | undefined; @@ -198,8 +220,7 @@ export const eqlExecutor = async ({ responseActions: completeRule.ruleParams.responseActions, }); } - - return result; + return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } catch (error) { if ( typeof error.message === 'string' && @@ -211,7 +232,7 @@ export const eqlExecutor = async ({ } result.errors.push(error.message); result.success = false; - return result; + return { result, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; } }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts index 0dd2b0e50d4ba..1e5b1749e94f5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/esql.ts @@ -27,8 +27,11 @@ import { createEnrichEventsFunction } from '../utils/enrichments'; import { rowToDocument } from './utils'; import { fetchSourceDocuments } from './fetch_source_documents'; import { buildReasonMessageForEsqlAlert } from '../utils/reason_formatters'; - +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { RunOpts, SignalSource, CreateRuleAdditionalOptions } from '../types'; +import { logEsqlRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; + import { addToSearchAfterReturn, createSearchAfterReturnType, @@ -66,13 +69,14 @@ export const esqlExecutor = async ({ }: { runOpts: RunOpts; services: RuleExecutorServices; - state: object; + state: Record; spaceId: string; version: string; experimentalFeatures: ExperimentalFeatures; licensing: LicensingPluginSetup; scheduleNotificationResponseActionsService: CreateRuleAdditionalOptions['scheduleNotificationResponseActionsService']; }) => { + const loggedRequests: RulePreviewLoggedRequest[] = []; const ruleParams = completeRule.ruleParams; /** * ES|QL returns results as a single page. max size of 10,000 @@ -80,91 +84,80 @@ export const esqlExecutor = async ({ * we don't want to overload ES/Kibana with large responses */ const ESQL_PAGE_SIZE_CIRCUIT_BREAKER = tuple.maxSignals * 3; + const isLoggedRequestsEnabled = state?.isLoggedRequestsEnabled ?? false; return withSecuritySpan('esqlExecutor', async () => { const result = createSearchAfterReturnType(); let size = tuple.maxSignals; - while ( - result.createdSignalsCount <= tuple.maxSignals && - size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER - ) { - const esqlRequest = buildEsqlSearchRequest({ - query: ruleParams.query, - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - size, - filters: [], - primaryTimestamp, - secondaryTimestamp, - exceptionFilter, - }); - - ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); - const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); - if (exceptionsWarning) { - result.warningMessages.push(exceptionsWarning); - } + try { + while ( + result.createdSignalsCount <= tuple.maxSignals && + size <= ESQL_PAGE_SIZE_CIRCUIT_BREAKER + ) { + const esqlRequest = buildEsqlSearchRequest({ + query: ruleParams.query, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + size, + filters: [], + primaryTimestamp, + secondaryTimestamp, + exceptionFilter, + }); - const esqlSignalSearchStart = performance.now(); + if (isLoggedRequestsEnabled) { + loggedRequests.push({ + request: logEsqlRequest(esqlRequest), + description: i18n.ESQL_SEARCH_REQUEST_DESCRIPTION, + }); + } - const response = await performEsqlRequest({ - esClient: services.scopedClusterClient.asCurrentUser, - requestParams: esqlRequest, - }); + ruleExecutionLogger.debug(`ES|QL query request: ${JSON.stringify(esqlRequest)}`); + const exceptionsWarning = getUnprocessedExceptionsWarnings(unprocessedExceptions); + if (exceptionsWarning) { + result.warningMessages.push(exceptionsWarning); + } - const esqlSearchDuration = makeFloatString(performance.now() - esqlSignalSearchStart); - result.searchAfterTimes.push(esqlSearchDuration); + const esqlSignalSearchStart = performance.now(); - ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`); + const response = await performEsqlRequest({ + esClient: services.scopedClusterClient.asCurrentUser, + requestParams: esqlRequest, + }); - const isRuleAggregating = computeIsESQLQueryAggregating(completeRule.ruleParams.query); + const esqlSearchDuration = performance.now() - esqlSignalSearchStart; + result.searchAfterTimes.push(makeFloatString(esqlSearchDuration)); - const results = response.values - // slicing already processed results in previous iterations - .slice(size - tuple.maxSignals) - .map((row) => rowToDocument(response.columns, row)); + if (isLoggedRequestsEnabled && loggedRequests[0]) { + loggedRequests[0].duration = Math.round(esqlSearchDuration); + } - const index = getIndexListFromEsqlQuery(completeRule.ruleParams.query); + ruleExecutionLogger.debug(`ES|QL query request took: ${esqlSearchDuration}ms`); - const sourceDocuments = await fetchSourceDocuments({ - esClient: services.scopedClusterClient.asCurrentUser, - results, - index, - isRuleAggregating, - }); + const isRuleAggregating = computeIsESQLQueryAggregating(completeRule.ruleParams.query); - const isAlertSuppressionActive = await getIsAlertSuppressionActive({ - alertSuppression: completeRule.ruleParams.alertSuppression, - licensing, - }); + const results = response.values + // slicing already processed results in previous iterations + .slice(size - tuple.maxSignals) + .map((row) => rowToDocument(response.columns, row)); - const wrapHits = (events: Array>) => - wrapEsqlAlerts({ - events, - spaceId, - completeRule, - mergeStrategy, + const index = getIndexListFromEsqlQuery(completeRule.ruleParams.query); + + const sourceDocuments = await fetchSourceDocuments({ + esClient: services.scopedClusterClient.asCurrentUser, + results, + index, isRuleAggregating, - alertTimestampOverride, - ruleExecutionLogger, - publicBaseUrl, - tuple, + loggedRequests: isLoggedRequestsEnabled ? loggedRequests : undefined, }); - const syntheticHits: Array> = results.map((document) => { - const { _id, _version, _index, ...source } = document; - - return { - _source: source as SignalSource, - fields: _id ? sourceDocuments[_id]?.fields : {}, - _id: _id ?? '', - _index: _index ?? '', - }; - }); + const isAlertSuppressionActive = await getIsAlertSuppressionActive({ + alertSuppression: completeRule.ruleParams.alertSuppression, + licensing, + }); - if (isAlertSuppressionActive) { - const wrapSuppressedHits = (events: Array>) => - wrapSuppressedEsqlAlerts({ + const wrapHits = (events: Array>) => + wrapEsqlAlerts({ events, spaceId, completeRule, @@ -173,78 +166,108 @@ export const esqlExecutor = async ({ alertTimestampOverride, ruleExecutionLogger, publicBaseUrl, - primaryTimestamp, - secondaryTimestamp, tuple, }); - const bulkCreateResult = await bulkCreateSuppressedAlertsInMemory({ - enrichedEvents: syntheticHits, - toReturn: result, - wrapHits, - bulkCreate, - services, - ruleExecutionLogger, - tuple, - alertSuppression: completeRule.ruleParams.alertSuppression, - wrapSuppressedHits, - alertTimestampOverride, - alertWithSuppression, - experimentalFeatures, - buildReasonMessage: buildReasonMessageForEsqlAlert, - mergeSourceAndFields: true, - // passing 1 here since ES|QL does not support pagination - maxNumberOfAlertsMultiplier: 1, + const syntheticHits: Array> = results.map((document) => { + const { _id, _version, _index, ...source } = document; + + return { + _source: source as SignalSource, + fields: _id ? sourceDocuments[_id]?.fields : {}, + _id: _id ?? '', + _index: _index ?? '', + }; }); - ruleExecutionLogger.debug( - `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` - ); + if (isAlertSuppressionActive) { + const wrapSuppressedHits = (events: Array>) => + wrapSuppressedEsqlAlerts({ + events, + spaceId, + completeRule, + mergeStrategy, + isRuleAggregating, + alertTimestampOverride, + ruleExecutionLogger, + publicBaseUrl, + primaryTimestamp, + secondaryTimestamp, + tuple, + }); - if (bulkCreateResult.alertsWereTruncated) { - result.warningMessages.push(getSuppressionMaxSignalsWarning()); - break; - } - } else { - const wrappedAlerts = wrapHits(syntheticHits); + const bulkCreateResult = await bulkCreateSuppressedAlertsInMemory({ + enrichedEvents: syntheticHits, + toReturn: result, + wrapHits, + bulkCreate, + services, + ruleExecutionLogger, + tuple, + alertSuppression: completeRule.ruleParams.alertSuppression, + wrapSuppressedHits, + alertTimestampOverride, + alertWithSuppression, + experimentalFeatures, + buildReasonMessage: buildReasonMessageForEsqlAlert, + mergeSourceAndFields: true, + // passing 1 here since ES|QL does not support pagination + maxNumberOfAlertsMultiplier: 1, + }); - const enrichAlerts = createEnrichEventsFunction({ - services, - logger: ruleExecutionLogger, - }); - const bulkCreateResult = await bulkCreate( - wrappedAlerts, - tuple.maxSignals - result.createdSignalsCount, - enrichAlerts - ); + ruleExecutionLogger.debug( + `Created ${bulkCreateResult.createdItemsCount} alerts. Suppressed ${bulkCreateResult.suppressedItemsCount} alerts` + ); - addToSearchAfterReturn({ current: result, next: bulkCreateResult }); - ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + if (bulkCreateResult.alertsWereTruncated) { + result.warningMessages.push(getSuppressionMaxSignalsWarning()); + break; + } + } else { + const wrappedAlerts = wrapHits(syntheticHits); - if (bulkCreateResult.alertsWereTruncated) { - result.warningMessages.push(getMaxSignalsWarning()); - break; + const enrichAlerts = createEnrichEventsFunction({ + services, + logger: ruleExecutionLogger, + }); + const bulkCreateResult = await bulkCreate( + wrappedAlerts, + tuple.maxSignals - result.createdSignalsCount, + enrichAlerts + ); + + addToSearchAfterReturn({ current: result, next: bulkCreateResult }); + ruleExecutionLogger.debug(`Created ${bulkCreateResult.createdItemsCount} alerts`); + + if (bulkCreateResult.alertsWereTruncated) { + result.warningMessages.push(getMaxSignalsWarning()); + break; + } + } + + if (scheduleNotificationResponseActionsService) { + scheduleNotificationResponseActionsService({ + signals: result.createdSignals, + signalsCount: result.createdSignalsCount, + responseActions: completeRule.ruleParams.responseActions, + }); } - } - if (scheduleNotificationResponseActionsService) { - scheduleNotificationResponseActionsService({ - signals: result.createdSignals, - signalsCount: result.createdSignalsCount, - responseActions: completeRule.ruleParams.responseActions, - }); - } - // no more results will be found - if (response.values.length < size) { - ruleExecutionLogger.debug( - `End of search: Found ${response.values.length} results with page size ${size}` - ); - break; + // no more results will be found + if (response.values.length < size) { + ruleExecutionLogger.debug( + `End of search: Found ${response.values.length} results with page size ${size}` + ); + break; + } + // ES|QL does not support pagination so we need to increase size of response to be able to catch all events + size += tuple.maxSignals; } - // ES|QL does not support pagination so we need to increase size of response to be able to catch all events - size += tuple.maxSignals; + } catch (error) { + result.errors.push(error.message); + result.success = false; } - return { ...result, state }; + return { ...result, state, ...(isLoggedRequestsEnabled ? { loggedRequests } : {}) }; }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts index 13828c0ed6770..7ee34183f0317 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/esql/fetch_source_documents.ts @@ -7,12 +7,16 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/types'; +import type { RulePreviewLoggedRequest } from '../../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; +import { logQueryRequest } from '../utils/logged_requests'; +import * as i18n from '../translations'; interface FetchSourceDocumentsArgs { isRuleAggregating: boolean; esClient: ElasticsearchClient; index: string[]; results: Array>; + loggedRequests?: RulePreviewLoggedRequest[]; } /** * fetches source documents by list of their ids @@ -24,6 +28,7 @@ export const fetchSourceDocuments = async ({ results, esClient, index, + loggedRequests, }: FetchSourceDocumentsArgs): Promise> => { const ids = results.reduce((acc, doc) => { if (doc._id) { @@ -47,16 +52,30 @@ export const fetchSourceDocuments = async ({ }, }; + const searchBody = { + query: idsQuery.query, + _source: false, + fields: ['*'], + }; + const ignoreUnavailable = true; + + if (loggedRequests) { + loggedRequests.push({ + request: logQueryRequest(searchBody, { index, ignoreUnavailable }), + description: i18n.FIND_SOURCE_DOCUMENTS_REQUEST_DESCRIPTION, + }); + } + const response = await esClient.search({ index, - body: { - query: idsQuery.query, - _source: false, - fields: ['*'], - }, - ignore_unavailable: true, + body: searchBody, + ignore_unavailable: ignoreUnavailable, }); + if (loggedRequests) { + loggedRequests[loggedRequests.length - 1].duration = response.took; + } + return response.hits.hits.reduce>( (acc, hit) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts new file mode 100644 index 0000000000000..88445b0d41dc3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ESQL_SEARCH_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.esqlSearchRequestDescription', + { + defaultMessage: 'ES|QL request to find all matches', + } +); + +export const FIND_SOURCE_DOCUMENTS_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.findSourceDocumentsRequestDescription', + { + defaultMessage: 'Retrieve source documents when ES|QL query is not aggregable', + } +); + +export const EQL_SEARCH_REQUEST_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.esqlRuleType.eqlSearchRequestDescription', + { + defaultMessage: 'EQL request to find all matches', + } +); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index a29beef7bbb20..78c0a729be10e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -35,6 +35,7 @@ import type { TypeOfFieldMap } from '@kbn/rule-registry-plugin/common/field_map' import type { Filter, DataViewFieldBase } from '@kbn/es-query'; import type { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; +import type { RulePreviewLoggedRequest } from '../../../../common/api/detection_engine/rule_preview/rule_preview.gen'; import type { RuleResponseAction } from '../../../../common/api/detection_engine/model/rule_response_actions'; import type { ConfigType } from '../../../config'; import type { SetupPlugins } from '../../../plugin'; @@ -74,6 +75,7 @@ export interface SecurityAlertTypeReturnValue { warning: boolean; warningMessages: string[]; suppressedAlertsCount?: number; + loggedRequests?: RulePreviewLoggedRequest[]; } export interface RunOpts { @@ -126,7 +128,12 @@ export type SecurityAlertType< services: PersistenceServices; runOpts: RunOpts; } - ) => Promise; + ) => Promise< + SearchAfterAndBulkCreateReturnType & { + state: TState; + loggedRequests?: RulePreviewLoggedRequest[]; + } + >; }; export interface CreateSecurityRuleTypeWrapperProps { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts new file mode 100644 index 0000000000000..7cea6e0d75fa6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './log_esql'; +export * from './log_eql'; +export * from './log_query'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts new file mode 100644 index 0000000000000..3f735e533b3b6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_eql.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EqlSearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export const logEqlRequest = (request: EqlSearchRequest): string => { + const allowNoIndices = + request.allow_no_indices != null ? `?allow_no_indices=${request.allow_no_indices}` : ''; + + return `POST /${request.index}/_eql/search${allowNoIndices}\n${JSON.stringify( + request.body, + null, + 2 + )}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts new file mode 100644 index 0000000000000..ea2d652dc5e5e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_esql.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export const logEsqlRequest = (esqlRequest: { + query: string; + filter: QueryDslQueryContainer; +}): string => { + return `POST _query\n${JSON.stringify(esqlRequest, null, 2)}`; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts new file mode 100644 index 0000000000000..8ea992793e31d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/utils/logged_requests/log_query.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + QueryDslQueryContainer, + SearchSourceConfig, + Indices, + Fields, +} from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +interface SearchRequest { + query?: QueryDslQueryContainer; + _source?: SearchSourceConfig; + fields?: Fields; +} + +interface LogQueryRequestParams { + index: Indices; + ignoreUnavailable?: boolean; +} + +export const logQueryRequest = ( + searchRequest: SearchRequest, + { index, ignoreUnavailable = false }: LogQueryRequestParams +): string => { + return `POST /${index}/_search?ignore_unavailable=${ignoreUnavailable}\n${JSON.stringify( + searchRequest, + null, + 2 + )}`; +}; diff --git a/x-pack/test/api_integration/services/security_solution_api.gen.ts b/x-pack/test/api_integration/services/security_solution_api.gen.ts index 5bf7ac87908d3..44f928e98bd0f 100644 --- a/x-pack/test/api_integration/services/security_solution_api.gen.ts +++ b/x-pack/test/api_integration/services/security_solution_api.gen.ts @@ -116,7 +116,10 @@ import { PreviewRiskScoreRequestBodyInput } from '@kbn/security-solution-plugin/ import { ReadAlertsMigrationStatusRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals_migration/read_signals_migration_status/read_signals_migration_status.gen'; import { ReadRuleRequestQueryInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_management/crud/read_rule/read_rule_route.gen'; import { ResolveTimelineRequestQueryInput } from '@kbn/security-solution-plugin/common/api/timeline/resolve_timeline/resolve_timeline_route.gen'; -import { RulePreviewRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; +import { + RulePreviewRequestQueryInput, + RulePreviewRequestBodyInput, +} from '@kbn/security-solution-plugin/common/api/detection_engine/rule_preview/rule_preview.gen'; import { SearchAlertsRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/query_signals/query_signals_route.gen'; import { SetAlertAssigneesRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen'; import { SetAlertsStatusRequestBodyInput } from '@kbn/security-solution-plugin/common/api/detection_engine/signals/set_signal_status/set_signals_status_route.gen'; @@ -1058,7 +1061,8 @@ detection engine rules. .set('kbn-xsrf', 'true') .set(ELASTIC_HTTP_VERSION_HEADER, '2023-10-31') .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana') - .send(props.body as object); + .send(props.body as object) + .query(props.query); }, scheduleRiskEngineNow() { return supertest @@ -1394,6 +1398,7 @@ export interface ResolveTimelineProps { query: ResolveTimelineRequestQueryInput; } export interface RulePreviewProps { + query: RulePreviewRequestQueryInput; body: RulePreviewRequestBodyInput; } export interface SearchAlertsProps { diff --git a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts index 6cd2702857e0f..705c0b8686dd0 100644 --- a/x-pack/test/security_solution_api_integration/config/ess/config.base.ts +++ b/x-pack/test/security_solution_api_integration/config/ess/config.base.ts @@ -82,6 +82,7 @@ export function createTestConfig(options: CreateTestConfigOptions, testFiles?: s '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'previewTelemetryUrlEnabled', + 'loggingRequestsEnabled', 'riskScoringPersistence', 'riskScoringRoutesEnabled', 'manualRuleRunEnabled', diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts index 6691a781d4e1e..8f64a859b7002 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/configs/serverless.config.ts @@ -17,6 +17,9 @@ export default createTestConfig({ 'testing_ignored.constant', '/testing_regex*/', ])}`, // See tests within the file "ignore_fields.ts" which use these values in "alertIgnoreFields" - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, ], }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts index 53b2843399c62..aff2ccc6bccb3 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/eql.ts @@ -1189,5 +1189,32 @@ export default ({ getService }: FtrProviderContext) => { expect(updatedAlerts.hits.hits[0]._source?.[ALERT_SUPPRESSION_DOCS_COUNT]).equal(1); }); }); + + // skipped on MKI since feature flags are not supported there + describe('@skipInServerlessMKI preview logged requests', () => { + it('should not return requests property when not enabled', async () => { + const { logs } = await previewRule({ + supertest, + rule: getEqlRuleForAlertTesting(['auditbeat-*']), + }); + + expect(logs[0].requests).equal(undefined); + }); + it('should return requests property when enable_logged_requests set to true', async () => { + const { logs } = await previewRule({ + supertest, + rule: getEqlRuleForAlertTesting(['auditbeat-*']), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + + expect(requests).to.have.length(1); + expect(requests![0].description).to.be('EQL request to find all matches'); + expect(requests![0].request).to.contain( + 'POST /auditbeat-*/_eql/search?allow_no_indices=true' + ); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts index 9fbda25bdae64..166a62b9b08ad 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/detection_engine/rule_execution_logic/trial_license_complete_tier/execution_logic/esql.ts @@ -1408,5 +1408,63 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + // skipped on MKI since feature flags are not supported there + describe('@skipInServerlessMKI preview logged requests', () => { + let rule: EsqlRuleCreateProps; + let id: string; + beforeEach(async () => { + id = uuidv4(); + const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z']; + const doc1 = { agent: { name: 'test-1' } }; + + rule = { + ...getCreateEsqlRulesSchemaMock('rule-1', true), + query: `from ecs_compliant metadata _id ${internalIdPipe( + id + )} | where agent.name=="test-1"`, + from: 'now-1h', + interval: '1h', + }; + + await indexEnhancedDocuments({ documents: [doc1], interval, id }); + }); + + it('should not return requests property when not enabled', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + }); + + expect(logs[0]).not.toHaveProperty('requests'); + }); + it('should return requests property when enable_logged_requests set to true', async () => { + const { logs } = await previewRule({ + supertest, + rule, + timeframeEnd: new Date('2020-10-28T06:30:00.000Z'), + enableLoggedRequests: true, + }); + + const requests = logs[0].requests; + expect(requests).toHaveLength(2); + + expect(requests).toHaveProperty('0.description', 'ES|QL request to find all matches'); + expect(requests).toHaveProperty('0.duration', expect.any(Number)); + expect(requests![0].request).toContain( + `"query": "from ecs_compliant metadata _id | where id==\\\"${id}\\\" | where agent.name==\\\"test-1\\\" | limit 101",` + ); + + expect(requests).toHaveProperty( + '1.description', + 'Retrieve source documents when ES|QL query is not aggregable' + ); + expect(requests).toHaveProperty('1.duration', expect.any(Number)); + expect(requests![1].request).toContain( + 'POST /ecs_compliant/_search?ignore_unavailable=true' + ); + }); + }); }); }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts index a601edde98168..79668b902f5d0 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/rules/preview_rule.ts @@ -26,11 +26,13 @@ export const previewRule = async ({ rule, invocationCount = 1, timeframeEnd = new Date(), + enableLoggedRequests, }: { supertest: SuperTest.Agent; rule: RuleCreateProps; invocationCount?: number; timeframeEnd?: Date; + enableLoggedRequests?: boolean; }): Promise<{ previewId: string; logs: RulePreviewLogs[]; @@ -43,6 +45,7 @@ export const previewRule = async ({ }; const response = await supertest .post(DETECTION_ENGINE_RULES_PREVIEW) + .query(enableLoggedRequests ? { enable_logged_requests: true } : {}) .set('kbn-xsrf', 'true') .set('elastic-api-version', '2023-10-31') .send(previewRequest) diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 58d873369d99d..88752eb1b5f93 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { // See https://github.com/elastic/kibana/pull/125396 for details '--xpack.alerting.rules.minimumScheduleInterval.value=1s', '--xpack.ruleRegistry.unsafe.legacyMultiTenancy.enabled=true', - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts new file mode 100644 index 0000000000000..ce298bafbfea0 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/preview.cy.ts @@ -0,0 +1,85 @@ +/* + * 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 { getEsqlRule, getSimpleCustomQueryRule } from '../../../../objects/rule'; + +import { + PREVIEW_LOGGED_REQUEST_DESCRIPTION, + PREVIEW_LOGGED_REQUEST_CODE_BLOCK, + PREVIEW_LOGGED_REQUESTS_CHECKBOX, + RULES_CREATION_PREVIEW_REFRESH_BUTTON, +} from '../../../../screens/create_new_rule'; + +import { createRule } from '../../../../tasks/api_calls/rules'; + +import { deleteAlertsAndRules } from '../../../../tasks/api_calls/common'; +import { + checkEnableLoggedRequests, + submitRulePreview, + toggleLoggedRequestsAccordion, + toggleLoggedRequestsItemAccordion, +} from '../../../../tasks/create_new_rule'; +import { login } from '../../../../tasks/login'; + +import { visitEditRulePage } from '../../../../tasks/edit_rule'; + +const expectedValidEsqlQuery = 'from auditbeat* METADATA _id'; + +describe( + 'Detection rules, preview', + { + // Currently FF are not supported on MKI environments, so this test should be skipped from MKI environments. + // Once `manualRuleRunEnabled` FF is removed, we can remove `@skipInServerlessMKI` as well + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], + env: { + kbnServerArgs: [ + `--xpack.securitySolution.enableExperimental=${JSON.stringify(['loggingRequestsEnabled'])}`, + ], + }, + }, + () => { + beforeEach(() => { + login(); + deleteAlertsAndRules(); + }); + + describe('supports preview logged requests', () => { + beforeEach(() => { + createRule({ ...getEsqlRule(), query: expectedValidEsqlQuery }).then((createdRule) => { + visitEditRulePage(createdRule.body.id); + }); + }); + + it('shows preview logged requests', () => { + checkEnableLoggedRequests(); + submitRulePreview(); + + toggleLoggedRequestsAccordion(); + toggleLoggedRequestsItemAccordion(); + + cy.get(PREVIEW_LOGGED_REQUEST_DESCRIPTION) + .first() + .contains('ES|QL request to find all matches'); + + cy.get(PREVIEW_LOGGED_REQUEST_CODE_BLOCK).first().contains(expectedValidEsqlQuery); + }); + }); + + describe('does not support preview logged requests', () => { + beforeEach(() => { + createRule(getSimpleCustomQueryRule()).then((createdRule) => { + visitEditRulePage(createdRule.body.id); + }); + }); + + it('does not show preview logged requests checkbox', () => { + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).should('be.visible'); + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).should('not.exist'); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts index 72d1104985d77..a191fc22aa339 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/create_new_rule.ts @@ -296,3 +296,19 @@ export const RULE_INDICES = '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="comboBoxInput"]'; export const ALERTS_INDEX_BUTTON = 'span[title=".alerts-security.alerts-default"] button'; + +export const PREVIEW_SUBMIT_BUTTON = '[data-test-subj="previewSubmitButton"]'; + +export const PREVIEW_LOGGED_REQUESTS_CHECKBOX = '[data-test-subj="show-elasticsearch-requests"]'; + +export const PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON = + '[data-test-subj="preview-logged-requests-accordion"] button'; + +export const PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON = + '[data-test-subj="preview-logged-requests-item-accordion"] button'; + +export const PREVIEW_LOGGED_REQUEST_DESCRIPTION = + '[data-test-subj="preview-logged-request-description"]'; + +export const PREVIEW_LOGGED_REQUEST_CODE_BLOCK = + '[data-test-subj="preview-logged-request-code-block"]'; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts index ecc37de80b456..68dc2cfffd908 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/create_new_rule.ts @@ -130,6 +130,9 @@ import { RELATED_INTEGRATION_COMBO_BOX_INPUT, SAVE_WITH_ERRORS_MODAL, SAVE_WITH_ERRORS_MODAL_CONFIRM_BTN, + PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON, + PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON, + PREVIEW_LOGGED_REQUESTS_CHECKBOX, } from '../screens/create_new_rule'; import { INDEX_SELECTOR, @@ -996,3 +999,20 @@ export const uncheckLoadQueryDynamically = () => { export const openAddFilterPopover = () => { cy.get(QUERY_BAR_ADD_FILTER).click(); }; + +export const checkEnableLoggedRequests = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).click(); + cy.get(PREVIEW_LOGGED_REQUESTS_CHECKBOX).should('be.checked'); +}; + +export const submitRulePreview = () => { + cy.get(RULES_CREATION_PREVIEW_REFRESH_BUTTON).click(); +}; + +export const toggleLoggedRequestsAccordion = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_ACCORDION_BUTTON).first().click(); +}; + +export const toggleLoggedRequestsItemAccordion = () => { + cy.get(PREVIEW_LOGGED_REQUESTS_ITEM_ACCORDION_BUTTON).should('be.visible').first().click(); +}; diff --git a/x-pack/test/security_solution_cypress/serverless_config.ts b/x-pack/test/security_solution_cypress/serverless_config.ts index 226f2db6dbbc0..13877fcbf5af4 100644 --- a/x-pack/test/security_solution_cypress/serverless_config.ts +++ b/x-pack/test/security_solution_cypress/serverless_config.ts @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { { product_line: 'endpoint', product_tier: 'complete' }, { product_line: 'cloud', product_tier: 'complete' }, ])}`, - `--xpack.securitySolution.enableExperimental=${JSON.stringify(['manualRuleRunEnabled'])}`, + `--xpack.securitySolution.enableExperimental=${JSON.stringify([ + 'manualRuleRunEnabled', + 'loggingRequestsEnabled', + ])}`, '--csp.strict=false', '--csp.warnLegacyBrowsers=false', ], From af0582c3467c300f575a33cf9674c46d66b66e72 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 19 Sep 2024 08:48:39 -0500 Subject: [PATCH 10/24] [codeql] Collect statistics weekly (#193308) Moves stats collection from after the analyze scan to a weekly cron. --- .github/CODEOWNERS | 1 + .github/workflows/codeql-stats.yml | 28 ++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 9 --------- 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/codeql-stats.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9284e5ba821f9..7ec76d03da9e0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1361,6 +1361,7 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib # Kibana Platform Security /.github/codeql @elastic/kibana-security /.github/workflows/codeql.yml @elastic/kibana-security +/.github/workflows/codeql-stats.yml @elastic/kibana-security /src/dev/eslint/security_eslint_rule_tests.ts @elastic/kibana-security /src/core/server/integration_tests/config/check_dynamic_config.test.ts @elastic/kibana-security /src/plugins/telemetry/server/config/telemetry_labels.ts @elastic/kibana-security diff --git a/.github/workflows/codeql-stats.yml b/.github/workflows/codeql-stats.yml new file mode 100644 index 0000000000000..fc01cdd969295 --- /dev/null +++ b/.github/workflows/codeql-stats.yml @@ -0,0 +1,28 @@ +name: CodeQL statistics + +on: + schedule: + - cron: '27 0 * * 1' # At 00:27 every Monday + +jobs: + stats: + name: CodeQL statistics + runs-on: ubuntu-latest + if: github.repository == 'elastic/kibana' # Hack: Do not run on forks + steps: + - name: Checkout kibana-operations + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + repository: 'elastic/kibana-operations' + ref: main + path: ./kibana-operations + token: ${{secrets.KIBANAMACHINE_TOKEN}} + + - name: CodeQL alert statistics + working-directory: ./kibana-operations/triage + env: + GITHUB_TOKEN: ${{secrets.KIBANAMACHINE_TOKEN}} + SLACK_TOKEN: ${{secrets.CODE_SCANNING_SLACK_TOKEN}} + run: | + npm ci --omit=dev + node codeql-alert-stats diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 87a022376ac93..82080dd9235f9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -76,12 +76,3 @@ jobs: run: | npm ci --omit=dev node codeql-alert - - - name: CodeQL alert statistics - working-directory: ./kibana-operations/triage - env: - GITHUB_TOKEN: ${{secrets.KIBANAMACHINE_TOKEN}} - SLACK_TOKEN: ${{secrets.CODE_SCANNING_SLACK_TOKEN}} - run: | - npm ci --omit=dev - node codeql-alert-stats From db5557429979b9a0f3420a50a06c7fd69cbdf5b2 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 19 Sep 2024 08:02:49 -0600 Subject: [PATCH 11/24] [Embeddable Rebuild] [Controls] Clean up services + TODOs (#193180) Part of https://github.com/elastic/kibana/issues/192005 Closes https://github.com/elastic/kibana/issues/167438 ## Summary ## Summary This PR represents the second major cleanup task for the control group embeddable refactor. The tasks included in this PR can be loosely summarized as follows: 1. It removes the old, cumbersome services implementation and replaces it with a much simpler system (which is the same one used in the `links` + `presentation_panel` plugins). - This it the main reason for the decrease in lines - the old system required a **huge** amount of boilerplate code, which is no longer necessary with the new method for storing services. 2. It addresses and/or removes any remaining TODO comments in the `controls` plugin - This includes renaming `ControlStyle` and `DEFAULT_CONTROL_STYLE` to `ControlLabelPosition` and `DEFAULT_CONTROL_LABEL_POSITION` respectively, which represents a bulk of the changes. 3. It moves all compatibility checks for all control actions to be async imported. 4. It removes the ability to register controls from the `controls` plugin setup contract. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/controls/common/constants.ts | 4 +- .../controls/common/control_group/types.ts | 6 +- src/plugins/controls/common/index.ts | 4 +- .../controls/common/options_list/types.ts | 2 - src/plugins/controls/common/types.ts | 2 +- src/plugins/controls/jest_setup.ts | 7 +- src/plugins/controls/kibana.jsonc | 4 +- .../public/actions/clear_control_action.tsx | 49 ++------- ...lear_control_action_compatibility_check.ts | 42 ++++++++ .../actions/delete_control_action.test.tsx | 19 +--- .../public/actions/delete_control_action.tsx | 100 ++++++------------ ...lete_control_action_compatibility_check.ts | 42 ++++++++ .../actions/edit_control_action.test.tsx | 20 +--- src/plugins/controls/public/index.ts | 1 - src/plugins/controls/public/plugin.ts | 65 +++--------- .../components/control_error.tsx | 2 - .../components/control_group.tsx | 4 +- .../components/control_group_editor.test.tsx | 6 +- .../components/control_group_editor.tsx | 4 +- .../components/control_panel.test.tsx | 6 +- .../get_control_group_factory.tsx | 51 ++++----- .../open_edit_control_group_flyout.tsx | 15 ++- .../register_control_group_embeddable.ts | 17 +-- .../react_controls/control_group/types.ts | 6 +- .../utils/control_group_state_builder.ts | 7 +- .../utils/initialization_utils.ts | 4 +- .../data_control_editor.test.tsx | 9 +- .../data_controls/data_control_editor.tsx | 13 +-- .../data_control_editor_utils.ts | 2 - .../initialize_data_control.test.tsx | 21 ++-- .../data_controls/initialize_data_control.ts | 12 +-- .../open_data_control_editor.tsx | 18 ++-- .../fetch_and_validate.tsx | 5 +- .../get_options_list_control_factory.test.tsx | 16 +-- .../get_options_list_control_factory.tsx | 27 +++-- .../options_list_fetch_cache.ts | 11 +- .../register_options_list_control.ts | 17 ++- .../get_range_slider_control_factory.test.tsx | 16 +-- .../get_range_slider_control_factory.tsx | 27 +++-- .../range_slider/has_no_results.ts | 13 +-- .../data_controls/range_slider/min_max.ts | 13 +-- .../register_range_slider_control.ts | 17 ++- .../controls/data_controls/types.ts | 9 -- .../timeslider_control/get_time_range_meta.ts | 23 ++-- .../get_timeslider_control_factory.test.tsx | 17 ++- .../get_timeslider_control_factory.tsx | 11 +- .../init_time_range_subscription.ts | 11 +- .../register_timeslider_control.ts | 16 ++- .../controls/timeslider_control/types.ts | 7 -- .../public/react_controls/controls/types.ts | 11 +- .../control_group_renderer.test.tsx | 8 +- .../public/services/controls/controls.stub.ts | 46 -------- .../services/controls/controls_service.ts | 24 ----- .../public/services/controls/types.ts | 23 ---- .../public/services/core/core.stub.ts | 24 ----- .../public/services/core/core_service.ts | 28 ----- .../controls/public/services/core/types.ts | 17 --- .../public/services/data/data.stub.ts | 31 ------ .../public/services/data/data_service.ts | 29 ----- .../controls/public/services/data/types.ts | 15 --- .../services/data_views/data_views.stub.ts | 52 --------- .../services/data_views/data_views_service.ts | 29 ----- .../public/services/data_views/types.ts | 16 --- .../services/embeddable/embeddable.stub.ts | 19 ---- .../services/embeddable/embeddable_service.ts | 23 ---- .../public/services/embeddable/types.ts | 14 --- .../public/services/http/http.stub.ts | 19 ---- .../public/services/http/http_service.ts | 27 ----- .../controls/public/services/http/types.ts | 15 --- src/plugins/controls/public/services/index.ts | 10 -- .../public/services/kibana_services.ts | 42 ++++++++ src/plugins/controls/public/services/mocks.ts | 23 ++++ .../public/services/overlays/overlays.stub.ts | 33 ------ .../services/overlays/overlays_service.ts | 27 ----- .../public/services/overlays/types.ts | 20 ---- .../public/services/plugin_services.stub.ts | 54 ---------- .../public/services/plugin_services.ts | 52 --------- .../public/services/settings/settings.stub.ts | 18 ---- .../services/settings/settings_service.ts | 32 ------ .../public/services/settings/types.ts | 16 --- .../services/storage/storage_service.stub.ts | 20 ---- .../services/storage/storage_service.ts | 31 ------ .../controls/public/services/storage/types.ts | 13 --- src/plugins/controls/public/services/types.ts | 35 ------ .../public/services/unified_search/types.ts | 14 --- .../unified_search/unified_search.stub.ts | 27 ----- .../unified_search/unified_search_service.ts | 27 ----- src/plugins/controls/public/types.ts | 16 +-- .../control_group_persistence.ts | 4 +- .../options_list_cluster_settings_route.ts | 2 +- src/plugins/controls/tsconfig.json | 1 - 91 files changed, 399 insertions(+), 1408 deletions(-) create mode 100644 src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts create mode 100644 src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts delete mode 100644 src/plugins/controls/public/services/controls/controls.stub.ts delete mode 100644 src/plugins/controls/public/services/controls/controls_service.ts delete mode 100644 src/plugins/controls/public/services/controls/types.ts delete mode 100644 src/plugins/controls/public/services/core/core.stub.ts delete mode 100644 src/plugins/controls/public/services/core/core_service.ts delete mode 100644 src/plugins/controls/public/services/core/types.ts delete mode 100644 src/plugins/controls/public/services/data/data.stub.ts delete mode 100644 src/plugins/controls/public/services/data/data_service.ts delete mode 100644 src/plugins/controls/public/services/data/types.ts delete mode 100644 src/plugins/controls/public/services/data_views/data_views.stub.ts delete mode 100644 src/plugins/controls/public/services/data_views/data_views_service.ts delete mode 100644 src/plugins/controls/public/services/data_views/types.ts delete mode 100644 src/plugins/controls/public/services/embeddable/embeddable.stub.ts delete mode 100644 src/plugins/controls/public/services/embeddable/embeddable_service.ts delete mode 100644 src/plugins/controls/public/services/embeddable/types.ts delete mode 100644 src/plugins/controls/public/services/http/http.stub.ts delete mode 100644 src/plugins/controls/public/services/http/http_service.ts delete mode 100644 src/plugins/controls/public/services/http/types.ts delete mode 100644 src/plugins/controls/public/services/index.ts create mode 100644 src/plugins/controls/public/services/kibana_services.ts create mode 100644 src/plugins/controls/public/services/mocks.ts delete mode 100644 src/plugins/controls/public/services/overlays/overlays.stub.ts delete mode 100644 src/plugins/controls/public/services/overlays/overlays_service.ts delete mode 100644 src/plugins/controls/public/services/overlays/types.ts delete mode 100644 src/plugins/controls/public/services/plugin_services.stub.ts delete mode 100644 src/plugins/controls/public/services/plugin_services.ts delete mode 100644 src/plugins/controls/public/services/settings/settings.stub.ts delete mode 100644 src/plugins/controls/public/services/settings/settings_service.ts delete mode 100644 src/plugins/controls/public/services/settings/types.ts delete mode 100644 src/plugins/controls/public/services/storage/storage_service.stub.ts delete mode 100644 src/plugins/controls/public/services/storage/storage_service.ts delete mode 100644 src/plugins/controls/public/services/storage/types.ts delete mode 100644 src/plugins/controls/public/services/types.ts delete mode 100644 src/plugins/controls/public/services/unified_search/types.ts delete mode 100644 src/plugins/controls/public/services/unified_search/unified_search.stub.ts delete mode 100644 src/plugins/controls/public/services/unified_search/unified_search_service.ts diff --git a/src/plugins/controls/common/constants.ts b/src/plugins/controls/common/constants.ts index e100474177c71..e375a7b2315bc 100644 --- a/src/plugins/controls/common/constants.ts +++ b/src/plugins/controls/common/constants.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ControlStyle, ControlWidth } from './types'; +import { ControlLabelPosition, ControlWidth } from './types'; export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'medium'; export const DEFAULT_CONTROL_GROW: boolean = true; -export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine'; +export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition = 'oneLine'; export const TIME_SLIDER_CONTROL = 'timeSlider'; export const RANGE_SLIDER_CONTROL = 'rangeSliderControl'; diff --git a/src/plugins/controls/common/control_group/types.ts b/src/plugins/controls/common/control_group/types.ts index cb51cf79e5400..eb47d8b13eb79 100644 --- a/src/plugins/controls/common/control_group/types.ts +++ b/src/plugins/controls/common/control_group/types.ts @@ -8,7 +8,7 @@ */ import { DataViewField } from '@kbn/data-views-plugin/common'; -import { ControlStyle, DefaultControlState, ParentIgnoreSettings } from '../types'; +import { ControlLabelPosition, DefaultControlState, ParentIgnoreSettings } from '../types'; export const CONTROL_GROUP_TYPE = 'control_group'; @@ -31,7 +31,7 @@ export interface ControlGroupEditorConfig { export interface ControlGroupRuntimeState { chainingSystem: ControlGroupChainingSystem; - labelPosition: ControlStyle; // TODO: Rename this type to ControlLabelPosition + labelPosition: ControlLabelPosition; autoApplySelections: boolean; ignoreParentSettings?: ParentIgnoreSettings; @@ -50,7 +50,7 @@ export interface ControlGroupSerializedState ignoreParentSettingsJSON: string; // In runtime state, we refer to this property as `labelPosition`; // to avoid migrations, we will continue to refer to this property as `controlStyle` in the serialized state - controlStyle: ControlStyle; + controlStyle: ControlLabelPosition; // In runtime state, we refer to the inverse of this property as `autoApplySelections` // to avoid migrations, we will continue to refer to this property as `showApplySelections` in the serialized state showApplySelections?: boolean; diff --git a/src/plugins/controls/common/index.ts b/src/plugins/controls/common/index.ts index c59e4c04ac1b0..dd9c56778bb68 100644 --- a/src/plugins/controls/common/index.ts +++ b/src/plugins/controls/common/index.ts @@ -8,7 +8,7 @@ */ export type { - ControlStyle, + ControlLabelPosition, ControlWidth, DefaultControlState, DefaultDataControlState, @@ -18,7 +18,7 @@ export type { export { DEFAULT_CONTROL_GROW, - DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_LABEL_POSITION, DEFAULT_CONTROL_WIDTH, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index e5eccbcab5cf5..10d4a88553586 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -15,8 +15,6 @@ import { OptionsListSortingType } from './suggestions_sorting'; import { DefaultDataControlState } from '../types'; import { OptionsListSearchTechnique } from './suggestions_searching'; -export const OPTIONS_LIST_CONTROL = 'optionsListControl'; // TODO: Replace with OPTIONS_LIST_CONTROL_TYPE - /** * ---------------------------------------------------------------- * Options list state types diff --git a/src/plugins/controls/common/types.ts b/src/plugins/controls/common/types.ts index 34d4708b3e991..d3a6261aeb9da 100644 --- a/src/plugins/controls/common/types.ts +++ b/src/plugins/controls/common/types.ts @@ -8,7 +8,7 @@ */ export type ControlWidth = 'small' | 'medium' | 'large'; -export type ControlStyle = 'twoLine' | 'oneLine'; +export type ControlLabelPosition = 'twoLine' | 'oneLine'; export type TimeSlice = [number, number]; diff --git a/src/plugins/controls/jest_setup.ts b/src/plugins/controls/jest_setup.ts index 722f87562328e..04a52e3b6653f 100644 --- a/src/plugins/controls/jest_setup.ts +++ b/src/plugins/controls/jest_setup.ts @@ -8,8 +8,5 @@ */ // Start the services with stubs -import { pluginServices } from './public/services'; -import { registry } from './public/services/plugin_services.stub'; - -registry.start({}); -pluginServices.setRegistry(registry); +import { setStubKibanaServices } from './public/services/mocks'; +setStubKibanaServices(); diff --git a/src/plugins/controls/kibana.jsonc b/src/plugins/controls/kibana.jsonc index bd65ecc2d0b6f..add8c14ee3391 100644 --- a/src/plugins/controls/kibana.jsonc +++ b/src/plugins/controls/kibana.jsonc @@ -9,8 +9,6 @@ "browser": true, "requiredPlugins": [ "presentationUtil", - "kibanaReact", - "expressions", "embeddable", "dataViews", "data", @@ -18,6 +16,6 @@ "uiActions" ], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaUtils"] + "requiredBundles": [] } } diff --git a/src/plugins/controls/public/actions/clear_control_action.tsx b/src/plugins/controls/public/actions/clear_control_action.tsx index b7c2777473fd5..02347ace2fd8d 100644 --- a/src/plugins/controls/public/actions/clear_control_action.tsx +++ b/src/plugins/controls/public/actions/clear_control_action.tsx @@ -11,42 +11,10 @@ import React, { SyntheticEvent } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - apiIsPresentationContainer, - type PresentationContainer, -} from '@kbn/presentation-containers'; -import { - apiCanAccessViewMode, - apiHasParentApi, - apiHasType, - apiHasUniqueId, - apiIsOfType, - type EmbeddableApiContext, - type HasParentApi, - type HasType, - type HasUniqueId, -} from '@kbn/presentation-publishing'; -import { type Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { EmbeddableApiContext, HasUniqueId } from '@kbn/presentation-publishing'; +import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public'; import { ACTION_CLEAR_CONTROL } from '.'; -import { CONTROL_GROUP_TYPE } from '..'; -import { isClearableControl, type CanClearSelections } from '../types'; - -export type ClearControlActionApi = HasType & - HasUniqueId & - CanClearSelections & - HasParentApi; - -const isApiCompatible = (api: unknown | null): api is ClearControlActionApi => - Boolean( - apiHasType(api) && - apiHasUniqueId(api) && - isClearableControl(api) && - apiHasParentApi(api) && - apiCanAccessViewMode(api.parentApi) && - apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && - apiIsPresentationContainer(api.parentApi) - ); export class ClearControlAction implements Action { public readonly type = ACTION_CLEAR_CONTROL; @@ -56,12 +24,10 @@ export class ClearControlAction implements Action { constructor() {} public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { - if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); - return ( ) => { @@ -75,23 +41,24 @@ export class ClearControlAction implements Action { }; public getDisplayName({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return i18n.translate('controls.controlGroup.floatingActions.clearTitle', { defaultMessage: 'Clear', }); } public getIconType({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return 'eraser'; } public async isCompatible({ embeddable }: EmbeddableApiContext) { - return isApiCompatible(embeddable); + const { isCompatible } = await import('./clear_control_action_compatibility_check'); + return isCompatible(embeddable); } public async execute({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./clear_control_action_compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); + embeddable.clearSelections(); } } diff --git a/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts b/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts new file mode 100644 index 0000000000000..f04cb91bc9a3a --- /dev/null +++ b/src/plugins/controls/public/actions/clear_control_action_compatibility_check.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + HasParentApi, + HasType, + HasUniqueId, + apiCanAccessViewMode, + apiHasParentApi, + apiHasType, + apiHasUniqueId, + apiIsOfType, +} from '@kbn/presentation-publishing'; +import { CONTROL_GROUP_TYPE } from '../../common'; +import { isClearableControl, type CanClearSelections } from '../types'; + +type ClearControlActionApi = HasType & + HasUniqueId & + CanClearSelections & + HasParentApi; + +export const compatibilityCheck = (api: unknown | null): api is ClearControlActionApi => + Boolean( + apiHasType(api) && + apiHasUniqueId(api) && + isClearableControl(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api.parentApi) && + apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && + apiIsPresentationContainer(api.parentApi) + ); + +export function isCompatible(api: unknown) { + return compatibilityCheck(api); +} diff --git a/src/plugins/controls/public/actions/delete_control_action.test.tsx b/src/plugins/controls/public/actions/delete_control_action.test.tsx index 65be8a65ecd6f..c158d743f69ae 100644 --- a/src/plugins/controls/public/actions/delete_control_action.test.tsx +++ b/src/plugins/controls/public/actions/delete_control_action.test.tsx @@ -9,22 +9,15 @@ import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { ViewMode } from '@kbn/presentation-publishing'; - import { getOptionsListControlFactory } from '../react_controls/controls/data_controls/options_list_control/get_options_list_control_factory'; import { OptionsListControlApi } from '../react_controls/controls/data_controls/options_list_control/types'; import { getMockedBuildApi, getMockedControlGroupApi, } from '../react_controls/controls/mocks/control_mocks'; -import { pluginServices } from '../services'; import { DeleteControlAction } from './delete_control_action'; - -const mockDataViews = dataViewPluginMocks.createStartContract(); -const mockCore = coreMock.createStart(); +import { coreServices } from '../services/kibana_services'; const dashboardApi = { viewMode: new BehaviorSubject('view'), @@ -38,11 +31,7 @@ const controlGroupApi = getMockedControlGroupApi(dashboardApi, { let controlApi: OptionsListControlApi; beforeAll(async () => { - const controlFactory = getOptionsListControlFactory({ - core: mockCore, - data: dataPluginMock.createStartContract(), - dataViews: mockDataViews, - }); + const controlFactory = getOptionsListControlFactory(); const uuid = 'testControl'; const control = await controlFactory.buildControl( @@ -72,7 +61,7 @@ test('Execute throws an error when called with an embeddable not in a parent', a describe('Execute should open a confirm modal', () => { test('Canceling modal will keep control', async () => { const spyOn = jest.fn().mockResolvedValue(false); - pluginServices.getServices().overlays.openConfirm = spyOn; + coreServices.overlays.openConfirm = spyOn; const deleteControlAction = new DeleteControlAction(); await deleteControlAction.execute({ embeddable: controlApi }); @@ -83,7 +72,7 @@ describe('Execute should open a confirm modal', () => { test('Confirming modal will delete control', async () => { const spyOn = jest.fn().mockResolvedValue(true); - pluginServices.getServices().overlays.openConfirm = spyOn; + coreServices.overlays.openConfirm = spyOn; const deleteControlAction = new DeleteControlAction(); await deleteControlAction.execute({ embeddable: controlApi }); diff --git a/src/plugins/controls/public/actions/delete_control_action.tsx b/src/plugins/controls/public/actions/delete_control_action.tsx index 45a7a20385627..7ee55ddd3da69 100644 --- a/src/plugins/controls/public/actions/delete_control_action.tsx +++ b/src/plugins/controls/public/actions/delete_control_action.tsx @@ -10,65 +10,25 @@ import React from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { - apiIsPresentationContainer, - type PresentationContainer, -} from '@kbn/presentation-containers'; -import { - apiCanAccessViewMode, - apiHasParentApi, - apiHasType, - apiHasUniqueId, - apiIsOfType, - getInheritedViewMode, - type EmbeddableApiContext, - type HasParentApi, - type HasType, - type HasUniqueId, - type PublishesViewMode, -} from '@kbn/presentation-publishing'; +import type { HasUniqueId, EmbeddableApiContext } from '@kbn/presentation-publishing'; import { IncompatibleActionError, type Action } from '@kbn/ui-actions-plugin/public'; import { ACTION_DELETE_CONTROL } from '.'; -import { CONTROL_GROUP_TYPE } from '..'; -import { pluginServices } from '../services'; - -export type DeleteControlActionApi = HasType & - HasUniqueId & - HasParentApi; - -const isApiCompatible = (api: unknown | null): api is DeleteControlActionApi => - Boolean( - apiHasType(api) && - apiHasUniqueId(api) && - apiHasParentApi(api) && - apiCanAccessViewMode(api.parentApi) && - apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && - apiIsPresentationContainer(api.parentApi) - ); +import { coreServices } from '../services/kibana_services'; export class DeleteControlAction implements Action { public readonly type = ACTION_DELETE_CONTROL; public readonly id = ACTION_DELETE_CONTROL; public order = 100; // should always be last - private openConfirm; - - constructor() { - ({ - overlays: { openConfirm: this.openConfirm }, - } = pluginServices.getServices()); - } + constructor() {} public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => { - if (!isApiCompatible(context.embeddable)) throw new IncompatibleActionError(); - return ( this.execute(context)} @@ -79,46 +39,46 @@ export class DeleteControlAction implements Action { }; public getDisplayName({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return i18n.translate('controls.controlGroup.floatingActions.removeTitle', { defaultMessage: 'Delete', }); } public getIconType({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); return 'trash'; } public async isCompatible({ embeddable }: EmbeddableApiContext) { - return ( - isApiCompatible(embeddable) && getInheritedViewMode(embeddable.parentApi) === ViewMode.EDIT - ); + const { isCompatible } = await import('./delete_control_action_compatibility_check'); + return isCompatible(embeddable); } public async execute({ embeddable }: EmbeddableApiContext) { - if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); + const { compatibilityCheck } = await import('./delete_control_action_compatibility_check'); + if (!compatibilityCheck(embeddable)) throw new IncompatibleActionError(); - this.openConfirm( - i18n.translate('controls.controlGroup.management.delete.sub', { - defaultMessage: 'Controls are not recoverable once removed.', - }), - { - confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { - defaultMessage: 'Delete', - }), - cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { - defaultMessage: 'Cancel', + coreServices.overlays + .openConfirm( + i18n.translate('controls.controlGroup.management.delete.sub', { + defaultMessage: 'Controls are not recoverable once removed.', }), - title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { - defaultMessage: 'Delete control?', - }), - buttonColor: 'danger', - } - ).then((confirmed) => { - if (confirmed) { - embeddable.parentApi.removePanel(embeddable.uuid); - } - }); + { + confirmButtonText: i18n.translate('controls.controlGroup.management.delete.confirm', { + defaultMessage: 'Delete', + }), + cancelButtonText: i18n.translate('controls.controlGroup.management.delete.cancel', { + defaultMessage: 'Cancel', + }), + title: i18n.translate('controls.controlGroup.management.delete.deleteTitle', { + defaultMessage: 'Delete control?', + }), + buttonColor: 'danger', + } + ) + .then((confirmed) => { + if (confirmed) { + embeddable.parentApi.removePanel(embeddable.uuid); + } + }); } } diff --git a/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts b/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts new file mode 100644 index 0000000000000..a09b3448b2fc1 --- /dev/null +++ b/src/plugins/controls/public/actions/delete_control_action_compatibility_check.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { PresentationContainer, apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + HasParentApi, + HasType, + HasUniqueId, + PublishesViewMode, + apiCanAccessViewMode, + apiHasParentApi, + apiHasType, + apiHasUniqueId, + apiIsOfType, + getInheritedViewMode, +} from '@kbn/presentation-publishing'; +import { CONTROL_GROUP_TYPE } from '../../common'; + +type DeleteControlActionApi = HasType & + HasUniqueId & + HasParentApi; + +export const compatibilityCheck = (api: unknown | null): api is DeleteControlActionApi => + Boolean( + apiHasType(api) && + apiHasUniqueId(api) && + apiHasParentApi(api) && + apiCanAccessViewMode(api.parentApi) && + apiIsOfType(api.parentApi, CONTROL_GROUP_TYPE) && + apiIsPresentationContainer(api.parentApi) + ); + +export function isCompatible(api: unknown) { + return compatibilityCheck(api) && getInheritedViewMode(api.parentApi) === ViewMode.EDIT; +} diff --git a/src/plugins/controls/public/actions/edit_control_action.test.tsx b/src/plugins/controls/public/actions/edit_control_action.test.tsx index 3c28feb907421..b1c24d779aaf6 100644 --- a/src/plugins/controls/public/actions/edit_control_action.test.tsx +++ b/src/plugins/controls/public/actions/edit_control_action.test.tsx @@ -9,9 +9,6 @@ import { BehaviorSubject } from 'rxjs'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import dateMath from '@kbn/datemath'; import type { TimeRange } from '@kbn/es-query'; import type { ViewMode } from '@kbn/presentation-publishing'; @@ -23,12 +20,10 @@ import { getMockedControlGroupApi, } from '../react_controls/controls/mocks/control_mocks'; import { getTimesliderControlFactory } from '../react_controls/controls/timeslider_control/get_timeslider_control_factory'; +import { dataService } from '../services/kibana_services'; import { EditControlAction } from './edit_control_action'; -const mockDataViews = dataViewPluginMocks.createStartContract(); -const mockCore = coreMock.createStart(); -const dataStartServiceMock = dataPluginMock.createStartContract(); -dataStartServiceMock.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { +dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { min: dateMath.parse(timeRange.from, { forceNow: now }), @@ -48,11 +43,7 @@ const controlGroupApi = getMockedControlGroupApi(dashboardApi, { let optionsListApi: OptionsListControlApi; beforeAll(async () => { - const controlFactory = getOptionsListControlFactory({ - core: mockCore, - data: dataStartServiceMock, - dataViews: mockDataViews, - }); + const controlFactory = getOptionsListControlFactory(); const optionsListUuid = 'optionsListControl'; const optionsListControl = await controlFactory.buildControl( @@ -73,10 +64,7 @@ beforeAll(async () => { describe('Incompatible embeddables', () => { test('Action is incompatible with embeddables that are not editable', async () => { - const timeSliderFactory = getTimesliderControlFactory({ - core: mockCore, - data: dataStartServiceMock, - }); + const timeSliderFactory = getTimesliderControlFactory(); const timeSliderUuid = 'timeSliderControl'; const timeSliderControl = await timeSliderFactory.buildControl( {}, diff --git a/src/plugins/controls/public/index.ts b/src/plugins/controls/public/index.ts index 6a490248b8929..6c7a548cb091d 100644 --- a/src/plugins/controls/public/index.ts +++ b/src/plugins/controls/public/index.ts @@ -21,7 +21,6 @@ export { ACTION_CLEAR_CONTROL, ACTION_DELETE_CONTROL, ACTION_EDIT_CONTROL } from export type { DataControlApi, DataControlFactory, - DataControlServices, } from './react_controls/controls/data_controls/types'; export { diff --git a/src/plugins/controls/public/plugin.ts b/src/plugins/controls/public/plugin.ts index 039b960bfc3c5..c6e1a2873b169 100644 --- a/src/plugins/controls/public/plugin.ts +++ b/src/plugins/controls/public/plugin.ts @@ -10,63 +10,36 @@ import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; import { PANEL_HOVER_TRIGGER } from '@kbn/embeddable-plugin/public'; +import { ClearControlAction } from './actions/clear_control_action'; +import { DeleteControlAction } from './actions/delete_control_action'; +import { EditControlAction } from './actions/edit_control_action'; import { registerControlGroupEmbeddable } from './react_controls/control_group/register_control_group_embeddable'; import { registerOptionsListControl } from './react_controls/controls/data_controls/options_list_control/register_options_list_control'; import { registerRangeSliderControl } from './react_controls/controls/data_controls/range_slider/register_range_slider_control'; import { registerTimeSliderControl } from './react_controls/controls/timeslider_control/register_timeslider_control'; -import { controlsService } from './services/controls/controls_service'; -import type { - ControlsPluginSetup, - ControlsPluginSetupDeps, - ControlsPluginStart, - ControlsPluginStartDeps, -} from './types'; +import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; +import type { ControlsPluginSetupDeps, ControlsPluginStartDeps } from './types'; export class ControlsPlugin - implements - Plugin< - ControlsPluginSetup, - ControlsPluginStart, - ControlsPluginSetupDeps, - ControlsPluginStartDeps - > + implements Plugin { - private async startControlsKibanaServices( - coreStart: CoreStart, - startPlugins: ControlsPluginStartDeps - ) { - const { registry, pluginServices } = await import('./services/plugin_services'); - pluginServices.setRegistry(registry.start({ coreStart, startPlugins })); - } - public setup( - _coreSetup: CoreSetup, + _coreSetup: CoreSetup, _setupPlugins: ControlsPluginSetupDeps - ): ControlsPluginSetup { - const { registerControlFactory } = controlsService; + ) { const { embeddable } = _setupPlugins; - registerControlGroupEmbeddable(_coreSetup, embeddable); - registerOptionsListControl(_coreSetup); - registerRangeSliderControl(_coreSetup); - registerTimeSliderControl(_coreSetup); - - return { - registerControlFactory, - }; + registerControlGroupEmbeddable(embeddable); + registerOptionsListControl(); + registerRangeSliderControl(); + registerTimeSliderControl(); } - public start(coreStart: CoreStart, startPlugins: ControlsPluginStartDeps): ControlsPluginStart { - this.startControlsKibanaServices(coreStart, startPlugins).then(async () => { - const { uiActions } = startPlugins; - - const [{ DeleteControlAction }, { EditControlAction }, { ClearControlAction }] = - await Promise.all([ - import('./actions/delete_control_action'), - import('./actions/edit_control_action'), - import('./actions/clear_control_action'), - ]); + public start(coreStart: CoreStart, startPlugins: ControlsPluginStartDeps) { + const { uiActions } = startPlugins; + setKibanaServices(coreStart, startPlugins); + untilPluginStartServicesReady().then(() => { const deleteControlAction = new DeleteControlAction(); uiActions.registerAction(deleteControlAction); uiActions.attachAction(PANEL_HOVER_TRIGGER, deleteControlAction.id); @@ -79,12 +52,6 @@ export class ControlsPlugin uiActions.registerAction(clearControlAction); uiActions.attachAction(PANEL_HOVER_TRIGGER, clearControlAction.id); }); - - const { getControlFactory, getAllControlTypes } = controlsService; - return { - getControlFactory, - getAllControlTypes, - }; } public stop() {} diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx index 52b9e7d9b806a..2ef6b06faeedd 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_error.tsx @@ -13,8 +13,6 @@ import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { Markdown } from '@kbn/shared-ux-markdown'; -/** TODO: This file is duplicated from the controls plugin to avoid exporting it */ - interface ControlErrorProps { error: Error | string; } diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx index 9c962113d1a7f..54e778684806a 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group.tsx @@ -30,7 +30,7 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from ' import { css } from '@emotion/react'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { ControlStyle } from '../../../../common'; +import type { ControlLabelPosition } from '../../../../common'; import type { DefaultControlApi } from '../../controls/types'; import { ControlGroupStrings } from '../control_group_strings'; import { ControlsInOrder } from '../init_controls_manager'; @@ -49,7 +49,7 @@ interface Props { setControlApi: (uuid: string, controlApi: DefaultControlApi) => void; }; hasUnappliedSelections: boolean; - labelPosition: ControlStyle; + labelPosition: ControlLabelPosition; } export function ControlGroup({ diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx index 79d0312b29537..b3705106afe2c 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.test.tsx @@ -15,8 +15,8 @@ import { render } from '@testing-library/react'; import { ControlGroupApi } from '../../..'; import { ControlGroupChainingSystem, - ControlStyle, - DEFAULT_CONTROL_STYLE, + ControlLabelPosition, + DEFAULT_CONTROL_LABEL_POSITION, ParentIgnoreSettings, } from '../../../../common'; import { DefaultControlApi } from '../../controls/types'; @@ -33,7 +33,7 @@ describe('render', () => { onDeleteAll: () => {}, stateManager: { chainingSystem: new BehaviorSubject('HIERARCHICAL'), - labelPosition: new BehaviorSubject(DEFAULT_CONTROL_STYLE), + labelPosition: new BehaviorSubject(DEFAULT_CONTROL_LABEL_POSITION), autoApplySelections: new BehaviorSubject(true), ignoreParentSettings: new BehaviorSubject(undefined), }, diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx index f908a557366fa..c4e7dc61476ba 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_group_editor.tsx @@ -27,7 +27,7 @@ import { } from '@elastic/eui'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import type { ControlStyle, ParentIgnoreSettings } from '../../../../common'; +import type { ControlLabelPosition, ParentIgnoreSettings } from '../../../../common'; import { CONTROL_LAYOUT_OPTIONS } from '../../controls/data_controls/editor_constants'; import type { ControlStateManager } from '../../controls/types'; import { ControlGroupStrings } from '../control_group_strings'; @@ -86,7 +86,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager idSelected={selectedLabelPosition} legend={ControlGroupStrings.management.labelPosition.getLabelPositionLegend()} onChange={(newPosition: string) => { - stateManager.labelPosition.next(newPosition as ControlStyle); + stateManager.labelPosition.next(newPosition as ControlLabelPosition); }} /> diff --git a/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx b/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx index bbf8e8127813e..365c896bb908e 100644 --- a/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx +++ b/src/plugins/controls/public/react_controls/control_group/components/control_panel.test.tsx @@ -14,7 +14,7 @@ import { pluginServices as presentationUtilPluginServices } from '@kbn/presentat import { registry as presentationUtilServicesRegistry } from '@kbn/presentation-util-plugin/public/services/plugin_services.story'; import { render, waitFor } from '@testing-library/react'; -import type { ControlStyle, ControlWidth } from '../../../../common'; +import type { ControlLabelPosition, ControlWidth } from '../../../../common'; import { ControlPanel } from './control_panel'; describe('render', () => { @@ -74,7 +74,7 @@ describe('render', () => { mockApi = { uuid: 'control1', parentApi: { - labelPosition: new BehaviorSubject('oneLine'), + labelPosition: new BehaviorSubject('oneLine'), }, }; const controlPanel = render(); @@ -92,7 +92,7 @@ describe('render', () => { mockApi = { uuid: 'control1', parentApi: { - labelPosition: new BehaviorSubject('twoLine'), + labelPosition: new BehaviorSubject('twoLine'), }, }; const controlPanel = render(); diff --git a/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx b/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx index 6c3e8d10c3c66..77da1480eb494 100644 --- a/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx +++ b/src/plugins/controls/public/react_controls/control_group/get_control_group_factory.tsx @@ -11,9 +11,7 @@ import fastIsEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; -import { CoreStart } from '@kbn/core/public'; import { DataView } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; import { @@ -31,11 +29,11 @@ import type { ControlGroupChainingSystem, ControlGroupRuntimeState, ControlGroupSerializedState, + ControlLabelPosition, ControlPanelsState, - ControlStyle, ParentIgnoreSettings, } from '../../../common'; -import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_STYLE } from '../../../common'; +import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_LABEL_POSITION } from '../../../common'; import { openDataControlEditor } from '../controls/data_controls/open_data_control_editor'; import { ControlGroup } from './components/control_group'; import { chaining$, controlFetch$, controlGroupFetch$ } from './control_fetch'; @@ -45,13 +43,11 @@ import { openEditControlGroupFlyout } from './open_edit_control_group_flyout'; import { initSelectionsManager } from './selections_manager'; import type { ControlGroupApi } from './types'; import { deserializeControlGroup } from './utils/serialization_utils'; +import { coreServices, dataViewsService } from '../../services/kibana_services'; const DEFAULT_CHAINING_SYSTEM = 'HIERARCHICAL'; -export const getControlGroupEmbeddableFactory = (services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; -}) => { +export const getControlGroupEmbeddableFactory = () => { const controlGroupEmbeddableFactory: ReactEmbeddableFactory< ControlGroupSerializedState, ControlGroupRuntimeState, @@ -75,7 +71,7 @@ export const getControlGroupEmbeddableFactory = (services: { } = initialRuntimeState; const autoApplySelections$ = new BehaviorSubject(autoApplySelections); - const defaultDataViewId = await services.dataViews.getDefaultId(); + const defaultDataViewId = await dataViewsService.getDefaultId(); const lastSavedControlsState$ = new BehaviorSubject( lastSavedRuntimeState.initialChildControlState ); @@ -94,15 +90,12 @@ export const getControlGroupEmbeddableFactory = (services: { const ignoreParentSettings$ = new BehaviorSubject( ignoreParentSettings ); - const labelPosition$ = new BehaviorSubject( // TODO: Rename `ControlStyle` - initialLabelPosition ?? DEFAULT_CONTROL_STYLE // TODO: Rename `DEFAULT_CONTROL_STYLE` + const labelPosition$ = new BehaviorSubject( + initialLabelPosition ?? DEFAULT_CONTROL_LABEL_POSITION ); const allowExpensiveQueries$ = new BehaviorSubject(true); const disabledActionIds$ = new BehaviorSubject(undefined); - /** TODO: Handle loading; loading should be true if any child is loading */ - const dataLoading$ = new BehaviorSubject(false); - const unsavedChanges = initializeControlGroupUnsavedChanges( selectionsManager.applySelections, controlsManager.api.children$, @@ -122,7 +115,10 @@ export const getControlGroupEmbeddableFactory = (services: { (next: ParentIgnoreSettings | undefined) => ignoreParentSettings$.next(next), fastIsEqual, ], - labelPosition: [labelPosition$, (next: ControlStyle) => labelPosition$.next(next)], + labelPosition: [ + labelPosition$, + (next: ControlLabelPosition) => labelPosition$.next(next), + ], }, controlsManager.snapshotControlsRuntimeState, controlsManager.resetControlsUnsavedChanges, @@ -157,18 +153,13 @@ export const getControlGroupEmbeddableFactory = (services: { initialChildControlState: controlsManager.snapshotControlsRuntimeState(), }; }, - dataLoading: dataLoading$, onEdit: async () => { - openEditControlGroupFlyout( - api, - { - chainingSystem: chainingSystem$, - labelPosition: labelPosition$, - autoApplySelections: autoApplySelections$, - ignoreParentSettings: ignoreParentSettings$, - }, - { core: services.core } - ); + openEditControlGroupFlyout(api, { + chainingSystem: chainingSystem$, + labelPosition: labelPosition$, + autoApplySelections: autoApplySelections$, + ignoreParentSettings: ignoreParentSettings$, + }); }, isEditingEnabled: () => true, openAddDataControlFlyout: (settings) => { @@ -193,7 +184,6 @@ export const getControlGroupEmbeddableFactory = (services: { settings?.onSave?.(); }, controlGroupApi: api, - services, }); }, serializeState: () => { @@ -201,7 +191,7 @@ export const getControlGroupEmbeddableFactory = (services: { return { rawState: { chainingSystem: chainingSystem$.getValue(), - controlStyle: labelPosition$.getValue(), // Rename "labelPosition" to "controlStyle" + controlStyle: labelPosition$.getValue(), showApplySelections: !autoApplySelections$.getValue(), ignoreParentSettingsJSON: JSON.stringify(ignoreParentSettings$.getValue()), panelsJSON, @@ -265,10 +255,9 @@ export const getControlGroupEmbeddableFactory = (services: { /** Fetch the allowExpensiveQuries setting for the children to use if necessary */ const fetchAllowExpensiveQueries = async () => { try { - const { allowExpensiveQueries } = await services.core.http.get<{ + const { allowExpensiveQueries } = await coreServices.http.get<{ allowExpensiveQueries: boolean; - // TODO: Rename this route as part of https://github.com/elastic/kibana/issues/174961 - }>('/internal/controls/optionsList/getExpensiveQueriesSetting', { + }>('/internal/controls/getExpensiveQueriesSetting', { version: '1', }); if (!allowExpensiveQueries) { diff --git a/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx b/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx index 5e7026282123a..5e7baf1f73e5d 100644 --- a/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx +++ b/src/plugins/controls/public/react_controls/control_group/open_edit_control_group_flyout.tsx @@ -8,7 +8,6 @@ */ import { OverlayRef } from '@kbn/core-mount-utils-browser'; -import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { apiHasParentApi } from '@kbn/presentation-publishing'; @@ -19,13 +18,11 @@ import { BehaviorSubject } from 'rxjs'; import { ControlStateManager } from '../controls/types'; import { ControlGroupEditor } from './components/control_group_editor'; import { ControlGroupApi, ControlGroupEditorState } from './types'; +import { coreServices } from '../../services/kibana_services'; export const openEditControlGroupFlyout = ( controlGroupApi: ControlGroupApi, - stateManager: ControlStateManager, - services: { - core: CoreStart; - } + stateManager: ControlStateManager ) => { /** * Duplicate all state into a new manager because we do not want to actually apply the changes @@ -50,7 +47,7 @@ export const openEditControlGroupFlyout = ( }; const onDeleteAll = (ref: OverlayRef) => { - services.core.overlays + coreServices.overlays .openConfirm( i18n.translate('controls.controlGroup.management.delete.sub', { defaultMessage: 'Controls are not recoverable once removed.', @@ -77,7 +74,7 @@ export const openEditControlGroupFlyout = ( }); }; - const overlay = services.core.overlays.openFlyout( + const overlay = coreServices.overlays.openFlyout( toMountPoint( closeOverlay(overlay)} />, { - theme: services.core.theme, - i18n: services.core.i18n, + theme: coreServices.theme, + i18n: coreServices.i18n, } ), { diff --git a/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts b/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts index 513633e46a875..a64faa63e8efc 100644 --- a/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts +++ b/src/plugins/controls/public/react_controls/control_group/register_control_group_embeddable.ts @@ -7,23 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; -import type { ControlsPluginStartDeps } from '../../types'; import { CONTROL_GROUP_TYPE } from '../../../common'; +import { untilPluginStartServicesReady } from '../../services/kibana_services'; -export function registerControlGroupEmbeddable( - coreSetup: CoreSetup, - embeddableSetup: EmbeddableSetup -) { +export function registerControlGroupEmbeddable(embeddableSetup: EmbeddableSetup) { embeddableSetup.registerReactEmbeddableFactory(CONTROL_GROUP_TYPE, async () => { - const [{ getControlGroupEmbeddableFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getControlGroupEmbeddableFactory }] = await Promise.all([ import('./get_control_group_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getControlGroupEmbeddableFactory({ - core: coreStart, - dataViews: depsStart.data.dataViews, - }); + return getControlGroupEmbeddableFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/control_group/types.ts b/src/plugins/controls/public/react_controls/control_group/types.ts index cb09cac975e7a..37f9f40c4079f 100644 --- a/src/plugins/controls/public/react_controls/control_group/types.ts +++ b/src/plugins/controls/public/react_controls/control_group/types.ts @@ -19,7 +19,6 @@ import { import { HasEditCapabilities, HasParentApi, - PublishesDataLoading, PublishesDisabledActionIds, PublishesFilters, PublishesTimeslice, @@ -35,8 +34,8 @@ import { ControlGroupEditorConfig, ControlGroupRuntimeState, ControlGroupSerializedState, + ControlLabelPosition, ControlPanelState, - ControlStyle, DefaultControlState, ParentIgnoreSettings, } from '../../../common'; @@ -54,7 +53,6 @@ export type ControlGroupApi = PresentationContainer & PublishesDataViews & HasSerializedChildState & HasEditCapabilities & - PublishesDataLoading & Pick, 'unsavedChanges'> & PublishesTimeslice & PublishesDisabledActionIds & @@ -62,7 +60,7 @@ export type ControlGroupApi = PresentationContainer & allowExpensiveQueries$: PublishingSubject; autoApplySelections$: PublishingSubject; ignoreParentSettings$: PublishingSubject; - labelPosition: PublishingSubject; + labelPosition: PublishingSubject; asyncResetUnsavedChanges: () => Promise; controlFetch$: (controlUuid: string) => Observable; diff --git a/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts b/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts index 91e5379416c5e..1c051e58af46f 100644 --- a/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts +++ b/src/plugins/controls/public/react_controls/control_group/utils/control_group_state_builder.ts @@ -13,11 +13,12 @@ import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, TIME_SLIDER_CONTROL, + type ControlGroupRuntimeState, + type ControlPanelsState, type DefaultDataControlState, } from '../../../../common'; -import { type ControlGroupRuntimeState, type ControlPanelsState } from '../../../../common'; import type { OptionsListControlState } from '../../../../common/options_list'; -import { pluginServices } from '../../../services'; +import { dataViewsService } from '../../../services/kibana_services'; import { getDataControlFieldRegistry } from '../../controls/data_controls/data_control_editor_utils'; import type { RangesliderControlState } from '../../controls/data_controls/range_slider/types'; @@ -82,7 +83,7 @@ export const controlGroupStateBuilder = { }; async function getCompatibleControlType(dataViewId: string, fieldName: string) { - const dataView = await pluginServices.getServices().dataViews.get(dataViewId); + const dataView = await dataViewsService.get(dataViewId); const fieldRegistry = await getDataControlFieldRegistry(dataView); const field = fieldRegistry[fieldName]; if (field.compatibleControlTypes.length === 0) { diff --git a/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts b/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts index 8bd19c3d6478c..ef81b4e30b361 100644 --- a/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts +++ b/src/plugins/controls/public/react_controls/control_group/utils/initialization_utils.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { type ControlGroupRuntimeState, DEFAULT_CONTROL_STYLE } from '../../../../common'; +import { DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState } from '../../../../common'; export const getDefaultControlGroupRuntimeState = (): ControlGroupRuntimeState => ({ initialChildControlState: {}, - labelPosition: DEFAULT_CONTROL_STYLE, + labelPosition: DEFAULT_CONTROL_LABEL_POSITION, chainingSystem: 'HIERARCHICAL', autoApplySelections: true, ignoreParentSettings: { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx index 5f8f17c57bce5..8d8385d603fb3 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.test.tsx @@ -12,7 +12,6 @@ import { BehaviorSubject } from 'rxjs'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; import { stubFieldSpecMap } from '@kbn/data-views-plugin/common/field.stub'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { TimeRange } from '@kbn/es-query'; import { I18nProvider } from '@kbn/i18n-react'; import { act, fireEvent, render, RenderResult, waitFor } from '@testing-library/react'; @@ -22,6 +21,7 @@ import { DEFAULT_CONTROL_WIDTH, type DefaultDataControlState, } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import { getAllControlTypes, getControlFactory } from '../../control_factory_registry'; import type { ControlGroupApi } from '../../control_group/types'; import type { ControlFactory } from '../types'; @@ -39,7 +39,6 @@ jest.mock('../../control_factory_registry', () => ({ getControlFactory: jest.fn(), })); -const mockDataViews = dataViewPluginMocks.createStartContract(); const mockDataView = createStubDataView({ spec: { id: 'logstash-*', @@ -58,7 +57,6 @@ const mockDataView = createStubDataView({ timeFieldName: '@timestamp', }, }); -mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); const dashboardApi = { timeRange$: new BehaviorSubject(undefined), @@ -82,7 +80,7 @@ describe('Data control editor', () => { controlType?: string; initialDefaultPanelTitle?: string; }) => { - mockDataViews.get = jest.fn().mockResolvedValue(mockDataView); + dataViewsService.get = jest.fn().mockResolvedValue(mockDataView); const controlEditor = render( @@ -97,13 +95,12 @@ describe('Data control editor', () => { controlId={controlId} controlType={controlType} initialDefaultPanelTitle={initialDefaultPanelTitle} - services={{ dataViews: mockDataViews }} /> ); await waitFor(() => { - expect(mockDataViews.get).toHaveBeenCalledTimes(1); + expect(dataViewsService.get).toHaveBeenCalledTimes(1); }); return controlEditor; diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx index 5254fe200e97c..35e21ca3b407a 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/data_control_editor.tsx @@ -33,7 +33,6 @@ import { EuiToolTip, } from '@elastic/eui'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { LazyDataViewPicker, LazyFieldPicker, @@ -46,6 +45,7 @@ import { type ControlWidth, type DefaultDataControlState, } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import { getAllControlTypes, getControlFactory } from '../../control_factory_registry'; import type { ControlGroupApi } from '../../control_group/types'; import { DataControlEditorStrings } from './data_control_constants'; @@ -67,9 +67,6 @@ export interface ControlEditorProps< controlGroupApi: ControlGroupApi; // controls must always have a parent API onCancel: (newState: Partial) => void; onSave: (newState: Partial, type: string) => void; - services: { - dataViews: DataViewsPublicPluginStart; - }; } const FieldPicker = withSuspense(LazyFieldPicker, null); @@ -151,8 +148,6 @@ export const DataControlEditor = ) => { const [editorState, setEditorState] = useState>(initialState); const [defaultPanelTitle, setDefaultPanelTitle] = useState( @@ -163,16 +158,14 @@ export const DataControlEditor = (true); const editorConfig = useMemo(() => controlGroupApi.getEditorConfig(), [controlGroupApi]); - // TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 const { loading: dataViewListLoading, value: dataViewListItems = [], error: dataViewListError, } = useAsync(async () => { - return dataViewService.getIdsWithTitle(); + return dataViewsService.getIdsWithTitle(); }); - // TODO: Maybe remove `useAsync` - see https://github.com/elastic/kibana/pull/182842#discussion_r1624909709 const { loading: dataViewLoading, value: { selectedDataView, fieldRegistry } = { @@ -185,7 +178,7 @@ export const DataControlEditor = { return await loadFieldRegistryFromDataView(dataView); @@ -21,7 +20,6 @@ export const getDataControlFieldRegistry = memoize( (dataView: DataView) => [dataView.id, JSON.stringify(dataView.fields.getAll())].join('|') ); -/** TODO: This function is duplicated from the controls plugin to avoid exporting it */ const loadFieldRegistryFromDataView = async ( dataView: DataView ): Promise => { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx index 8ff50a2bc4ada..d189d0aaa1ae9 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.test.tsx @@ -7,10 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock } from '@kbn/core/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import type { DataView } from '@kbn/data-views-plugin/public'; import { first, skip } from 'rxjs'; +import { dataViewsService } from '../../../services/kibana_services'; import { ControlGroupApi } from '../../control_group/types'; import { initializeDataControl } from './initialize_data_control'; @@ -21,9 +20,8 @@ describe('initializeDataControl', () => { }; const editorStateManager = {}; const controlGroupApi = {} as unknown as ControlGroupApi; - const mockDataViews = dataViewPluginMocks.createStartContract(); - // @ts-ignore - mockDataViews.get = async (id: string): Promise => { + + dataViewsService.get = async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); } @@ -40,10 +38,6 @@ describe('initializeDataControl', () => { }, } as unknown as DataView; }; - const services = { - core: coreMock.createStart(), - dataViews: mockDataViews, - }; describe('dataViewId subscription', () => { describe('no blocking errors', () => { @@ -55,8 +49,7 @@ describe('initializeDataControl', () => { 'referenceNameSuffix', dataControlState, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => { @@ -90,8 +83,7 @@ describe('initializeDataControl', () => { dataViewId: 'notGonnaFindMeDataViewId', }, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.dataViews.pipe(skip(1), first()).subscribe(() => { @@ -129,8 +121,7 @@ describe('initializeDataControl', () => { fieldName: 'notGonnaFindMeFieldName', }, editorStateManager, - controlGroupApi, - services + controlGroupApi ); dataControl.api.defaultPanelTitle!.pipe(skip(1), first()).subscribe(() => { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts index 6b814efa1ec3d..11fb453d56350 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/initialize_data_control.ts @@ -10,19 +10,18 @@ import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest, debounceTime, first, skip, switchMap, tap } from 'rxjs'; -import { CoreStart } from '@kbn/core-lifecycle-browser'; import { DATA_VIEW_SAVED_OBJECT_TYPE, DataView, DataViewField, } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { Filter } from '@kbn/es-query'; import { SerializedPanelState } from '@kbn/presentation-containers'; import { StateComparators } from '@kbn/presentation-publishing'; import { i18n } from '@kbn/i18n'; import type { DefaultControlState, DefaultDataControlState } from '../../../../common'; +import { dataViewsService } from '../../../services/kibana_services'; import type { ControlGroupApi } from '../../control_group/types'; import { initializeDefaultControlApi } from '../initialize_default_control_api'; import type { ControlApiInitialization, ControlStateManager } from '../types'; @@ -40,11 +39,7 @@ export const initializeDataControl = ( * responsible for managing */ editorStateManager: ControlStateManager, - controlGroupApi: ControlGroupApi, - services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; - } + controlGroupApi: ControlGroupApi ): { api: ControlApiInitialization; cleanup: () => void; @@ -88,7 +83,7 @@ export const initializeDataControl = ( switchMap(async (currentDataViewId) => { let dataView: DataView | undefined; try { - dataView = await services.dataViews.get(currentDataViewId); + dataView = await dataViewsService.get(currentDataViewId); return { dataView }; } catch (error) { return { error }; @@ -156,7 +151,6 @@ export const initializeDataControl = ( // open the editor to get the new state openDataControlEditor({ - services, onSave: ({ type: newType, state: newState }) => { if (newType === controlType) { // apply the changes from the new state via the state manager diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx index fe629555dea3c..08118702a003e 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/open_data_control_editor.tsx @@ -10,14 +10,14 @@ import React from 'react'; import deepEqual from 'react-fast-compare'; -import { CoreStart, OverlayRef } from '@kbn/core/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import { OverlayRef } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { tracksOverlays } from '@kbn/presentation-containers'; import { apiHasParentApi } from '@kbn/presentation-publishing'; import { toMountPoint } from '@kbn/react-kibana-mount'; import type { DefaultDataControlState } from '../../../../common'; +import { coreServices } from '../../../services/kibana_services'; import type { ControlGroupApi } from '../../control_group/types'; import { DataControlEditor } from './data_control_editor'; @@ -30,7 +30,6 @@ export const openDataControlEditor = < initialDefaultPanelTitle, onSave, controlGroupApi, - services, }: { initialState: Partial; controlType?: string; @@ -38,10 +37,6 @@ export const openDataControlEditor = < initialDefaultPanelTitle?: string; onSave: ({ type, state }: { type: string; state: Partial }) => void; controlGroupApi: ControlGroupApi; - services: { - core: CoreStart; - dataViews: DataViewsPublicPluginStart; - }; }): void => { const closeOverlay = (overlayRef: OverlayRef) => { if (apiHasParentApi(controlGroupApi) && tracksOverlays(controlGroupApi.parentApi)) { @@ -55,7 +50,7 @@ export const openDataControlEditor = < closeOverlay(overlay); return; } - services.core.overlays + coreServices.overlays .openConfirm( i18n.translate('controls.controlGroup.management.discard.sub', { defaultMessage: `Changes that you've made to this control will be discarded, are you sure you want to continue?`, @@ -80,7 +75,7 @@ export const openDataControlEditor = < }); }; - const overlay = services.core.overlays.openFlyout( + const overlay = coreServices.overlays.openFlyout( toMountPoint( controlGroupApi={controlGroupApi} @@ -95,11 +90,10 @@ export const openDataControlEditor = < closeOverlay(overlay); onSave({ type: selectedControlType, state }); }} - services={{ dataViews: services.dataViews }} />, { - theme: services.core.theme, - i18n: services.core.i18n, + theme: coreServices.theme, + i18n: coreServices.i18n, } ), { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx index c2b1d7d84250e..2e2cd341e8704 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/fetch_and_validate.tsx @@ -26,13 +26,11 @@ import { isValidSearch } from '../../../../../common/options_list/is_valid_searc import { OptionsListSelection } from '../../../../../common/options_list/options_list_selections'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlStateManager } from '../../types'; -import { DataControlServices } from '../types'; import { OptionsListFetchCache } from './options_list_fetch_cache'; import { OptionsListComponentApi, OptionsListComponentState, OptionsListControlApi } from './types'; export function fetchAndValidate$({ api, - services, stateManager, }: { api: Pick & @@ -41,7 +39,6 @@ export function fetchAndValidate$({ loadingSuggestions$: BehaviorSubject; debouncedSearchString: Observable; }; - services: DataControlServices; stateManager: ControlStateManager< Pick > & { @@ -126,7 +123,7 @@ export function fetchAndValidate$({ const newAbortController = new AbortController(); abortController = newAbortController; try { - return await requestCache.runFetchRequest(request, newAbortController.signal, services); + return await requestCache.runFetchRequest(request, newAbortController.signal); } catch (error) { return { error }; } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx index 99e15a3d0f31f..20911d1cdb872 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.test.tsx @@ -9,26 +9,22 @@ import React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataView } from '@kbn/data-views-plugin/common'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { act, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { coreServices, dataViewsService } from '../../../../services/kibana_services'; import { getMockedBuildApi, getMockedControlGroupApi } from '../../mocks/control_mocks'; import { getOptionsListControlFactory } from './get_options_list_control_factory'; describe('Options List Control Api', () => { const uuid = 'myControl1'; const controlGroupApi = getMockedControlGroupApi(); - const mockDataViews = dataViewPluginMocks.createStartContract(); - const mockCore = coreMock.createStart(); const waitOneTick = () => act(() => new Promise((resolve) => setTimeout(resolve, 0))); - mockDataViews.get = jest.fn().mockImplementation(async (id: string): Promise => { + dataViewsService.get = jest.fn().mockImplementation(async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`Simulated error: no data view found for id ${id}`); } @@ -60,11 +56,7 @@ describe('Options List Control Api', () => { return stubDataView; }); - const factory = getOptionsListControlFactory({ - core: mockCore, - data: dataPluginMock.createStartContract(), - dataViews: mockDataViews, - }); + const factory = getOptionsListControlFactory(); describe('filters$', () => { test('should not set filters$ when selectedOptions is not provided', async () => { @@ -177,7 +169,7 @@ describe('Options List Control Api', () => { describe('make selection', () => { beforeAll(() => { - mockCore.http.fetch = jest.fn().mockResolvedValue({ + coreServices.http.fetch = jest.fn().mockResolvedValue({ suggestions: [ { value: 'woof', docCount: 10 }, { value: 'bark', docCount: 15 }, diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx index d0c40736552ce..2a23ac9341ab9 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/get_options_list_control_factory.tsx @@ -9,7 +9,7 @@ import fastIsEqual from 'fast-deep-equal'; import React, { useEffect } from 'react'; -import { BehaviorSubject, combineLatest, debounceTime, filter, skip } from 'rxjs'; +import { BehaviorSubject, combineLatest, debounceTime, filter, map, skip } from 'rxjs'; import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter, Filter } from '@kbn/es-query'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; @@ -25,7 +25,7 @@ import type { } from '../../../../../common/options_list'; import { getSelectionAsFieldType, isValidSearch } from '../../../../../common/options_list'; import { initializeDataControl } from '../initialize_data_control'; -import type { DataControlFactory, DataControlServices } from '../types'; +import type { DataControlFactory } from '../types'; import { OptionsListControl } from './components/options_list_control'; import { OptionsListEditorOptions } from './components/options_list_editor_options'; import { @@ -39,9 +39,10 @@ import { initializeOptionsListSelections } from './options_list_control_selectio import { OptionsListStrings } from './options_list_strings'; import type { OptionsListControlApi } from './types'; -export const getOptionsListControlFactory = ( - services: DataControlServices -): DataControlFactory => { +export const getOptionsListControlFactory = (): DataControlFactory< + OptionsListControlState, + OptionsListControlApi +> => { return { type: OPTIONS_LIST_CONTROL, order: 3, // should always be first, since this is the most popular control @@ -78,6 +79,7 @@ export const getOptionsListControlFactory = ( const searchStringValid$ = new BehaviorSubject(true); const requestSize$ = new BehaviorSubject(MIN_OPTIONS_LIST_REQUEST_SIZE); + const dataLoading$ = new BehaviorSubject(undefined); const availableOptions$ = new BehaviorSubject(undefined); const invalidSelections$ = new BehaviorSubject>(new Set()); const totalCardinality$ = new BehaviorSubject(0); @@ -90,8 +92,7 @@ export const getOptionsListControlFactory = ( 'optionsListDataView', initialState, { searchTechnique: searchTechnique$, singleSelect: singleSelect$ }, - controlGroupApi, - services + controlGroupApi ); const selections = initializeOptionsListSelections( @@ -115,12 +116,16 @@ export const getOptionsListControlFactory = ( /** Handle loading state; since suggestion fetching and validation are tied, only need one loading subject */ const loadingSuggestions$ = new BehaviorSubject(false); - const dataLoadingSubscription = loadingSuggestions$ + const dataLoadingSubscription = combineLatest([ + loadingSuggestions$, + dataControl.api.dataLoading, + ]) .pipe( - debounceTime(100) // debounce set loading so that it doesn't flash as the user types + debounceTime(100), // debounce set loading so that it doesn't flash as the user types + map((values) => values.some((value) => value)) ) .subscribe((isLoading) => { - dataControl.api.setDataLoading(isLoading); + dataLoading$.next(isLoading); }); /** Debounce the search string changes to reduce the number of fetch requests */ @@ -161,7 +166,6 @@ export const getOptionsListControlFactory = ( /** Fetch the suggestions and perform validation */ const loadMoreSubject = new BehaviorSubject(null); const fetchSubscription = fetchAndValidate$({ - services, api: { ...dataControl.api, loadMoreSubject, @@ -235,6 +239,7 @@ export const getOptionsListControlFactory = ( const api = buildApi( { ...dataControl.api, + dataLoading: dataLoading$, getTypeDisplayName: OptionsListStrings.control.getDisplayName, serializeState: () => { const { rawState: dataControlState, references } = dataControl.serialize(); diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts index 548b1efebd02a..60b1463118733 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/options_list_fetch_cache.ts @@ -20,7 +20,7 @@ import type { OptionsListResponse, OptionsListSuccessResponse, } from '../../../../../common/options_list/types'; -import type { DataControlServices } from '../types'; +import { coreServices, dataService } from '../../../../services/kibana_services'; const REQUEST_CACHE_SIZE = 50; // only store a max of 50 responses const REQUEST_CACHE_TTL = 1000 * 60; // time to live = 1 minute @@ -80,8 +80,7 @@ export class OptionsListFetchCache { public async runFetchRequest( request: OptionsListRequest, - abortSignal: AbortSignal, - services: DataControlServices + abortSignal: AbortSignal ): Promise { const requestHash = this.getRequestHash(request); @@ -90,11 +89,11 @@ export class OptionsListFetchCache { } else { const index = request.dataView.getIndexPattern(); - const timeService = services.data.query.timefilter.timefilter; + const timeService = dataService.query.timefilter.timefilter; const { query, filters, dataView, timeRange, field, ...passThroughProps } = request; const timeFilter = timeRange ? timeService.createFilter(dataView, timeRange) : undefined; const filtersToUse = [...(filters ?? []), ...(timeFilter ? [timeFilter] : [])]; - const config = getEsQueryConfig(services.core.uiSettings); + const config = getEsQueryConfig(coreServices.uiSettings); const esFilters = [buildEsQuery(dataView, query ?? [], filtersToUse ?? [], config)]; const requestBody = { @@ -105,7 +104,7 @@ export class OptionsListFetchCache { runtimeFieldMap: dataView.toSpec?.().runtimeFieldMap, }; - const result = await services.core.http.fetch( + const result = await coreServices.http.fetch( `/internal/controls/optionsList/${index}`, { version: '1', diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts index 417eb42d4b1bd..b58189a75daca 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/options_list_control/register_options_list_control.ts @@ -7,21 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../../types'; -import { registerControlFactory } from '../../../control_factory_registry'; import { OPTIONS_LIST_CONTROL } from '../../../../../common'; +import { untilPluginStartServicesReady } from '../../../../services/kibana_services'; +import { registerControlFactory } from '../../../control_factory_registry'; -export function registerOptionsListControl(coreSetup: CoreSetup) { +export function registerOptionsListControl() { registerControlFactory(OPTIONS_LIST_CONTROL, async () => { - const [{ getOptionsListControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getOptionsListControlFactory }] = await Promise.all([ import('./get_options_list_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getOptionsListControlFactory({ - core: coreStart, - data: depsStart.data, - dataViews: depsStart.data.dataViews, - }); + return getOptionsListControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx index 76cb52981e8c1..925ec3443849a 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.test.tsx @@ -11,13 +11,11 @@ import React from 'react'; import { of } from 'rxjs'; import { estypes } from '@elastic/elasticsearch'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { SerializedPanelState } from '@kbn/presentation-containers'; import { fireEvent, render, waitFor } from '@testing-library/react'; +import { dataService, dataViewsService } from '../../../../services/kibana_services'; import { getMockedBuildApi, getMockedControlGroupApi } from '../../mocks/control_mocks'; import { getRangesliderControlFactory } from './get_range_slider_control_factory'; import { RangesliderControlState } from './types'; @@ -31,11 +29,10 @@ describe('RangesliderControlApi', () => { const controlGroupApi = getMockedControlGroupApi(); - const dataStartServiceMock = dataPluginMock.createStartContract(); let totalResults = DEFAULT_TOTAL_RESULTS; let min: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MIN; let max: estypes.AggregationsSingleMetricAggregateBase['value'] = DEFAULT_MAX; - dataStartServiceMock.search.searchSource.create = jest.fn().mockImplementation(() => { + dataService.search.searchSource.create = jest.fn().mockImplementation(() => { let isAggsRequest = false; return { setField: (key: string) => { @@ -54,9 +51,8 @@ describe('RangesliderControlApi', () => { }, }; }); - const mockDataViews = dataViewPluginMocks.createStartContract(); - mockDataViews.get = jest.fn().mockImplementation(async (id: string): Promise => { + dataViewsService.get = jest.fn().mockImplementation(async (id: string): Promise => { if (id !== 'myDataViewId') { throw new Error(`no data view found for id ${id}`); } @@ -82,11 +78,7 @@ describe('RangesliderControlApi', () => { } as unknown as DataView; }); - const factory = getRangesliderControlFactory({ - core: coreMock.createStart(), - data: dataStartServiceMock, - dataViews: mockDataViews, - }); + const factory = getRangesliderControlFactory(); beforeEach(() => { totalResults = DEFAULT_TOTAL_RESULTS; diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx index 596206dc2f4f6..3ad3b97af7414 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/get_range_slider_control_factory.tsx @@ -16,7 +16,7 @@ import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { RANGE_SLIDER_CONTROL } from '../../../../../common'; import { initializeDataControl } from '../initialize_data_control'; -import type { DataControlFactory, DataControlServices } from '../types'; +import type { DataControlFactory } from '../types'; import { RangeSliderControl } from './components/range_slider_control'; import { hasNoResults$ } from './has_no_results'; import { minMax$ } from './min_max'; @@ -24,9 +24,10 @@ import { initializeRangeControlSelections } from './range_control_selections'; import { RangeSliderStrings } from './range_slider_strings'; import type { RangesliderControlApi, RangesliderControlState } from './types'; -export const getRangesliderControlFactory = ( - services: DataControlServices -): DataControlFactory => { +export const getRangesliderControlFactory = (): DataControlFactory< + RangesliderControlState, + RangesliderControlApi +> => { return { type: RANGE_SLIDER_CONTROL, getIconType: () => 'controlsHorizontal', @@ -71,8 +72,7 @@ export const getRangesliderControlFactory = ( { step: step$, }, - controlGroupApi, - services + controlGroupApi ); const selections = initializeRangeControlSelections( @@ -111,13 +111,14 @@ export const getRangesliderControlFactory = ( } ); - const dataLoadingSubscription = combineLatest([loadingMinMax$, loadingHasNoResults$]) + const dataLoadingSubscription = combineLatest([ + loadingMinMax$, + loadingHasNoResults$, + dataControl.api.dataLoading, + ]) .pipe( - map((values) => { - return values.some((value) => { - return value; - }); - }) + debounceTime(100), + map((values) => values.some((value) => value)) ) .subscribe((isLoading) => { dataLoading$.next(isLoading); @@ -138,7 +139,6 @@ export const getRangesliderControlFactory = ( const min$ = new BehaviorSubject(undefined); const minMaxSubscription = minMax$({ controlFetch$, - data: services.data, dataViews$: dataControl.api.dataViews, fieldName$: dataControl.stateManager.fieldName, setIsLoading: (isLoading: boolean) => { @@ -198,7 +198,6 @@ export const getRangesliderControlFactory = ( const selectionHasNoResults$ = new BehaviorSubject(false); const hasNotResultsSubscription = hasNoResults$({ controlFetch$, - data: services.data, dataViews$: dataControl.api.dataViews, rangeFilters$: dataControl.api.filters$, ignoreParentSettings$: controlGroupApi.ignoreParentSettings$, diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts index 27676f5f7b649..24d4510b3fc22 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/has_no_results.ts @@ -8,25 +8,23 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { PublishesDataViews } from '@kbn/presentation-publishing'; -import { combineLatest, lastValueFrom, Observable, switchMap, tap } from 'rxjs'; +import { Observable, combineLatest, lastValueFrom, switchMap, tap } from 'rxjs'; +import { dataService } from '../../../../services/kibana_services'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlGroupApi } from '../../../control_group/types'; import { DataControlApi } from '../types'; export function hasNoResults$({ controlFetch$, - data, dataViews$, rangeFilters$, ignoreParentSettings$, setIsLoading, }: { controlFetch$: Observable; - data: DataPublicPluginStart; dataViews$?: PublishesDataViews['dataViews']; rangeFilters$: DataControlApi['filters$']; ignoreParentSettings$: ControlGroupApi['ignoreParentSettings$']; @@ -53,7 +51,6 @@ export function hasNoResults$({ prevRequestAbortController = abortController; return await hasNoResults({ abortSignal: abortController.signal, - data, dataView, rangeFilter, ...controlFetchContext, @@ -71,7 +68,6 @@ export function hasNoResults$({ async function hasNoResults({ abortSignal, - data, dataView, filters, query, @@ -79,14 +75,13 @@ async function hasNoResults({ timeRange, }: { abortSignal: AbortSignal; - data: DataPublicPluginStart; dataView: DataView; filters?: Filter[]; query?: Query | AggregateQuery; rangeFilter: Filter; timeRange?: TimeRange; }): Promise { - const searchSource = await data.search.searchSource.create(); + const searchSource = await dataService.search.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); // Tracking total hits accurately has a performance cost @@ -97,7 +92,7 @@ async function hasNoResults({ const allFilters = filters ? [...filters] : []; allFilters.push(rangeFilter); if (timeRange) { - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + const timeFilter = dataService.query.timefilter.timefilter.createFilter(dataView, timeRange); if (timeFilter) allFilters.push(timeFilter); } if (allFilters.length) { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts index d3335e182f101..8e4d5e00374af 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/min_max.ts @@ -8,26 +8,24 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import { PublishesDataViews, PublishingSubject } from '@kbn/presentation-publishing'; -import { combineLatest, lastValueFrom, Observable, of, startWith, switchMap, tap } from 'rxjs'; import { apiPublishesReload } from '@kbn/presentation-publishing/interfaces/fetch/publishes_reload'; +import { Observable, combineLatest, lastValueFrom, of, startWith, switchMap, tap } from 'rxjs'; +import { dataService } from '../../../../services/kibana_services'; import { ControlFetchContext } from '../../../control_group/control_fetch'; import { ControlGroupApi } from '../../../control_group/types'; export function minMax$({ controlFetch$, controlGroupApi, - data, dataViews$, fieldName$, setIsLoading, }: { controlFetch$: Observable; controlGroupApi: ControlGroupApi; - data: DataPublicPluginStart; dataViews$: PublishesDataViews['dataViews']; fieldName$: PublishingSubject; setIsLoading: (isLoading: boolean) => void; @@ -60,7 +58,6 @@ export function minMax$({ prevRequestAbortController = abortController; return await getMinMax({ abortSignal: abortController.signal, - data, dataView, field: dataViewField, ...controlFetchContext, @@ -77,7 +74,6 @@ export function minMax$({ export async function getMinMax({ abortSignal, - data, dataView, field, filters, @@ -85,20 +81,19 @@ export async function getMinMax({ timeRange, }: { abortSignal: AbortSignal; - data: DataPublicPluginStart; dataView: DataView; field: DataViewField; filters?: Filter[]; query?: Query | AggregateQuery; timeRange?: TimeRange; }): Promise<{ min: number | undefined; max: number | undefined }> { - const searchSource = await data.search.searchSource.create(); + const searchSource = await dataService.search.searchSource.create(); searchSource.setField('size', 0); searchSource.setField('index', dataView); const allFilters = filters ? [...filters] : []; if (timeRange) { - const timeFilter = data.query.timefilter.timefilter.createFilter(dataView, timeRange); + const timeFilter = dataService.query.timefilter.timefilter.createFilter(dataView, timeRange); if (timeFilter) allFilters.push(timeFilter); } if (allFilters.length) { diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts index 4f77fc3bac7e4..0e1c0fd925792 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/range_slider/register_range_slider_control.ts @@ -7,22 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../../types'; -import { registerControlFactory } from '../../../control_factory_registry'; import { RANGE_SLIDER_CONTROL } from '../../../../../common'; +import { untilPluginStartServicesReady } from '../../../../services/kibana_services'; +import { registerControlFactory } from '../../../control_factory_registry'; -export function registerRangeSliderControl(coreSetup: CoreSetup) { +export function registerRangeSliderControl() { registerControlFactory(RANGE_SLIDER_CONTROL, async () => { - const [{ getRangesliderControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getRangesliderControlFactory }] = await Promise.all([ import('./get_range_slider_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getRangesliderControlFactory({ - core: coreStart, - data: depsStart.data, - dataViews: depsStart.data.dataViews, - }); + return getRangesliderControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/data_controls/types.ts b/src/plugins/controls/public/react_controls/controls/data_controls/types.ts index 9eac141642402..89912e6eabb03 100644 --- a/src/plugins/controls/public/react_controls/controls/data_controls/types.ts +++ b/src/plugins/controls/public/react_controls/controls/data_controls/types.ts @@ -7,10 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/common'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { FieldFormatConvertFunction } from '@kbn/field-formats-plugin/common'; import { HasEditCapabilities, @@ -62,12 +59,6 @@ export const isDataControlFactory = ( return typeof (factory as DataControlFactory).isFieldCompatible === 'function'; }; -export interface DataControlServices { - core: CoreStart; - data: DataPublicPluginStart; - dataViews: DataViewsPublicPluginStart; -} - interface DataControlField { field: DataViewField; compatibleControlTypes: string[]; diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts index 1cccb264d19e7..5c84cfbdef508 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_time_range_meta.ts @@ -9,6 +9,7 @@ import { EuiRangeTick } from '@elastic/eui'; import { TimeRange } from '@kbn/es-query'; +import { coreServices, dataService } from '../../../services/kibana_services'; import { FROM_INDEX, getStepSize, @@ -17,7 +18,6 @@ import { roundUpToNextStepSizeFactor, TO_INDEX, } from './time_utils'; -import { Services } from './types'; export interface TimeRangeMeta { format: string; @@ -29,12 +29,9 @@ export interface TimeRangeMeta { timeRangeMin: number; } -export function getTimeRangeMeta( - timeRange: TimeRange | undefined, - services: Services -): TimeRangeMeta { - const nextBounds = timeRangeToBounds(timeRange ?? getDefaultTimeRange(services), services); - const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], getTimezone(services)); +export function getTimeRangeMeta(timeRange: TimeRange | undefined): TimeRangeMeta { + const nextBounds = timeRangeToBounds(timeRange ?? getDefaultTimeRange()); + const ticks = getTicks(nextBounds[FROM_INDEX], nextBounds[TO_INDEX], getTimezone()); const { format, stepSize } = getStepSize(ticks); return { format, @@ -47,17 +44,17 @@ export function getTimeRangeMeta( }; } -export function getTimezone(services: Services) { - return services.core.uiSettings.get('dateFormat:tz', 'Browser'); +export function getTimezone() { + return coreServices.uiSettings.get('dateFormat:tz', 'Browser'); } -function getDefaultTimeRange(services: Services) { - const defaultTimeRange = services.core.uiSettings.get('timepicker:timeDefaults'); +function getDefaultTimeRange() { + const defaultTimeRange = coreServices.uiSettings.get('timepicker:timeDefaults'); return defaultTimeRange ? defaultTimeRange : { from: 'now-15m', to: 'now' }; } -function timeRangeToBounds(timeRange: TimeRange, services: Services): [number, number] { - const timeRangeBounds = services.data.query.timefilter.timefilter.calculateBounds(timeRange); +function timeRangeToBounds(timeRange: TimeRange): [number, number] { + const timeRangeBounds = dataService.query.timefilter.timefilter.calculateBounds(timeRange); return timeRangeBounds.min === undefined || timeRangeBounds.max === undefined ? [Date.now() - 1000 * 60 * 15, Date.now()] : [timeRangeBounds.min.valueOf(), timeRangeBounds.max.valueOf()]; diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx index 12381ad83c407..d4b8ff6c13461 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.test.tsx @@ -7,14 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { coreMock } from '@kbn/core/public/mocks'; -import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import React from 'react'; +import { BehaviorSubject } from 'rxjs'; + import dateMath from '@kbn/datemath'; import { TimeRange } from '@kbn/es-query'; import { StateComparators } from '@kbn/presentation-publishing'; import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; -import { BehaviorSubject } from 'rxjs'; + +import { dataService } from '../../../services/kibana_services'; import { getMockedControlGroupApi } from '../mocks/control_mocks'; import { ControlApiRegistration } from '../types'; import { getTimesliderControlFactory } from './get_timeslider_control_factory'; @@ -28,18 +29,14 @@ describe('TimesliderControlApi', () => { }; const controlGroupApi = getMockedControlGroupApi(dashboardApi); - const dataStartServiceMock = dataPluginMock.createStartContract(); - dataStartServiceMock.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { + dataService.query.timefilter.timefilter.calculateBounds = (timeRange: TimeRange) => { const now = new Date(); return { min: dateMath.parse(timeRange.from, { forceNow: now }), max: dateMath.parse(timeRange.to, { roundUp: true, forceNow: now }), }; }; - const factory = getTimesliderControlFactory({ - core: coreMock.createStart(), - data: dataStartServiceMock, - }); + const factory = getTimesliderControlFactory(); let comparators: StateComparators | undefined; function buildApiMock( api: ControlApiRegistration, diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx index 20baf8fb545e1..cfc8e50bee1b5 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/get_timeslider_control_factory.tsx @@ -36,22 +36,23 @@ import { roundDownToNextStepSizeFactor, roundUpToNextStepSizeFactor, } from './time_utils'; -import { Services, Timeslice, TimesliderControlApi, TimesliderControlState } from './types'; +import { Timeslice, TimesliderControlApi, TimesliderControlState } from './types'; const displayName = i18n.translate('controls.timesliderControl.displayName', { defaultMessage: 'Time slider', }); -export const getTimesliderControlFactory = ( - services: Services -): ControlFactory => { +export const getTimesliderControlFactory = (): ControlFactory< + TimesliderControlState, + TimesliderControlApi +> => { return { type: TIME_SLIDER_CONTROL, getIconType: () => 'search', getDisplayName: () => displayName, buildControl: async (initialState, buildApi, uuid, controlGroupApi) => { const { timeRangeMeta$, formatDate, cleanupTimeRangeSubscription } = - initTimeRangeSubscription(controlGroupApi, services); + initTimeRangeSubscription(controlGroupApi); const timeslice$ = new BehaviorSubject<[number, number] | undefined>(undefined); const isAnchored$ = new BehaviorSubject(initialState.isAnchored); const isPopoverOpen$ = new BehaviorSubject(false); diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts index 7b4a2deb9f0d1..7934e9deaa9b4 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/init_time_range_subscription.ts @@ -14,26 +14,23 @@ import moment from 'moment'; import { BehaviorSubject, skip } from 'rxjs'; import { getTimeRangeMeta, getTimezone, TimeRangeMeta } from './get_time_range_meta'; import { getMomentTimezone } from './time_utils'; -import { Services } from './types'; -export function initTimeRangeSubscription(controlGroupApi: unknown, services: Services) { +export function initTimeRangeSubscription(controlGroupApi: unknown) { const timeRange$ = apiHasParentApi(controlGroupApi) && apiPublishesTimeRange(controlGroupApi.parentApi) ? controlGroupApi.parentApi.timeRange$ : new BehaviorSubject(undefined); - const timeRangeMeta$ = new BehaviorSubject( - getTimeRangeMeta(timeRange$.value, services) - ); + const timeRangeMeta$ = new BehaviorSubject(getTimeRangeMeta(timeRange$.value)); const timeRangeSubscription = timeRange$.pipe(skip(1)).subscribe((timeRange) => { - timeRangeMeta$.next(getTimeRangeMeta(timeRange, services)); + timeRangeMeta$.next(getTimeRangeMeta(timeRange)); }); return { timeRangeMeta$, formatDate: (epoch: number) => { return moment - .tz(epoch, getMomentTimezone(getTimezone(services))) + .tz(epoch, getMomentTimezone(getTimezone())) .locale(i18n.getLocale()) .format(timeRangeMeta$.value.format); }, diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts index 8fbf23305820f..338a52631c931 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/register_timeslider_control.ts @@ -7,20 +7,16 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { CoreSetup } from '@kbn/core/public'; -import type { ControlsPluginStartDeps } from '../../../types'; -import { registerControlFactory } from '../../control_factory_registry'; import { TIME_SLIDER_CONTROL } from '../../../../common'; +import { untilPluginStartServicesReady } from '../../../services/kibana_services'; +import { registerControlFactory } from '../../control_factory_registry'; -export function registerTimeSliderControl(coreSetup: CoreSetup) { +export function registerTimeSliderControl() { registerControlFactory(TIME_SLIDER_CONTROL, async () => { - const [{ getTimesliderControlFactory }, [coreStart, depsStart]] = await Promise.all([ + const [{ getTimesliderControlFactory }] = await Promise.all([ import('./get_timeslider_control_factory'), - coreSetup.getStartServices(), + untilPluginStartServicesReady(), ]); - return getTimesliderControlFactory({ - core: coreStart, - data: depsStart.data, - }); + return getTimesliderControlFactory(); }); } diff --git a/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts b/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts index 702d02ae9accc..634e0351e77eb 100644 --- a/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts +++ b/src/plugins/controls/public/react_controls/controls/timeslider_control/types.ts @@ -7,8 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreStart } from '@kbn/core/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { PublishesPanelTitle, PublishesTimeslice } from '@kbn/presentation-publishing'; import type { DefaultControlState } from '../../../../common'; import type { DefaultControlApi } from '../types'; @@ -25,8 +23,3 @@ export interface TimesliderControlState extends DefaultControlState { export type TimesliderControlApi = DefaultControlApi & Pick & PublishesTimeslice; - -export interface Services { - core: CoreStart; - data: DataPublicPluginStart; -} diff --git a/src/plugins/controls/public/react_controls/controls/types.ts b/src/plugins/controls/public/react_controls/controls/types.ts index 85045f8bd70ba..ce4ad9f194fa3 100644 --- a/src/plugins/controls/public/react_controls/controls/types.ts +++ b/src/plugins/controls/public/react_controls/controls/types.ts @@ -40,15 +40,15 @@ export type DefaultControlApi = PublishesDataLoading & HasType & HasUniqueId & HasParentApi & { - // Can not use HasSerializableState interface - // HasSerializableState types serializeState as function returning 'MaybePromise' - // Controls serializeState is sync - serializeState: () => SerializedPanelState; - /** TODO: Make these non-public as part of https://github.com/elastic/kibana/issues/174961 */ setDataLoading: (loading: boolean) => void; setBlockingError: (error: Error | undefined) => void; grow: PublishingSubject; width: PublishingSubject; + + // Can not use HasSerializableState interface + // HasSerializableState types serializeState as function returning 'MaybePromise' + // Controls serializeState is sync + serializeState: () => SerializedPanelState; }; export type ControlApiRegistration = Omit< @@ -62,7 +62,6 @@ export type ControlApiInitialization; -// TODO: Move this to the Control plugin's setup contract export interface ControlFactory< State extends DefaultControlState = DefaultControlState, ControlApi extends DefaultControlApi = DefaultControlApi diff --git a/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx b/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx index 58269308f1846..e034ca817908e 100644 --- a/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx +++ b/src/plugins/controls/public/react_controls/external_api/control_group_renderer.test.tsx @@ -9,26 +9,22 @@ import React from 'react'; -import { coreMock } from '@kbn/core/public/mocks'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { Filter } from '@kbn/es-query'; import { PublishesUnifiedSearch, PublishingSubject } from '@kbn/presentation-publishing'; import { act, render, waitFor } from '@testing-library/react'; import { ControlGroupRendererApi } from '.'; +import { CONTROL_GROUP_TYPE } from '../..'; import { getControlGroupEmbeddableFactory } from '../control_group/get_control_group_factory'; import { ControlGroupRenderer, ControlGroupRendererProps } from './control_group_renderer'; -import { CONTROL_GROUP_TYPE } from '../..'; type ParentApiType = PublishesUnifiedSearch & { unifiedSearchFilters$?: PublishingSubject; }; describe('control group renderer', () => { - const core = coreMock.createStart(); - const dataViews = dataViewPluginMocks.createStartContract(); - const factory = getControlGroupEmbeddableFactory({ core, dataViews }); + const factory = getControlGroupEmbeddableFactory(); const buildControlGroupSpy = jest.spyOn(factory, 'buildEmbeddable'); const mountControlGroupRenderer = async ( diff --git a/src/plugins/controls/public/services/controls/controls.stub.ts b/src/plugins/controls/public/services/controls/controls.stub.ts deleted file mode 100644 index 2e182998c9071..0000000000000 --- a/src/plugins/controls/public/services/controls/controls.stub.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlFactory, DefaultControlApi } from '../../react_controls/controls/types'; -import { ControlsServiceType } from './types'; - -export type ControlsServiceFactory = PluginServiceFactory; -export const controlsServiceFactory = () => getStubControlsService(); - -export const getStubControlsService = () => { - const controlsFactoriesMap: { [key: string]: ControlFactory } = {}; - - const mockRegisterControlFactory = async < - State extends object = object, - ApiType extends DefaultControlApi = DefaultControlApi - >( - controlType: string, - getFactory: () => Promise> - ) => { - controlsFactoriesMap[controlType] = (await getFactory()) as ControlFactory; - }; - - const mockGetControlFactory = < - State extends object = object, - ApiType extends DefaultControlApi = DefaultControlApi - >( - type: string - ) => { - return controlsFactoriesMap[type] as ControlFactory; - }; - - const getAllControlTypes = () => Object.keys(controlsFactoriesMap); - - return { - registerControlFactory: mockRegisterControlFactory, - getControlFactory: mockGetControlFactory, - getAllControlTypes, - }; -}; diff --git a/src/plugins/controls/public/services/controls/controls_service.ts b/src/plugins/controls/public/services/controls/controls_service.ts deleted file mode 100644 index c794c056a4f8d..0000000000000 --- a/src/plugins/controls/public/services/controls/controls_service.ts +++ /dev/null @@ -1,24 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - getAllControlTypes, - getControlFactory, - registerControlFactory, -} from '../../react_controls/control_factory_registry'; -import { ControlsServiceType } from './types'; - -export const controlsServiceFactory = () => controlsService; - -// export controls service directly for use in plugin setup lifecycle -export const controlsService: ControlsServiceType = { - registerControlFactory, - getControlFactory, - getAllControlTypes, -}; diff --git a/src/plugins/controls/public/services/controls/types.ts b/src/plugins/controls/public/services/controls/types.ts deleted file mode 100644 index d9011819d815c..0000000000000 --- a/src/plugins/controls/public/services/controls/types.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { - getAllControlTypes, - getControlFactory, - registerControlFactory, -} from '../../react_controls/control_factory_registry'; - -export type ControlsServiceFactory = PluginServiceFactory; - -export interface ControlsServiceType { - registerControlFactory: typeof registerControlFactory; - getControlFactory: typeof getControlFactory; - getAllControlTypes: typeof getAllControlTypes; -} diff --git a/src/plugins/controls/public/services/core/core.stub.ts b/src/plugins/controls/public/services/core/core.stub.ts deleted file mode 100644 index 052ddf46129e2..0000000000000 --- a/src/plugins/controls/public/services/core/core.stub.ts +++ /dev/null @@ -1,24 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { analyticsServiceMock, coreMock, themeServiceMock } from '@kbn/core/public/mocks'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsCoreService } from './types'; - -export type CoreServiceFactory = PluginServiceFactory; - -export const coreServiceFactory: CoreServiceFactory = () => { - const corePluginMock = coreMock.createStart(); - return { - analytics: analyticsServiceMock.createAnalyticsServiceStart(), - theme: themeServiceMock.createSetupContract(), - i18n: corePluginMock.i18n, - notifications: corePluginMock.notifications, - }; -}; diff --git a/src/plugins/controls/public/services/core/core_service.ts b/src/plugins/controls/public/services/core/core_service.ts deleted file mode 100644 index 090784c32d806..0000000000000 --- a/src/plugins/controls/public/services/core/core_service.ts +++ /dev/null @@ -1,28 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsCoreService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type CoreServiceFactory = KibanaPluginServiceFactory< - ControlsCoreService, - ControlsPluginStartDeps ->; - -export const coreServiceFactory: CoreServiceFactory = ({ coreStart }) => { - const { analytics, theme, i18n, notifications } = coreStart; - - return { - analytics, - theme, - i18n, - notifications, - }; -}; diff --git a/src/plugins/controls/public/services/core/types.ts b/src/plugins/controls/public/services/core/types.ts deleted file mode 100644 index 9424a490b3f3d..0000000000000 --- a/src/plugins/controls/public/services/core/types.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreStart } from '@kbn/core/public'; - -export interface ControlsCoreService { - analytics: CoreStart['analytics']; - i18n: CoreStart['i18n']; - theme: CoreStart['theme']; - notifications: CoreStart['notifications']; -} diff --git a/src/plugins/controls/public/services/data/data.stub.ts b/src/plugins/controls/public/services/data/data.stub.ts deleted file mode 100644 index 5cd50a68768ef..0000000000000 --- a/src/plugins/controls/public/services/data/data.stub.ts +++ /dev/null @@ -1,31 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { of } from 'rxjs'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { ControlsDataService } from './types'; - -export type DataServiceFactory = PluginServiceFactory; -export const dataServiceFactory: DataServiceFactory = () => ({ - query: {} as unknown as DataPublicPluginStart['query'], - searchSource: { - create: () => ({ - setField: () => {}, - fetch$: () => - of({ - rawResponse: { aggregations: { minAgg: { value: 0 }, maxAgg: { value: 1000 } } }, - }), - }), - } as unknown as DataPublicPluginStart['search']['searchSource'], - timefilter: { - createFilter: () => {}, - } as unknown as DataPublicPluginStart['query']['timefilter']['timefilter'], - fetchFieldRange: () => Promise.resolve({ min: 0, max: 100 }), -}); diff --git a/src/plugins/controls/public/services/data/data_service.ts b/src/plugins/controls/public/services/data/data_service.ts deleted file mode 100644 index 84a36c2775dcd..0000000000000 --- a/src/plugins/controls/public/services/data/data_service.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDataService } from './types'; - -export type DataServiceFactory = KibanaPluginServiceFactory< - ControlsDataService, - ControlsPluginStartDeps ->; - -export const dataServiceFactory: DataServiceFactory = ({ startPlugins }) => { - const { - data: { query: queryPlugin, search }, - } = startPlugins; - - return { - query: queryPlugin, - searchSource: search.searchSource, - timefilter: queryPlugin.timefilter.timefilter, - }; -}; diff --git a/src/plugins/controls/public/services/data/types.ts b/src/plugins/controls/public/services/data/types.ts deleted file mode 100644 index 68a7553ee08dc..0000000000000 --- a/src/plugins/controls/public/services/data/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -export interface ControlsDataService { - query: DataPublicPluginStart['query']; - searchSource: DataPublicPluginStart['search']['searchSource']; - timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; -} diff --git a/src/plugins/controls/public/services/data_views/data_views.stub.ts b/src/plugins/controls/public/services/data_views/data_views.stub.ts deleted file mode 100644 index f2ffcf5f08a9d..0000000000000 --- a/src/plugins/controls/public/services/data_views/data_views.stub.ts +++ /dev/null @@ -1,52 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/common'; -import { ControlsDataViewsService } from './types'; - -export type DataViewsServiceFactory = PluginServiceFactory; - -let currentDataView: DataView | undefined; -export const injectStorybookDataView = (dataView?: DataView) => (currentDataView = dataView); - -export const dataViewsServiceFactory: DataViewsServiceFactory = () => ({ - get: ((dataViewId) => - new Promise((resolve, reject) => - setTimeout(() => { - if (!currentDataView) { - reject( - new Error( - 'mock DataViews service currentDataView is undefined, call injectStorybookDataView to set' - ) - ); - } else if (currentDataView.id === dataViewId) { - resolve(currentDataView); - } else { - reject( - new Error( - `mock DataViews service currentDataView.id: ${currentDataView.id} does not match requested dataViewId: ${dataViewId}` - ) - ); - } - }, 100) - ) as unknown) as DataViewsPublicPluginStart['get'], - getIdsWithTitle: (() => - new Promise((resolve) => - setTimeout(() => { - const idsWithTitle: Array<{ id: string | undefined; title: string }> = []; - if (currentDataView) { - idsWithTitle.push({ id: currentDataView.id, title: currentDataView.title }); - } - resolve(idsWithTitle); - }, 100) - ) as unknown) as DataViewsPublicPluginStart['getIdsWithTitle'], - getDefaultId: () => Promise.resolve(currentDataView?.id ?? null), -}); diff --git a/src/plugins/controls/public/services/data_views/data_views_service.ts b/src/plugins/controls/public/services/data_views/data_views_service.ts deleted file mode 100644 index 4ad4c0b8d2241..0000000000000 --- a/src/plugins/controls/public/services/data_views/data_views_service.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDataViewsService } from './types'; - -export type DataViewsServiceFactory = KibanaPluginServiceFactory< - ControlsDataViewsService, - ControlsPluginStartDeps ->; - -export const dataViewsServiceFactory: DataViewsServiceFactory = ({ startPlugins }) => { - const { - dataViews: { get, getIdsWithTitle, getDefaultId }, - } = startPlugins; - - return { - get, - getDefaultId, - getIdsWithTitle, - }; -}; diff --git a/src/plugins/controls/public/services/data_views/types.ts b/src/plugins/controls/public/services/data_views/types.ts deleted file mode 100644 index a204af439634e..0000000000000 --- a/src/plugins/controls/public/services/data_views/types.ts +++ /dev/null @@ -1,16 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; - -export interface ControlsDataViewsService { - get: DataViewsPublicPluginStart['get']; - getDefaultId: DataViewsPublicPluginStart['getDefaultId']; - getIdsWithTitle: DataViewsPublicPluginStart['getIdsWithTitle']; -} diff --git a/src/plugins/controls/public/services/embeddable/embeddable.stub.ts b/src/plugins/controls/public/services/embeddable/embeddable.stub.ts deleted file mode 100644 index 5f75b4e7b2d14..0000000000000 --- a/src/plugins/controls/public/services/embeddable/embeddable.stub.ts +++ /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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; -import { ControlsEmbeddableService } from './types'; - -export type EmbeddableServiceFactory = PluginServiceFactory; -export const embeddableServiceFactory: EmbeddableServiceFactory = () => { - const { doStart } = embeddablePluginMock.createInstance(); - const start = doStart(); - return { getEmbeddableFactory: start.getEmbeddableFactory }; -}; diff --git a/src/plugins/controls/public/services/embeddable/embeddable_service.ts b/src/plugins/controls/public/services/embeddable/embeddable_service.ts deleted file mode 100644 index 79c556f69c058..0000000000000 --- a/src/plugins/controls/public/services/embeddable/embeddable_service.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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsEmbeddableService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type EmbeddableServiceFactory = KibanaPluginServiceFactory< - ControlsEmbeddableService, - ControlsPluginStartDeps ->; - -export const embeddableServiceFactory: EmbeddableServiceFactory = ({ startPlugins }) => { - return { - getEmbeddableFactory: startPlugins.embeddable.getEmbeddableFactory, - }; -}; diff --git a/src/plugins/controls/public/services/embeddable/types.ts b/src/plugins/controls/public/services/embeddable/types.ts deleted file mode 100644 index 917f03fb55e9b..0000000000000 --- a/src/plugins/controls/public/services/embeddable/types.ts +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EmbeddableStart } from '@kbn/embeddable-plugin/public'; - -export interface ControlsEmbeddableService { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; -} diff --git a/src/plugins/controls/public/services/http/http.stub.ts b/src/plugins/controls/public/services/http/http.stub.ts deleted file mode 100644 index ead893559b488..0000000000000 --- a/src/plugins/controls/public/services/http/http.stub.ts +++ /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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { HttpResponse } from '@kbn/core/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsHTTPService } from './types'; - -type HttpServiceFactory = PluginServiceFactory; - -export const httpServiceFactory: HttpServiceFactory = () => ({ - get: async () => ({} as unknown as HttpResponse), - fetch: async () => ({} as unknown as HttpResponse), -}); diff --git a/src/plugins/controls/public/services/http/http_service.ts b/src/plugins/controls/public/services/http/http_service.ts deleted file mode 100644 index fac813ea92272..0000000000000 --- a/src/plugins/controls/public/services/http/http_service.ts +++ /dev/null @@ -1,27 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsHTTPService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type HttpServiceFactory = KibanaPluginServiceFactory< - ControlsHTTPService, - ControlsPluginStartDeps ->; -export const httpServiceFactory: HttpServiceFactory = ({ coreStart }) => { - const { - http: { get, fetch }, - } = coreStart; - - return { - get, - fetch, - }; -}; diff --git a/src/plugins/controls/public/services/http/types.ts b/src/plugins/controls/public/services/http/types.ts deleted file mode 100644 index 0072bc0dacff0..0000000000000 --- a/src/plugins/controls/public/services/http/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { CoreSetup } from '@kbn/core/public'; - -export interface ControlsHTTPService { - get: CoreSetup['http']['get']; - fetch: CoreSetup['http']['fetch']; -} diff --git a/src/plugins/controls/public/services/index.ts b/src/plugins/controls/public/services/index.ts deleted file mode 100644 index a7cc0715e08d9..0000000000000 --- a/src/plugins/controls/public/services/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { pluginServices } from './plugin_services'; diff --git a/src/plugins/controls/public/services/kibana_services.ts b/src/plugins/controls/public/services/kibana_services.ts new file mode 100644 index 0000000000000..b9e5fadcecaa0 --- /dev/null +++ b/src/plugins/controls/public/services/kibana_services.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; + +import { CoreStart } from '@kbn/core/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +import { ControlsPluginStartDeps } from '../types'; + +export let coreServices: CoreStart; +export let dataService: DataPublicPluginStart; +export let dataViewsService: DataViewsPublicPluginStart; + +const servicesReady$ = new BehaviorSubject(false); + +export const setKibanaServices = (kibanaCore: CoreStart, deps: ControlsPluginStartDeps) => { + coreServices = kibanaCore; + dataService = deps.data; + dataViewsService = deps.dataViews; + + servicesReady$.next(true); +}; + +export const untilPluginStartServicesReady = () => { + if (servicesReady$.value) return Promise.resolve(); + return new Promise((resolve) => { + const subscription = servicesReady$.subscribe((isInitialized) => { + if (isInitialized) { + subscription.unsubscribe(); + resolve(); + } + }); + }); +}; diff --git a/src/plugins/controls/public/services/mocks.ts b/src/plugins/controls/public/services/mocks.ts new file mode 100644 index 0000000000000..231323c37a12a --- /dev/null +++ b/src/plugins/controls/public/services/mocks.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { coreMock } from '@kbn/core/public/mocks'; +import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; + +import { setKibanaServices } from './kibana_services'; + +export const setStubKibanaServices = () => { + setKibanaServices(coreMock.createStart(), { + data: dataPluginMock.createStartContract(), + dataViews: dataViewPluginMocks.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), + }); +}; diff --git a/src/plugins/controls/public/services/overlays/overlays.stub.ts b/src/plugins/controls/public/services/overlays/overlays.stub.ts deleted file mode 100644 index d7938f5a8a3ce..0000000000000 --- a/src/plugins/controls/public/services/overlays/overlays.stub.ts +++ /dev/null @@ -1,33 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - MountPoint, - OverlayFlyoutOpenOptions, - OverlayModalConfirmOptions, - OverlayRef, -} from '@kbn/core/public'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsOverlaysService } from './types'; - -type OverlaysServiceFactory = PluginServiceFactory; - -class StubRef implements OverlayRef { - public readonly onClose: Promise = Promise.resolve(); - - public close(): Promise { - return this.onClose; - } -} - -export const overlaysServiceFactory: OverlaysServiceFactory = () => ({ - openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => new StubRef(), - openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => - Promise.resolve(true), -}); diff --git a/src/plugins/controls/public/services/overlays/overlays_service.ts b/src/plugins/controls/public/services/overlays/overlays_service.ts deleted file mode 100644 index b8e7309e4eb31..0000000000000 --- a/src/plugins/controls/public/services/overlays/overlays_service.ts +++ /dev/null @@ -1,27 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsOverlaysService } from './types'; - -export type OverlaysServiceFactory = KibanaPluginServiceFactory< - ControlsOverlaysService, - ControlsPluginStartDeps ->; -export const overlaysServiceFactory: OverlaysServiceFactory = ({ coreStart }) => { - const { - overlays: { openFlyout, openConfirm }, - } = coreStart; - - return { - openFlyout, - openConfirm, - }; -}; diff --git a/src/plugins/controls/public/services/overlays/types.ts b/src/plugins/controls/public/services/overlays/types.ts deleted file mode 100644 index 7cb5fd2975549..0000000000000 --- a/src/plugins/controls/public/services/overlays/types.ts +++ /dev/null @@ -1,20 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - MountPoint, - OverlayFlyoutOpenOptions, - OverlayModalConfirmOptions, - OverlayRef, -} from '@kbn/core/public'; - -export interface ControlsOverlaysService { - openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; - openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise; -} diff --git a/src/plugins/controls/public/services/plugin_services.stub.ts b/src/plugins/controls/public/services/plugin_services.stub.ts deleted file mode 100644 index 1e7b3982f88cc..0000000000000 --- a/src/plugins/controls/public/services/plugin_services.stub.ts +++ /dev/null @@ -1,54 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - PluginServiceProvider, - PluginServiceProviders, - PluginServiceRegistry, - PluginServices, -} from '@kbn/presentation-util-plugin/public'; - -import { ControlsPluginStart } from '../types'; -import { ControlsServices } from './types'; - -import { controlsServiceFactory } from './controls/controls.stub'; -import { coreServiceFactory } from './core/core.stub'; -import { dataServiceFactory } from './data/data.stub'; -import { dataViewsServiceFactory } from './data_views/data_views.stub'; -import { embeddableServiceFactory } from './embeddable/embeddable.stub'; -import { httpServiceFactory } from './http/http.stub'; -import { overlaysServiceFactory } from './overlays/overlays.stub'; -import { settingsServiceFactory } from './settings/settings.stub'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search.stub'; -import { storageServiceFactory } from './storage/storage_service.stub'; - -export const providers: PluginServiceProviders = { - embeddable: new PluginServiceProvider(embeddableServiceFactory), - controls: new PluginServiceProvider(controlsServiceFactory), - data: new PluginServiceProvider(dataServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - http: new PluginServiceProvider(httpServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), - settings: new PluginServiceProvider(settingsServiceFactory), - core: new PluginServiceProvider(coreServiceFactory), - storage: new PluginServiceProvider(storageServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry(providers); - -export const getStubPluginServices = (): ControlsPluginStart => { - pluginServices.setRegistry(registry.start({})); - return { - getControlFactory: pluginServices.getServices().controls.getControlFactory, - getAllControlTypes: pluginServices.getServices().controls.getAllControlTypes, - }; -}; diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts deleted file mode 100644 index d0d2552871173..0000000000000 --- a/src/plugins/controls/public/services/plugin_services.ts +++ /dev/null @@ -1,52 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - KibanaPluginServiceParams, - PluginServiceProvider, - PluginServiceProviders, - PluginServiceRegistry, - PluginServices, -} from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../types'; -import { ControlsServices } from './types'; - -import { controlsServiceFactory } from './controls/controls_service'; -import { coreServiceFactory } from './core/core_service'; -import { dataServiceFactory } from './data/data_service'; -import { dataViewsServiceFactory } from './data_views/data_views_service'; -import { embeddableServiceFactory } from './embeddable/embeddable_service'; -import { httpServiceFactory } from './http/http_service'; -import { overlaysServiceFactory } from './overlays/overlays_service'; -import { settingsServiceFactory } from './settings/settings_service'; -import { controlsStorageServiceFactory } from './storage/storage_service'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search_service'; - -export const providers: PluginServiceProviders< - ControlsServices, - KibanaPluginServiceParams -> = { - controls: new PluginServiceProvider(controlsServiceFactory), - data: new PluginServiceProvider(dataServiceFactory), - dataViews: new PluginServiceProvider(dataViewsServiceFactory), - embeddable: new PluginServiceProvider(embeddableServiceFactory), - http: new PluginServiceProvider(httpServiceFactory), - overlays: new PluginServiceProvider(overlaysServiceFactory), - settings: new PluginServiceProvider(settingsServiceFactory), - storage: new PluginServiceProvider(controlsStorageServiceFactory), - core: new PluginServiceProvider(coreServiceFactory), - unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), -}; - -export const pluginServices = new PluginServices(); - -export const registry = new PluginServiceRegistry< - ControlsServices, - KibanaPluginServiceParams ->(providers); diff --git a/src/plugins/controls/public/services/settings/settings.stub.ts b/src/plugins/controls/public/services/settings/settings.stub.ts deleted file mode 100644 index 6ee5bdce4f2f4..0000000000000 --- a/src/plugins/controls/public/services/settings/settings.stub.ts +++ /dev/null @@ -1,18 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsSettingsService } from './types'; - -export type SettingsServiceFactory = PluginServiceFactory; -export const settingsServiceFactory: SettingsServiceFactory = () => ({ - getTimezone: () => 'Browser', - getDateFormat: () => 'MMM D, YYYY @ HH:mm:ss.SSS', - getDefaultTimeRange: () => ({ from: 'now-15m', to: 'now' }), -}); diff --git a/src/plugins/controls/public/services/settings/settings_service.ts b/src/plugins/controls/public/services/settings/settings_service.ts deleted file mode 100644 index e5f8751503a67..0000000000000 --- a/src/plugins/controls/public/services/settings/settings_service.ts +++ /dev/null @@ -1,32 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsSettingsService } from './types'; -import { ControlsPluginStartDeps } from '../../types'; - -export type SettingsServiceFactory = KibanaPluginServiceFactory< - ControlsSettingsService, - ControlsPluginStartDeps ->; - -export const settingsServiceFactory: SettingsServiceFactory = ({ coreStart }) => { - return { - getDateFormat: () => { - return coreStart.uiSettings.get('dateFormat', 'MMM D, YYYY @ HH:mm:ss.SSS'); - }, - getTimezone: () => { - return coreStart.uiSettings.get('dateFormat:tz', 'Browser'); - }, - getDefaultTimeRange: () => { - const defaultTimeRange = coreStart.uiSettings.get('timepicker:timeDefaults'); - return defaultTimeRange ? defaultTimeRange : { from: 'now-15m', to: 'now' }; - }, - }; -}; diff --git a/src/plugins/controls/public/services/settings/types.ts b/src/plugins/controls/public/services/settings/types.ts deleted file mode 100644 index f8b29adeb85a7..0000000000000 --- a/src/plugins/controls/public/services/settings/types.ts +++ /dev/null @@ -1,16 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { TimeRange } from '@kbn/es-query'; - -export interface ControlsSettingsService { - getTimezone: () => string; - getDateFormat: () => string; - getDefaultTimeRange: () => TimeRange; -} diff --git a/src/plugins/controls/public/services/storage/storage_service.stub.ts b/src/plugins/controls/public/services/storage/storage_service.stub.ts deleted file mode 100644 index 389d804b51bb0..0000000000000 --- a/src/plugins/controls/public/services/storage/storage_service.stub.ts +++ /dev/null @@ -1,20 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsStorageService } from './types'; - -type StorageServiceFactory = PluginServiceFactory; - -export const storageServiceFactory: StorageServiceFactory = () => { - return { - getShowInvalidSelectionWarning: () => false, - setShowInvalidSelectionWarning: (value: boolean) => null, - }; -}; diff --git a/src/plugins/controls/public/services/storage/storage_service.ts b/src/plugins/controls/public/services/storage/storage_service.ts deleted file mode 100644 index 9f0403c505e74..0000000000000 --- a/src/plugins/controls/public/services/storage/storage_service.ts +++ /dev/null @@ -1,31 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { ControlsStorageService } from './types'; - -const STORAGE_KEY = 'controls:showInvalidSelectionWarning'; - -class StorageService implements ControlsStorageService { - private storage: Storage; - - constructor() { - this.storage = new Storage(localStorage); - } - - getShowInvalidSelectionWarning = () => { - return this.storage.get(STORAGE_KEY); - }; - - setShowInvalidSelectionWarning = (value: boolean) => { - this.storage.set(STORAGE_KEY, value); - }; -} - -export const controlsStorageServiceFactory = () => new StorageService(); diff --git a/src/plugins/controls/public/services/storage/types.ts b/src/plugins/controls/public/services/storage/types.ts deleted file mode 100644 index 2d1e8b08e8364..0000000000000 --- a/src/plugins/controls/public/services/storage/types.ts +++ /dev/null @@ -1,13 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export interface ControlsStorageService { - getShowInvalidSelectionWarning: () => boolean; - setShowInvalidSelectionWarning: (value: boolean) => void; -} diff --git a/src/plugins/controls/public/services/types.ts b/src/plugins/controls/public/services/types.ts deleted file mode 100644 index c38ad6b64fac6..0000000000000 --- a/src/plugins/controls/public/services/types.ts +++ /dev/null @@ -1,35 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ControlsServiceType } from './controls/types'; -import { ControlsCoreService } from './core/types'; -import { ControlsDataService } from './data/types'; -import { ControlsDataViewsService } from './data_views/types'; -import { ControlsEmbeddableService } from './embeddable/types'; -import { ControlsHTTPService } from './http/types'; -import { ControlsOverlaysService } from './overlays/types'; -import { ControlsSettingsService } from './settings/types'; -import { ControlsStorageService } from './storage/types'; -import { ControlsUnifiedSearchService } from './unified_search/types'; - -export interface ControlsServices { - // dependency services - dataViews: ControlsDataViewsService; - overlays: ControlsOverlaysService; - embeddable: ControlsEmbeddableService; - data: ControlsDataService; - unifiedSearch: ControlsUnifiedSearchService; - http: ControlsHTTPService; - settings: ControlsSettingsService; - core: ControlsCoreService; - - // controls plugin's own services - controls: ControlsServiceType; - storage: ControlsStorageService; -} diff --git a/src/plugins/controls/public/services/unified_search/types.ts b/src/plugins/controls/public/services/unified_search/types.ts deleted file mode 100644 index 28aa8e05feb2d..0000000000000 --- a/src/plugins/controls/public/services/unified_search/types.ts +++ /dev/null @@ -1,14 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -export interface ControlsUnifiedSearchService { - autocomplete: UnifiedSearchPublicPluginStart['autocomplete']; -} diff --git a/src/plugins/controls/public/services/unified_search/unified_search.stub.ts b/src/plugins/controls/public/services/unified_search/unified_search.stub.ts deleted file mode 100644 index c7b1959c8bbc0..0000000000000 --- a/src/plugins/controls/public/services/unified_search/unified_search.stub.ts +++ /dev/null @@ -1,27 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { DataViewField } from '@kbn/data-views-plugin/common'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -import { ControlsUnifiedSearchService } from './types'; - -let valueSuggestionMethod = ({ field, query }: { field: DataViewField; query: string }) => - Promise.resolve(['storybook', 'default', 'values']); -export const replaceValueSuggestionMethod = ( - newMethod: ({ field, query }: { field: DataViewField; query: string }) => Promise -) => (valueSuggestionMethod = newMethod); - -export type UnifiedSearchServiceFactory = PluginServiceFactory; -export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = () => ({ - autocomplete: { - getValueSuggestions: valueSuggestionMethod, - } as unknown as UnifiedSearchPublicPluginStart['autocomplete'], -}); diff --git a/src/plugins/controls/public/services/unified_search/unified_search_service.ts b/src/plugins/controls/public/services/unified_search/unified_search_service.ts deleted file mode 100644 index 45d10cfa9ac7d..0000000000000 --- a/src/plugins/controls/public/services/unified_search/unified_search_service.ts +++ /dev/null @@ -1,27 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsUnifiedSearchService } from './types'; - -export type UnifiedSearchServiceFactory = KibanaPluginServiceFactory< - ControlsUnifiedSearchService, - ControlsPluginStartDeps ->; - -export const unifiedSearchServiceFactory: UnifiedSearchServiceFactory = ({ startPlugins }) => { - const { - unifiedSearch: { autocomplete }, - } = startPlugins; - - return { - autocomplete, - }; -}; diff --git a/src/plugins/controls/public/types.ts b/src/plugins/controls/public/types.ts index 2ecbd38763603..bed3260bb4401 100644 --- a/src/plugins/controls/public/types.ts +++ b/src/plugins/controls/public/types.ts @@ -9,11 +9,8 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; -import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; +import { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; -import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - -import { ControlsServiceType } from './services/controls/types'; export interface CanClearSelections { clearSelections: () => void; @@ -26,22 +23,11 @@ export const isClearableControl = (control: unknown): control is CanClearSelecti /** * Plugin types */ -export interface ControlsPluginSetup { - registerControlFactory: ControlsServiceType['registerControlFactory']; -} - -export interface ControlsPluginStart { - getControlFactory: ControlsServiceType['getControlFactory']; - getAllControlTypes: ControlsServiceType['getAllControlTypes']; -} - export interface ControlsPluginSetupDeps { embeddable: EmbeddableSetup; } export interface ControlsPluginStartDeps { uiActions: UiActionsStart; - embeddable: EmbeddableStart; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; - unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/src/plugins/controls/server/control_group/control_group_persistence.ts b/src/plugins/controls/server/control_group/control_group_persistence.ts index eb63b28ccecba..e90aa850c6d1a 100644 --- a/src/plugins/controls/server/control_group/control_group_persistence.ts +++ b/src/plugins/controls/server/control_group/control_group_persistence.ts @@ -10,7 +10,7 @@ import { SerializableRecord } from '@kbn/utility-types'; import { - DEFAULT_CONTROL_STYLE, + DEFAULT_CONTROL_LABEL_POSITION, type ControlGroupRuntimeState, type ControlGroupSerializedState, type ControlPanelState, @@ -19,7 +19,7 @@ import { export const getDefaultControlGroupState = (): SerializableControlGroupState => ({ panels: {}, - labelPosition: DEFAULT_CONTROL_STYLE, + labelPosition: DEFAULT_CONTROL_LABEL_POSITION, chainingSystem: 'HIERARCHICAL', autoApplySelections: true, ignoreParentSettings: { diff --git a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts index f51b9a5b5b62c..04b0aaa3e6f78 100644 --- a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts +++ b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts @@ -15,7 +15,7 @@ export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { router.versioned .get({ access: 'internal', - path: '/internal/controls/optionsList/getExpensiveQueriesSetting', + path: '/internal/controls/getExpensiveQueriesSetting', }) .addVersion( { diff --git a/src/plugins/controls/tsconfig.json b/src/plugins/controls/tsconfig.json index fc9d6572ccf2f..abcafa291358e 100644 --- a/src/plugins/controls/tsconfig.json +++ b/src/plugins/controls/tsconfig.json @@ -35,7 +35,6 @@ "@kbn/presentation-containers", "@kbn/presentation-publishing", "@kbn/content-management-utils", - "@kbn/core-lifecycle-browser", "@kbn/field-formats-plugin", "@kbn/presentation-panel-plugin", "@kbn/shared-ux-utility" From 88163b063a79520e1dafc7d9548f040a1323522b Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 19 Sep 2024 16:07:44 +0200 Subject: [PATCH 12/24] [ES|QL] Fixes Incomplete string escaping or encoding error (#193384) ## Summary Closes https://github.com/elastic/kibana-team/issues/1087 I think that this change will close the second problem. Not 100% sure but this is how I interpret it --- packages/kbn-esql-utils/src/utils/append_to_query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-esql-utils/src/utils/append_to_query.ts b/packages/kbn-esql-utils/src/utils/append_to_query.ts index 76f317d55aa5d..f4161be073a8d 100644 --- a/packages/kbn-esql-utils/src/utils/append_to_query.ts +++ b/packages/kbn-esql-utils/src/utils/append_to_query.ts @@ -36,7 +36,7 @@ export function appendWhereClauseToESQLQuery( default: operator = '=='; } - let filterValue = typeof value === 'string' ? `"${value.replace(/"/g, '\\"')}"` : value; + let filterValue = typeof value === 'string' ? `"${value.replace(/\"/g, '\\"')}"` : value; // Adding the backticks here are they are needed for special char fields let fieldName = `\`${field}\``; From b4a7b2e2164974e5dd9b8c1708c35d58bd021105 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:57:47 +0200 Subject: [PATCH 13/24] Consume elasticsearch.publicBaseUrl where possible (#192741) ## Summary This actually consumes the public base url in the cloud plugin and the places depending on the `elasticsearchUrl` value populated there. --------- Co-authored-by: Rodney Norris --- .../kibana_connection_details_provider.tsx | 3 +- .../cloud/deployment_details/services.tsx | 15 +++++--- .../src/elasticsearch_config.test.ts | 1 + .../src/elasticsearch_config.ts | 8 +++++ .../src/elasticsearch_service.ts | 2 ++ .../src/contracts.ts | 6 ++++ .../src/plugin_context.ts | 1 + x-pack/plugins/cloud/common/constants.ts | 2 ++ x-pack/plugins/cloud/common/types.ts | 4 +++ x-pack/plugins/cloud/public/mocks.tsx | 5 ++- x-pack/plugins/cloud/public/plugin.test.ts | 17 +++++++-- x-pack/plugins/cloud/public/plugin.tsx | 33 ++++++++++++++--- x-pack/plugins/cloud/public/types.ts | 17 ++++++--- x-pack/plugins/cloud/server/plugin.ts | 6 +++- .../server/routes/elasticsearch_routes.ts | 35 +++++++++++++++++++ .../analytics_collection_integrate.test.tsx | 8 ++++- .../analytics_collection_integrate_view.tsx | 4 +++ .../shared/cloud_details/cloud_details.ts | 12 ++++++- .../get_cloud_enterprise_search_host.test.ts | 14 +++++--- .../shared/kibana/kibana_logic.ts | 11 +++--- .../test_helpers/test_utils.test_helper.tsx | 1 + .../plugins/fleet/.storybook/context/cloud.ts | 2 ++ .../details_page_overview.tsx | 12 ++++--- .../public/hooks/use_elasticsearch_url.ts | 11 +++++- .../examples/py_lang_client.test.tsx | 5 +-- .../examples/py_langchain_python.test.tsx | 5 +-- .../components/view_code/view_code_flyout.tsx | 24 ++++++------- .../index_management/api_empty_prompt.tsx | 13 +++---- .../application/components/overview.test.tsx | 2 +- .../application/components/overview.tsx | 19 +++++----- .../application/hooks/use_elastisearch_url.ts | 9 ++++- 31 files changed, 234 insertions(+), 73 deletions(-) create mode 100644 x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts diff --git a/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx b/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx index 444ff70ff9128..d400f67993f8a 100644 --- a/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx +++ b/packages/cloud/connection_details/kibana/kibana_connection_details_provider.tsx @@ -21,6 +21,7 @@ const createOpts = async (props: KibanaConnectionDetailsProviderProps) => { const { http, docLinks, analytics } = start.core; const locator = start.plugins?.share?.url?.locators.get('MANAGEMENT_APP_LOCATOR'); const manageKeysLink = await locator?.getUrl({ sectionId: 'security', appId: 'api_keys' }); + const elasticsearchConfig = await start.plugins?.cloud?.fetchElasticsearchConfig(); const result: ConnectionDetailsOpts = { ...options, navigateToUrl: start.core.application @@ -35,7 +36,7 @@ const createOpts = async (props: KibanaConnectionDetailsProviderProps) => { }, endpoints: { id: start.plugins?.cloud?.cloudId, - url: start.plugins?.cloud?.elasticsearchUrl, + url: elasticsearchConfig?.elasticsearchUrl, cloudIdLearMoreLink: docLinks?.links?.cloud?.beatsAndLogstashConfiguration, ...options?.endpoints, }, diff --git a/packages/cloud/deployment_details/services.tsx b/packages/cloud/deployment_details/services.tsx index d3ca0a340b600..73959627e98e6 100644 --- a/packages/cloud/deployment_details/services.tsx +++ b/packages/cloud/deployment_details/services.tsx @@ -6,8 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -import React, { FC, PropsWithChildren, useContext } from 'react'; +import React, { FC, PropsWithChildren, useContext, useEffect } from 'react'; export interface DeploymentDetailsContextValue { cloudId?: string; @@ -58,7 +57,7 @@ export interface DeploymentDetailsKibanaDependencies { cloud: { isCloudEnabled: boolean; cloudId?: string; - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise<{ elasticsearchUrl?: string }>; }; /** DocLinksStart contract */ docLinks: { @@ -79,11 +78,19 @@ export interface DeploymentDetailsKibanaDependencies { export const DeploymentDetailsKibanaProvider: FC< PropsWithChildren > = ({ children, ...services }) => { + const [elasticsearchUrl, setElasticsearchUrl] = React.useState(''); + + useEffect(() => { + services.cloud.fetchElasticsearchConfig().then((config) => { + setElasticsearchUrl(config.elasticsearchUrl || ''); + }); + }, [services.cloud]); + const { core: { application: { navigateToUrl }, }, - cloud: { isCloudEnabled, cloudId, elasticsearchUrl }, + cloud: { isCloudEnabled, cloudId }, share: { url: { locators }, }, diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts index 34a8bc07e0b5c..b7d7b40c49806 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.test.ts @@ -47,6 +47,7 @@ test('set correct defaults', () => { "maxSockets": 800, "password": undefined, "pingTimeout": "PT30S", + "publicBaseUrl": undefined, "requestHeadersWhitelist": Array [ "authorization", "es-client-authentication", diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts index 46b7a02768e7a..93fb64baf46d0 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_config.ts @@ -359,6 +359,13 @@ export class ElasticsearchConfig implements IElasticsearchConfig { */ public readonly hosts: string[]; + /** + * Optional host that users can use to connect to your Elasticsearch cluster, + * this URL will be shown in Kibana as the Elasticsearch URL + */ + + public readonly publicBaseUrl?: string; + /** * List of Kibana client-side headers to send to Elasticsearch when request * scoped cluster client is used. If this is an empty array then *no* client-side @@ -473,6 +480,7 @@ export class ElasticsearchConfig implements IElasticsearchConfig { this.skipStartupConnectionCheck = rawConfig.skipStartupConnectionCheck; this.apisToRedactInLogs = rawConfig.apisToRedactInLogs; this.dnsCacheTtl = rawConfig.dnsCacheTtl; + this.publicBaseUrl = rawConfig.publicBaseUrl; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts index 5a3f34d0565f8..83eb04832121e 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_service.ts @@ -135,6 +135,7 @@ export class ElasticsearchService agentStatsProvider: { getAgentsStats: agentManager.getAgentsStats.bind(agentManager), }, + publicBaseUrl: config.publicBaseUrl, }; } @@ -194,6 +195,7 @@ export class ElasticsearchService metrics: { elasticsearchWaitTime, }, + publicBaseUrl: config.publicBaseUrl, }; } diff --git a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts index f0a3a62d08f18..bc712a61a535e 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server/src/contracts.ts @@ -138,6 +138,12 @@ export interface ElasticsearchServiceStart { * Returns the capabilities for the default cluster. */ getCapabilities: () => ElasticsearchCapabilities; + + /** + * The public base URL (if any) that should be used by end users to access the Elasticsearch cluster. + */ + + readonly publicBaseUrl?: string; } /** diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 539b629974982..2fcdf384cb897 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -212,6 +212,7 @@ export function createPluginSetupContext({ docLinks: deps.docLinks, elasticsearch: { legacy: deps.elasticsearch.legacy, + publicBaseUrl: deps.elasticsearch.publicBaseUrl, setUnauthorizedErrorHandler: deps.elasticsearch.setUnauthorizedErrorHandler, }, executionContext: { diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts index f2009223f8ac1..27d755ab08214 100644 --- a/x-pack/plugins/cloud/common/constants.ts +++ b/x-pack/plugins/cloud/common/constants.ts @@ -11,3 +11,5 @@ export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; * This is the page for managing your snapshots on Cloud. */ export const CLOUD_SNAPSHOTS_PATH = 'elasticsearch/snapshots/'; + +export const ELASTICSEARCH_CONFIG_ROUTE = '/api/internal/cloud/elasticsearch_config'; diff --git a/x-pack/plugins/cloud/common/types.ts b/x-pack/plugins/cloud/common/types.ts index ac4593fd0e259..0f72caf515058 100644 --- a/x-pack/plugins/cloud/common/types.ts +++ b/x-pack/plugins/cloud/common/types.ts @@ -6,3 +6,7 @@ */ export type OnBoardingDefaultSolution = 'es' | 'oblt' | 'security'; + +export interface ElasticsearchConfigType { + elasticsearch_url?: string; +} diff --git a/x-pack/plugins/cloud/public/mocks.tsx b/x-pack/plugins/cloud/public/mocks.tsx index dd5c5eced618a..b9f6d850b9acf 100644 --- a/x-pack/plugins/cloud/public/mocks.tsx +++ b/x-pack/plugins/cloud/public/mocks.tsx @@ -19,7 +19,9 @@ function createSetupMock(): jest.Mocked { deploymentUrl: 'deployment-url', profileUrl: 'profile-url', organizationUrl: 'organization-url', - elasticsearchUrl: 'elasticsearch-url', + fetchElasticsearchConfig: jest + .fn() + .mockResolvedValue({ elasticsearchUrl: 'elasticsearch-url' }), kibanaUrl: 'kibana-url', cloudHost: 'cloud-host', cloudDefaultPort: '443', @@ -53,6 +55,7 @@ const createStartMock = (): jest.Mocked => ({ serverless: { projectId: undefined, }, + fetchElasticsearchConfig: jest.fn().mockResolvedValue({ elasticsearchUrl: 'elasticsearch-url' }), }); export const cloudMock = { diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts index 2c32ac8fe972a..583d274db77d8 100644 --- a/x-pack/plugins/cloud/public/plugin.test.ts +++ b/x-pack/plugins/cloud/public/plugin.test.ts @@ -37,6 +37,7 @@ describe('Cloud Plugin', () => { const plugin = new CloudPlugin(initContext); const coreSetup = coreMock.createSetup(); + coreSetup.http.get.mockResolvedValue({ elasticsearch_url: 'elasticsearch-url' }); const setup = plugin.setup(coreSetup); return { setup }; @@ -110,8 +111,8 @@ describe('Cloud Plugin', () => { it('exposes components decoded from the cloudId', () => { const decodedId: DecodedCloudId = { defaultPort: '9000', - host: 'host', elasticsearchUrl: 'elasticsearch-url', + host: 'host', kibanaUrl: 'kibana-url', }; decodeCloudIdMock.mockReturnValue(decodedId); @@ -120,7 +121,6 @@ describe('Cloud Plugin', () => { expect.objectContaining({ cloudDefaultPort: '9000', cloudHost: 'host', - elasticsearchUrl: 'elasticsearch-url', kibanaUrl: 'kibana-url', }) ); @@ -184,6 +184,11 @@ describe('Cloud Plugin', () => { }); expect(setup.serverless.projectType).toBe('security'); }); + it('exposes fetchElasticsearchConfig', async () => { + const { setup } = setupPlugin(); + const result = await setup.fetchElasticsearchConfig(); + expect(result).toEqual({ elasticsearchUrl: 'elasticsearch-url' }); + }); }); }); @@ -307,5 +312,13 @@ describe('Cloud Plugin', () => { const start = plugin.start(coreStart); expect(start.serverless.projectName).toBe('My Awesome Project'); }); + it('exposes fetchElasticsearchConfig', async () => { + const { plugin } = startPlugin(); + const coreStart = coreMock.createStart(); + coreStart.http.get.mockResolvedValue({ elasticsearch_url: 'elasticsearch-url' }); + const start = plugin.start(coreStart); + const result = await start.fetchElasticsearchConfig(); + expect(result).toEqual({ elasticsearchUrl: 'elasticsearch-url' }); + }); }); }); diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx index a661933955060..e89e63dc1c15b 100644 --- a/x-pack/plugins/cloud/public/plugin.tsx +++ b/x-pack/plugins/cloud/public/plugin.tsx @@ -12,12 +12,13 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kb import { registerCloudDeploymentMetadataAnalyticsContext } from '../common/register_cloud_deployment_id_analytics_context'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { parseDeploymentIdFromDeploymentUrl } from '../common/parse_deployment_id_from_deployment_url'; -import { CLOUD_SNAPSHOTS_PATH } from '../common/constants'; +import { CLOUD_SNAPSHOTS_PATH, ELASTICSEARCH_CONFIG_ROUTE } from '../common/constants'; import { decodeCloudId, type DecodedCloudId } from '../common/decode_cloud_id'; import { getFullCloudUrl } from '../common/utils'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; -import type { CloudSetup, CloudStart } from './types'; +import type { CloudSetup, CloudStart, PublicElasticsearchConfigType } from './types'; import { getSupportUrl } from './utils'; +import { ElasticsearchConfigType } from '../common/types'; export interface CloudConfigType { id?: string; @@ -66,12 +67,14 @@ export class CloudPlugin implements Plugin { private readonly isServerlessEnabled: boolean; private readonly contextProviders: Array>> = []; private readonly logger: Logger; + private elasticsearchConfig?: PublicElasticsearchConfigType; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); this.isCloudEnabled = getIsCloudEnabled(this.config.id); this.isServerlessEnabled = !!this.config.serverless?.project_id; this.logger = initializerContext.logger.get(); + this.elasticsearchConfig = undefined; } public setup(core: CoreSetup): CloudSetup { @@ -99,7 +102,6 @@ export class CloudPlugin implements Plugin { csp, baseUrl, ...this.getCloudUrls(), - elasticsearchUrl: decodedId?.elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, cloudHost: decodedId?.host, cloudDefaultPort: decodedId?.defaultPort, @@ -119,6 +121,7 @@ export class CloudPlugin implements Plugin { registerCloudService: (contextProvider) => { this.contextProviders.push(contextProvider); }, + fetchElasticsearchConfig: this.fetchElasticsearchConfig.bind(this, core.http), }; } @@ -166,7 +169,6 @@ export class CloudPlugin implements Plugin { profileUrl, organizationUrl, projectsUrl, - elasticsearchUrl: decodedId?.elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, isServerlessEnabled: this.isServerlessEnabled, serverless: { @@ -176,6 +178,7 @@ export class CloudPlugin implements Plugin { }, performanceUrl, usersAndRolesUrl, + fetchElasticsearchConfig: this.fetchElasticsearchConfig.bind(this, coreStart.http), }; } @@ -216,4 +219,26 @@ export class CloudPlugin implements Plugin { projectsUrl: fullCloudProjectsUrl, }; } + + private async fetchElasticsearchConfig( + http: CoreStart['http'] + ): Promise { + if (this.elasticsearchConfig !== undefined) { + // This config should be fully populated on first fetch, so we should avoid refetching from server + return this.elasticsearchConfig; + } + try { + const result = await http.get(ELASTICSEARCH_CONFIG_ROUTE, { + version: '1', + }); + + this.elasticsearchConfig = { elasticsearchUrl: result.elasticsearch_url || undefined }; + return this.elasticsearchConfig; + } catch { + this.logger.error('Failed to fetch Elasticsearch config'); + return { + elasticsearchUrl: undefined, + }; + } + } } diff --git a/x-pack/plugins/cloud/public/types.ts b/x-pack/plugins/cloud/public/types.ts index 8df1ba645cb48..1428e887f1b9f 100644 --- a/x-pack/plugins/cloud/public/types.ts +++ b/x-pack/plugins/cloud/public/types.ts @@ -58,9 +58,9 @@ export interface CloudStart { */ projectsUrl?: string; /** - * The full URL to the elasticsearch cluster. + * Fetches the full URL to the elasticsearch cluster. */ - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise; /** * The full URL to the Kibana deployment. */ @@ -150,9 +150,9 @@ export interface CloudSetup { */ snapshotsUrl?: string; /** - * The full URL to the elasticsearch cluster. + * Fetches the full URL to the elasticsearch cluster. */ - elasticsearchUrl?: string; + fetchElasticsearchConfig: () => Promise; /** * The full URL to the Kibana deployment. */ @@ -225,3 +225,12 @@ export interface CloudSetup { orchestratorTarget?: string; }; } + +export interface PublicElasticsearchConfigType { + /** + * The URL to the Elasticsearch cluster, derived from xpack.elasticsearch.publicBaseUrl if populated + * Otherwise this is based on the cloudId + * If neither is populated, this will be undefined + */ + elasticsearchUrl?: string; +} diff --git a/x-pack/plugins/cloud/server/plugin.ts b/x-pack/plugins/cloud/server/plugin.ts index a03878b760dd4..9f45b5398ac22 100644 --- a/x-pack/plugins/cloud/server/plugin.ts +++ b/x-pack/plugins/cloud/server/plugin.ts @@ -18,6 +18,7 @@ import { decodeCloudId, DecodedCloudId } from '../common/decode_cloud_id'; import { parseOnboardingSolution } from '../common/parse_onboarding_default_solution'; import { getFullCloudUrl } from '../common/utils'; import { readInstanceSizeMb } from './env'; +import { defineRoutes } from './routes/elasticsearch_routes'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; @@ -201,6 +202,9 @@ export class CloudPlugin implements Plugin { if (this.config.id) { decodedId = decodeCloudId(this.config.id, this.logger); } + const router = core.http.createRouter(); + const elasticsearchUrl = core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl; + defineRoutes({ logger: this.logger, router, elasticsearchUrl }); return { ...this.getCloudUrls(), @@ -209,7 +213,7 @@ export class CloudPlugin implements Plugin { organizationId, instanceSizeMb: readInstanceSizeMb(), deploymentId, - elasticsearchUrl: core.elasticsearch.publicBaseUrl || decodedId?.elasticsearchUrl, + elasticsearchUrl, kibanaUrl: decodedId?.kibanaUrl, cloudHost: decodedId?.host, cloudDefaultPort: decodedId?.defaultPort, diff --git a/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts b/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts new file mode 100644 index 0000000000000..5cdc2f90559cc --- /dev/null +++ b/x-pack/plugins/cloud/server/routes/elasticsearch_routes.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '@kbn/core/server'; +import { Logger } from '@kbn/logging'; +import { ElasticsearchConfigType } from '../../common/types'; +import { ELASTICSEARCH_CONFIG_ROUTE } from '../../common/constants'; + +export function defineRoutes({ + elasticsearchUrl, + logger, + router, +}: { + elasticsearchUrl?: string; + logger: Logger; + router: IRouter; +}) { + router.versioned + .get({ + path: ELASTICSEARCH_CONFIG_ROUTE, + access: 'internal', + }) + .addVersion({ version: '1', validate: {} }, async (context, request, response) => { + const body: ElasticsearchConfigType = { + elasticsearch_url: elasticsearchUrl, + }; + return response.ok({ + body, + }); + }); +} diff --git a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx index 9bb23b677f743..f55034f72ccd6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/analytics/components/analytics_collection_view/analytics_collection_integrate/analytics_collection_integrate.test.tsx @@ -22,6 +22,12 @@ jest.mock('../../../../shared/enterprise_search_url', () => ({ getEnterpriseSearchUrl: () => 'http://localhost:3002', })); +jest.mock('../../../../shared/cloud_details/cloud_details', () => ({ + useCloudDetails: () => ({ + elasticsearchUrl: 'your_deployment_url', + }), +})); + describe('AnalyticsCollectionIntegrate', () => { const analyticsCollections: AnalyticsCollection = { events_datastream: 'analytics-events-example', @@ -55,7 +61,7 @@ describe('AnalyticsCollectionIntegrate', () => { .toMatchInlineSnapshot(` "