From 61ad27e8632b1fa60256ffb6f0878fda96665cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Zaffarano?= Date: Fri, 26 Jul 2024 11:02:56 +0200 Subject: [PATCH 01/12] [Telemetry][Security Solution] Always enrich telemetry documents with license info (#188832) --- .../server/lib/telemetry/async_sender.ts | 25 +++++-------------- ...remove_time_fields_from_telemetry_stats.ts | 2 ++ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts index 6c2def2abb61d..edbd5d21e3aea 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/async_sender.ts @@ -276,28 +276,15 @@ export class AsyncTelemetryEventsSender implements IAsyncTelemetryEventsSender { private enrich(event: Event): Event { const clusterInfo = this.telemetryReceiver?.getClusterInfo(); - // TODO(szaffarano): generalize the enrichment at channel level to not hardcode the logic here if (typeof event.payload === 'object') { let additional = {}; - if (event.channel !== TelemetryChannel.TASK_METRICS) { - additional = { - cluster_name: clusterInfo?.cluster_name, - cluster_uuid: clusterInfo?.cluster_uuid, - }; - } else { - additional = { - cluster_uuid: clusterInfo?.cluster_uuid, - }; - } - - if (event.channel === TelemetryChannel.ENDPOINT_ALERTS) { - const licenseInfo = this.telemetryReceiver?.getLicenseInfo(); - additional = { - ...additional, - ...(licenseInfo ? { license: copyLicenseFields(licenseInfo) } : {}), - }; - } + const licenseInfo = this.telemetryReceiver?.getLicenseInfo(); + additional = { + cluster_name: clusterInfo?.cluster_name, + cluster_uuid: clusterInfo?.cluster_uuid, + ...(licenseInfo ? { license: copyLicenseFields(licenseInfo) } : {}), + }; event.payload = { ...event.payload, diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts index 912649862a24f..2d27b375684ba 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/telemetry/remove_time_fields_from_telemetry_stats.ts @@ -15,6 +15,8 @@ export const removeExtraFieldsFromTelemetryStats = (stats: any) => { unset(value, `[${i}][${j}].start_time`); unset(value, `[${i}][${j}].end_time`); unset(value, `[${i}][${j}].cluster_uuid`); + unset(value, `[${i}][${j}].cluster_name`); + unset(value, `[${i}][${j}].license`); }); }); }); From 10bfb4b4ae4e03f31f4e244f3286d67bebc0b5f5 Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Fri, 26 Jul 2024 11:35:40 +0200 Subject: [PATCH 02/12] [eem] narrow down index patterns in definition templates (#189182) In https://github.com/elastic/kibana/pull/188410 we moved history and latest index templates from global scope to definition scope. The definition-scoped templates have a wide pattern that would grep any other definition template already installed and throw the following error because of conflicting priority. This change narrows down the index patterns defined in the templates to only grep the ones from the installed definition ``` { "statusCode": 500, "error": "Internal Server Error", "message": """[illegal_argument_exception Root causes: illegal_argument_exception: index template [entities_v1_history_admin-console-services_index_template] has index patterns [.entities.v1.history.*] matching patterns from existing templates [entities_v1_history_builtin_services_from_ecs_data_index_template] with patterns (entities_v1_history_builtin_services_from_ecs_data_index_template => [.entities.v1.history.*]) that have the same priority [200], multiple index templates may not match during index creation, please use a different priority]: index template [entities_v1_history_admin-console-services_index_template] has index patterns [.entities.v1.history.*] matching patterns from existing templates [entities_v1_history_builtin_services_from_ecs_data_index_template] with patterns (entities_v1_history_builtin_services_from_ecs_data_index_template => [.entities.v1.history.*]) that have the same priority [200], multiple index templates may not match during index creation, please use a different priority""" } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/entities/delete_ingest_pipeline.ts | 4 +- .../entity_definition_with_backfill.ts | 2 +- .../lib/entities/helpers/fixtures/index.ts | 9 +++ .../lib/entities/install_entity_definition.ts | 6 +- .../lib/entities/read_entity_definition.ts | 2 +- .../lib/entities/stop_and_delete_transform.ts | 4 +- .../entities_history_template.test.ts.snap | 78 +++++++++++++++++++ .../entities_latest_template.test.ts.snap | 78 +++++++++++++++++++ .../entities_history_template.test.ts | 16 ++++ .../templates/entities_history_template.ts | 8 +- .../entities_latest_template.test.ts | 16 ++++ .../templates/entities_latest_template.ts | 10 +-- .../generate_history_transform.test.ts.snap | 4 +- .../apis/entity_manager/definitions.ts | 42 ++++++++++ .../apis/entity_manager/enablement.ts | 26 ++----- .../apis/entity_manager/helpers/request.ts | 39 ++++++++++ .../apis/entity_manager/index.ts | 1 + x-pack/test/tsconfig.json | 3 +- 18 files changed, 307 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts rename x-pack/plugins/observability_solution/entity_manager/server/{ => lib/entities}/templates/entities_history_template.ts (87%) create mode 100644 x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts rename x-pack/plugins/observability_solution/entity_manager/server/{ => lib/entities}/templates/entities_latest_template.ts (87%) create mode 100644 x-pack/test/api_integration/apis/entity_manager/definitions.ts create mode 100644 x-pack/test/api_integration/apis/entity_manager/helpers/request.ts diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts index c1516e342ac1d..f4c46d8447d8f 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/delete_ingest_pipeline.ts @@ -24,7 +24,7 @@ export async function deleteHistoryIngestPipeline( esClient.ingest.deletePipeline({ id: historyPipelineId }, { ignore: [404] }) ); } catch (e) { - logger.error(`Unable to delete history ingest pipeline [${definition.id}].`); + logger.error(`Unable to delete history ingest pipeline [${definition.id}]: ${e}`); throw e; } } @@ -40,7 +40,7 @@ export async function deleteLatestIngestPipeline( esClient.ingest.deletePipeline({ id: latestPipelineId }, { ignore: [404] }) ); } catch (e) { - logger.error(`Unable to delete latest ingest pipeline [${definition.id}].`); + logger.error(`Unable to delete latest ingest pipeline [${definition.id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts index 6d4026973ca38..66a79825fbfb0 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/entity_definition_with_backfill.ts @@ -7,7 +7,7 @@ import { entityDefinitionSchema } from '@kbn/entities-schema'; export const entityDefinitionWithBackfill = entityDefinitionSchema.parse({ - id: 'admin-console-services', + id: 'admin-console-services-backfill', version: '999.999.999', name: 'Services for Admin Console', type: 'service', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts new file mode 100644 index 0000000000000..eae0e8e8afc9a --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/fixtures/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { entityDefinition } from './entity_definition'; +export { entityDefinitionWithBackfill } from './entity_definition_with_backfill'; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts index 875242f73d751..a58019bf236ae 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/install_entity_definition.ts @@ -36,8 +36,8 @@ import { import { uninstallEntityDefinition } from './uninstall_entity_definition'; import { isBackfillEnabled } from './helpers/is_backfill_enabled'; import { deleteTemplate, upsertTemplate } from '../manage_index_templates'; -import { getEntitiesLatestIndexTemplateConfig } from '../../templates/entities_latest_template'; -import { getEntitiesHistoryIndexTemplateConfig } from '../../templates/entities_history_template'; +import { getEntitiesLatestIndexTemplateConfig } from './templates/entities_latest_template'; +import { getEntitiesHistoryIndexTemplateConfig } from './templates/entities_history_template'; export interface InstallDefinitionParams { esClient: ElasticsearchClient; @@ -111,7 +111,7 @@ export async function installEntityDefinition({ return entityDefinition; } catch (e) { - logger.error(`Failed to install entity definition ${definition.id}`, e); + logger.error(`Failed to install entity definition ${definition.id}: ${e}`); // Clean up anything that was successful. if (installState.definition) { await deleteEntityDefinition(soClient, definition, logger); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts index 91194140101b7..37a0a48b92b5a 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/read_entity_definition.ts @@ -30,7 +30,7 @@ export async function readEntityDefinition( try { return entityDefinitionSchema.parse(response.saved_objects[0].attributes); } catch (e) { - logger.error(`Unable to parse entity definition with [${id}]`); + logger.error(`Unable to parse entity definition with [${id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts index d49165be22106..17ffeb44affc1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/stop_and_delete_transform.ts @@ -38,7 +38,7 @@ export async function stopAndDeleteHistoryTransform( { logger } ); } catch (e) { - logger.error(`Cannot stop or delete history transform [${definition.id}]`); + logger.error(`Cannot stop or delete history transform [${definition.id}]: ${e}`); throw e; } } @@ -67,7 +67,7 @@ export async function stopAndDeleteHistoryBackfillTransform( { logger } ); } catch (e) { - logger.error(`Cannot stop or delete history backfill transform [${definition.id}]`); + logger.error(`Cannot stop or delete history backfill transform [${definition.id}]: ${e}`); throw e; } } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap new file mode 100644 index 0000000000000..94af9f3307f04 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getEntitiesHistoryIndexTemplateConfig(definitionId) should generate a valid index template 1`] = ` +Object { + "_meta": Object { + "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", + "ecs_version": "8.0.0", + "managed": true, + "managed_by": "elastic_entity_model", + }, + "composed_of": Array [ + "entities_v1_history_base", + "entities_v1_entity", + "entities_v1_event", + "admin-console-services@platform", + "admin-console-services-history@platform", + "admin-console-services@custom", + "admin-console-services-history@custom", + ], + "ignore_missing_component_templates": Array [ + "admin-console-services@platform", + "admin-console-services-history@platform", + "admin-console-services@custom", + "admin-console-services-history@custom", + ], + "index_patterns": Array [ + ".entities.v1.history.admin-console-services.*", + ], + "name": "entities_v1_history_admin-console-services_index_template", + "priority": 200, + "template": Object { + "mappings": Object { + "_meta": Object { + "version": "1.6.0", + }, + "date_detection": false, + "dynamic_templates": Array [ + Object { + "strings_as_keyword": Object { + "mapping": Object { + "fields": Object { + "text": Object { + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "match_mapping_type": "string", + }, + }, + Object { + "entity_metrics": Object { + "mapping": Object { + "type": "{dynamic_type}", + }, + "match_mapping_type": Array [ + "long", + "double", + ], + "path_match": "entity.metrics.*", + }, + }, + ], + }, + "settings": Object { + "index": Object { + "codec": "best_compression", + "mapping": Object { + "total_fields": Object { + "limit": 2000, + }, + }, + }, + }, + }, +} +`; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap new file mode 100644 index 0000000000000..b4247098d9498 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getEntitiesLatestIndexTemplateConfig(definitionId) should generate a valid index template 1`] = ` +Object { + "_meta": Object { + "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the latest dataset", + "ecs_version": "8.0.0", + "managed": true, + "managed_by": "elastic_entity_model", + }, + "composed_of": Array [ + "entities_v1_latest_base", + "entities_v1_entity", + "entities_v1_event", + "admin-console-services@platform", + "admin-console-services-latest@platform", + "admin-console-services@custom", + "admin-console-services-latest@custom", + ], + "ignore_missing_component_templates": Array [ + "admin-console-services@platform", + "admin-console-services-latest@platform", + "admin-console-services@custom", + "admin-console-services-latest@custom", + ], + "index_patterns": Array [ + ".entities.v1.latest.admin-console-services", + ], + "name": "entities_v1_latest_admin-console-services_index_template", + "priority": 200, + "template": Object { + "mappings": Object { + "_meta": Object { + "version": "1.6.0", + }, + "date_detection": false, + "dynamic_templates": Array [ + Object { + "strings_as_keyword": Object { + "mapping": Object { + "fields": Object { + "text": Object { + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "match_mapping_type": "string", + }, + }, + Object { + "entity_metrics": Object { + "mapping": Object { + "type": "{dynamic_type}", + }, + "match_mapping_type": Array [ + "long", + "double", + ], + "path_match": "entity.metrics.*", + }, + }, + ], + }, + "settings": Object { + "index": Object { + "codec": "best_compression", + "mapping": Object { + "total_fields": Object { + "limit": 2000, + }, + }, + }, + }, + }, +} +`; diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts new file mode 100644 index 0000000000000..11aad78741020 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.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 { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { getEntitiesHistoryIndexTemplateConfig } from './entities_history_template'; + +describe('getEntitiesHistoryIndexTemplateConfig(definitionId)', () => { + it('should generate a valid index template', () => { + const template = getEntitiesHistoryIndexTemplateConfig(entityDefinition.id); + expect(template).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts similarity index 87% rename from x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts rename to x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts index 63d589bfaa754..a0fb4b032a6e1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_history_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts @@ -6,14 +6,14 @@ */ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; -import { getEntityHistoryIndexTemplateV1 } from '../../common/helpers'; +import { getEntityHistoryIndexTemplateV1 } from '../../../../common/helpers'; import { ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, ENTITY_HISTORY_INDEX_PREFIX_V1, -} from '../../common/constants_entities'; -import { getCustomHistoryTemplateComponents } from './components/helpers'; +} from '../../../../common/constants_entities'; +import { getCustomHistoryTemplateComponents } from '../../../templates/components/helpers'; export const getEntitiesHistoryIndexTemplateConfig = ( definitionId: string @@ -33,7 +33,7 @@ export const getEntitiesHistoryIndexTemplateConfig = ( ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ...getCustomHistoryTemplateComponents(definitionId), ], - index_patterns: [`${ENTITY_HISTORY_INDEX_PREFIX_V1}.*`], + index_patterns: [`${ENTITY_HISTORY_INDEX_PREFIX_V1}.${definitionId}.*`], priority: 200, template: { mappings: { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts new file mode 100644 index 0000000000000..72583d941492c --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.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 { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { getEntitiesLatestIndexTemplateConfig } from './entities_latest_template'; + +describe('getEntitiesLatestIndexTemplateConfig(definitionId)', () => { + it('should generate a valid index template', () => { + const template = getEntitiesLatestIndexTemplateConfig(entityDefinition.id); + expect(template).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts similarity index 87% rename from x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts rename to x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts index 3ad09e7257a1a..466346f86b44d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/entities_latest_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts @@ -6,14 +6,14 @@ */ import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; -import { getEntityLatestIndexTemplateV1 } from '../../common/helpers'; +import { getEntityLatestIndexTemplateV1 } from '../../../../common/helpers'; import { ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1, ENTITY_LATEST_INDEX_PREFIX_V1, -} from '../../common/constants_entities'; -import { getCustomLatestTemplateComponents } from './components/helpers'; +} from '../../../../common/constants_entities'; +import { getCustomLatestTemplateComponents } from '../../../templates/components/helpers'; export const getEntitiesLatestIndexTemplateConfig = ( definitionId: string @@ -33,8 +33,8 @@ export const getEntitiesLatestIndexTemplateConfig = ( ENTITY_EVENT_COMPONENT_TEMPLATE_V1, ...getCustomLatestTemplateComponents(definitionId), ], - index_patterns: [`${ENTITY_LATEST_INDEX_PREFIX_V1}.*`], - priority: 1, + index_patterns: [`${ENTITY_LATEST_INDEX_PREFIX_V1}.${definitionId}`], + priority: 200, template: { mappings: { _meta: { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap index 4ecdd0c3ab024..551b9761341d2 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/transform/__snapshots__/generate_history_transform.test.ts.snap @@ -9,7 +9,7 @@ Object { "defer_validation": true, "dest": Object { "index": ".entities.v1.history.noop", - "pipeline": "entities-v1-history-admin-console-services", + "pipeline": "entities-v1-history-admin-console-services-backfill", }, "frequency": "5m", "pivot": Object { @@ -143,7 +143,7 @@ Object { "field": "@timestamp", }, }, - "transform_id": "entities-v1-history-backfill-admin-console-services", + "transform_id": "entities-v1-history-backfill-admin-console-services-backfill", } `; diff --git a/x-pack/test/api_integration/apis/entity_manager/definitions.ts b/x-pack/test/api_integration/apis/entity_manager/definitions.ts new file mode 100644 index 0000000000000..89b6bae918fbb --- /dev/null +++ b/x-pack/test/api_integration/apis/entity_manager/definitions.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { EntityDefinition } from '@kbn/entities-schema'; +import { + entityDefinition as mockDefinition, + entityDefinitionWithBackfill as mockBackfillDefinition, +} from '@kbn/entityManager-plugin/server/lib/entities/helpers/fixtures'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { installDefinition, uninstallDefinition, getInstalledDefinitions } from './helpers/request'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Entity definitions', () => { + describe('definitions installations', () => { + it('can install multiple definitions', async () => { + await installDefinition(supertest, mockDefinition); + await installDefinition(supertest, mockBackfillDefinition); + + const { definitions } = await getInstalledDefinitions(supertest); + expect(definitions.length).to.eql(2); + expect( + definitions.find((definition: EntityDefinition) => definition.id === mockDefinition.id) + ); + expect( + definitions.find( + (definition: EntityDefinition) => definition.id === mockBackfillDefinition.id + ) + ); + + await uninstallDefinition(supertest, mockDefinition.id); + await uninstallDefinition(supertest, mockBackfillDefinition.id); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/entity_manager/enablement.ts b/x-pack/test/api_integration/apis/entity_manager/enablement.ts index 8ec5f6743a51f..a84a293e36caf 100644 --- a/x-pack/test/api_integration/apis/entity_manager/enablement.ts +++ b/x-pack/test/api_integration/apis/entity_manager/enablement.ts @@ -11,11 +11,7 @@ import { builtInDefinitions } from '@kbn/entityManager-plugin/server/lib/entitie import { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { createAdmin, createRuntimeUser } from './helpers/user'; - -interface Auth { - username: string; - password: string; -} +import { Auth, getInstalledDefinitions } from './helpers/request'; export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); @@ -32,16 +28,6 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; - const getInstalledDefinitions = async (auth: Auth) => { - const response = await supertest - .get('/internal/entities/definition') - .auth(auth.username, auth.password) - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - return response.body; - }; - const entityDiscoveryState = enablementRequest('get'); const enableEntityDiscovery = enablementRequest('put'); const disableEntityDiscovery = enablementRequest('delete'); @@ -62,7 +48,7 @@ export default function ({ getService }: FtrProviderContext) { const enableResponse = await enableEntityDiscovery(authorizedUser); expect(enableResponse.success).to.eql(true, "authorized user can't enable EEM"); - let definitionsResponse = await getInstalledDefinitions(authorizedUser); + let definitionsResponse = await getInstalledDefinitions(supertest, authorizedUser); expect(definitionsResponse.definitions.length).to.eql(builtInDefinitions.length); expect( builtInDefinitions.every((builtin) => { @@ -93,7 +79,7 @@ export default function ({ getService }: FtrProviderContext) { stateResponse = await entityDiscoveryState(authorizedUser); expect(stateResponse.enabled).to.eql(false, 'EEM is not disabled'); - definitionsResponse = await getInstalledDefinitions(authorizedUser); + definitionsResponse = await getInstalledDefinitions(supertest, authorizedUser); expect(definitionsResponse.definitions).to.eql([]); }); }); @@ -107,7 +93,7 @@ export default function ({ getService }: FtrProviderContext) { const stateResponse = await entityDiscoveryState(unauthorizedUser); expect(stateResponse.enabled).to.eql(false, 'EEM is enabled'); - const definitionsResponse = await getInstalledDefinitions(unauthorizedUser); + const definitionsResponse = await getInstalledDefinitions(supertest, unauthorizedUser); expect(definitionsResponse.definitions).to.eql([]); }); @@ -115,11 +101,11 @@ export default function ({ getService }: FtrProviderContext) { const enableResponse = await enableEntityDiscovery(authorizedUser); expect(enableResponse.success).to.eql(true, "authorized user can't enable EEM"); - let disableResponse = await enableEntityDiscovery(unauthorizedUser); + let disableResponse = await disableEntityDiscovery(unauthorizedUser); expect(disableResponse.success).to.eql(false, 'unauthorized user can disable EEM'); expect(disableResponse.reason).to.eql(ERROR_USER_NOT_AUTHORIZED); - disableResponse = await enableEntityDiscovery(authorizedUser); + disableResponse = await disableEntityDiscovery(authorizedUser); expect(disableResponse.success).to.eql(true, "authorized user can't disable EEM"); }); }); diff --git a/x-pack/test/api_integration/apis/entity_manager/helpers/request.ts b/x-pack/test/api_integration/apis/entity_manager/helpers/request.ts new file mode 100644 index 0000000000000..edb1ccda18f6a --- /dev/null +++ b/x-pack/test/api_integration/apis/entity_manager/helpers/request.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Agent } from 'supertest'; +import { EntityDefinition } from '@kbn/entities-schema'; + +export interface Auth { + username: string; + password: string; +} + +export const getInstalledDefinitions = async (supertest: Agent, auth?: Auth) => { + let req = supertest.get('/internal/entities/definition').set('kbn-xsrf', 'xxx'); + if (auth) { + req = req.auth(auth.username, auth.password); + } + const response = await req.send().expect(200); + return response.body; +}; + +export const installDefinition = async (supertest: Agent, definition: EntityDefinition) => { + return supertest + .post('/internal/entities/definition') + .set('kbn-xsrf', 'xxx') + .send(definition) + .expect(200); +}; + +export const uninstallDefinition = (supertest: Agent, id: string) => { + return supertest + .delete(`/internal/entities/definition/${id}`) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); +}; diff --git a/x-pack/test/api_integration/apis/entity_manager/index.ts b/x-pack/test/api_integration/apis/entity_manager/index.ts index 74a5493401fa3..b6876849ef682 100644 --- a/x-pack/test/api_integration/apis/entity_manager/index.ts +++ b/x-pack/test/api_integration/apis/entity_manager/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['entityManager']); loadTestFile(require.resolve('./enablement')); + loadTestFile(require.resolve('./definitions')); }); } diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 4b07d1fa44d7d..6e05b3d929fbe 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -175,6 +175,7 @@ "@kbn/securitysolution-lists-common", "@kbn/securitysolution-exceptions-common", "@kbn/entityManager-plugin", - "@kbn/osquery-plugin" + "@kbn/osquery-plugin", + "@kbn/entities-schema" ] } From b51c50347994a984d5a46ca94b3f7f361bd0c5da Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Fri, 26 Jul 2024 12:45:42 +0200 Subject: [PATCH 03/12] Expose the encrypted saved objects key rotation API as internal in serverless (#189238) ## Summary In order to begin work for encryption key rotation in serverless, we will need to expose the endpoint use to bulk re-encrypt saved objects. This endpoint was previously unregistered in serverless. This PR registers the API and marks it as internal when a serverless build flavor is detected. ### Tests - x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../crypto/encryption_key_rotation_service.ts | 2 +- .../encrypted_saved_objects/server/plugin.ts | 31 ++++++++-------- .../server/routes/index.mock.ts | 2 ++ .../server/routes/index.ts | 2 ++ .../server/routes/key_rotation.ts | 2 ++ .../encrypted_saved_objects/tsconfig.json | 1 + .../encrypted_saved_objects.ts | 36 ++++++++++++++++--- 7 files changed, 55 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts index d8fa12a3e4973..80275ce3a91e4 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encryption_key_rotation_service.ts @@ -205,7 +205,7 @@ export class EncryptionKeyRotationService { } this.options.logger.info( - `Encryption key rotation is completed. ${result.successful} objects out ouf ${result.total} were successfully re-encrypted with the primary encryption key and ${result.failed} objects failed.` + `Encryption key rotation is completed. ${result.successful} objects out of ${result.total} were successfully re-encrypted with the primary encryption key and ${result.failed} objects failed.` ); return result; diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index 7aecb03868fa8..e7e0a7b482c03 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -110,22 +110,21 @@ export class EncryptedSavedObjectsPlugin getStartServices: core.getStartServices, }); - // In the serverless environment, the encryption keys for saved objects is managed internally and never - // exposed to users and administrators, eliminating the need for any public Encrypted Saved Objects HTTP APIs - if (this.initializerContext.env.packageInfo.buildFlavor !== 'serverless') { - defineRoutes({ - router: core.http.createRouter(), - logger: this.initializerContext.logger.get('routes'), - encryptionKeyRotationService: Object.freeze( - new EncryptionKeyRotationService({ - logger: this.logger.get('key-rotation-service'), - service, - getStartServices: core.getStartServices, - }) - ), - config, - }); - } + // Expose the key rotation route for both stateful and serverless environments + // The endpoint requires admin privileges, and is internal only in serverless + defineRoutes({ + router: core.http.createRouter(), + logger: this.initializerContext.logger.get('routes'), + encryptionKeyRotationService: Object.freeze( + new EncryptionKeyRotationService({ + logger: this.logger.get('key-rotation-service'), + service, + getStartServices: core.getStartServices, + }) + ), + config, + buildFlavor: this.initializerContext.env.packageInfo.buildFlavor, + }); return { canEncrypt, diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts index 822427687759f..e6263521f690d 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { BuildFlavor } from '@kbn/config'; import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import type { ConfigType } from '../config'; @@ -17,5 +18,6 @@ export const routeDefinitionParamsMock = { logger: loggingSystemMock.create().get(), config: ConfigSchema.validate(config) as ConfigType, encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(), + buildFlavor: 'traditional' as BuildFlavor, }), }; diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts index 28f8dde589c75..14d1933e8b765 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { BuildFlavor } from '@kbn/config'; import type { IRouter, Logger } from '@kbn/core/server'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -20,6 +21,7 @@ export interface RouteDefinitionParams { logger: Logger; config: ConfigType; encryptionKeyRotationService: PublicMethodsOf; + buildFlavor: BuildFlavor; } export function defineRoutes(params: RouteDefinitionParams) { diff --git a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts index c9c452cf9a031..e26efbb2a93b3 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/routes/key_rotation.ts @@ -23,6 +23,7 @@ export function defineKeyRotationRoutes({ router, logger, config, + buildFlavor, }: RouteDefinitionParams) { let rotationInProgress = false; router.post( @@ -41,6 +42,7 @@ export function defineKeyRotationRoutes({ options: { tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'], description: `Rotate a key for encrypted saved objects`, + access: buildFlavor === 'serverless' ? 'internal' : undefined, }, }, async (context, request, response) => { diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 83cdcd6225850..d2115146a4a42 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -14,6 +14,7 @@ "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-security-common", "@kbn/test-jest-helpers", + "@kbn/config", ], "exclude": [ "target/**/*", diff --git a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts index ad873c2bc9ce4..51a0d3f03be8f 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/platform_security/encrypted_saved_objects.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from 'expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { InternalRequestHeader, RoleCredentials } from '../../../../shared/services'; @@ -24,13 +25,40 @@ export default function ({ getService }: FtrProviderContext) { await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); }); describe('route access', () => { - describe('disabled', () => { + describe('internal', () => { it('rotate key', async () => { - const { body, status } = await supertestWithoutAuth + let body: unknown; + let status: number; + + ({ body, status } = await supertestWithoutAuth + .post('/api/encrypted_saved_objects/_rotate_key') + // .set(internalReqHeader) + .set(roleAuthc.apiKeyHeader)); + // svlCommonApi.assertApiNotFound(body, status); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining('Request must contain a kbn-xsrf header.'), + }); + expect(status).toBe(400); + + ({ body, status } = await supertestWithoutAuth .post('/api/encrypted_saved_objects/_rotate_key') .set(internalReqHeader) - .set(roleAuthc.apiKeyHeader); - svlCommonApi.assertApiNotFound(body, status); + .set(roleAuthc.apiKeyHeader)); + // expect a different, legitimate error when we use the internal header + // the config does not contain decryptionOnlyKeys, so when the API is + // called successfully, it will error for this reason, and not for an + // access or or missing header reason + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'Kibana is not configured to support encryption key rotation. Update `kibana.yml` to include `xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys` to rotate your encryption keys.' + ), + }); + expect(status).toBe(400); }); }); }); From 6739a1ed6302b5fdf4062f6945a446dc83fe579a Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 26 Jul 2024 07:42:41 -0400 Subject: [PATCH 04/12] =?UTF-8?q?Fixes=20Failing=20test:=20X-Pack=20Alerti?= =?UTF-8?q?ng=20API=20Integration=20Tests=20-=20Alerting=20-=20group2.x-pa?= =?UTF-8?q?ck/test/alerting=5Fapi=5Fintegration/spaces=5Fonly/tests/alerti?= =?UTF-8?q?ng/group2/monitoring=5Fcollection=C2=B7ts=20-=20Alerting=20moni?= =?UTF-8?q?toring=5Fcollection=20inMemoryMetrics=20should=20count=20timeou?= =?UTF-8?q?ts=20(#189214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves https://github.com/elastic/kibana/issues/187275 When looking at the flaky test logs, I could see some ES errors for the `test.cancellableRule` rule type we're using in this test. This rule type uses a shard delay aggregation to increase the execution duration of a rule to force a timeout. Switching instead to just delaying using an `await new Promise((resolve) => setTimeout(resolve, 10000));`. --- .../tests/alerting/group2/monitoring_collection.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts index f87c1c095dd8f..902362f9fc31c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/monitoring_collection.ts @@ -38,8 +38,7 @@ export default function alertingMonitoringCollectionTests({ getService }: FtrPro ? dedicatedTaskRunner.getSupertest() : supertest; - // Failing: See https://github.com/elastic/kibana/issues/187275 - describe.skip('monitoring_collection', () => { + describe('monitoring_collection', () => { let endDate: string; const objectRemover = new ObjectRemover(supertest); @@ -124,8 +123,8 @@ export default function alertingMonitoringCollectionTests({ getService }: FtrPro rule_type_id: 'test.cancellableRule', schedule: { interval: '4s' }, params: { - doLongSearch: true, - doLongPostProcessing: false, + doLongSearch: false, + doLongPostProcessing: true, }, }) ); From b75d74a9fa7a3436afd6eade6f65df2b39fadefb Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Fri, 26 Jul 2024 07:43:47 -0400 Subject: [PATCH 05/12] [Response Ops][Alerting] Assigning extra large cost to indicator match rule types (#189220) Resolves https://github.com/elastic/kibana/issues/189112 ## Summary Adds a mapping to the alerting rule type registry to manage rule types with a custom task cost and register appropriately. Adds an integration test to task manager so we can be alerted to task types that register with non-normal task costs. --- .../server/rule_type_registry.test.ts | 33 ++++++++++ .../alerting/server/rule_type_registry.ts | 7 +++ .../task_cost_check.test.ts.snap | 10 +++ .../integration_tests/task_cost_check.test.ts | 63 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap create mode 100644 x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 3ee3551a301d5..e678228660e51 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -564,6 +564,39 @@ describe('Create Lifecycle', () => { }); }); + test('injects custom cost for certain rule types', () => { + const ruleType: RuleType = { + id: 'siem.indicatorRule', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + category: 'test', + producer: 'alerts', + ruleTaskTimeout: '20m', + validate: { + params: { validate: (params) => params }, + }, + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + registry.register(ruleType); + expect(taskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); + expect(taskManager.registerTaskDefinitions.mock.calls[0][0]).toMatchObject({ + 'alerting:siem.indicatorRule': { + timeout: '20m', + title: 'Test', + cost: 10, + }, + }); + }); + test('shallow clones the given rule type', () => { const ruleType: RuleType = { id: 'test', diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index d1ffe59df3b6f..bc7a10d767ff0 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -14,6 +14,7 @@ import { Logger } from '@kbn/core/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { RunContext, TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { stateSchemaByVersion } from '@kbn/alerting-state-types'; +import { TaskCost } from '@kbn/task-manager-plugin/server/task'; import { TaskRunnerFactory } from './task_runner'; import { RuleType, @@ -40,6 +41,9 @@ import { AlertsService } from './alerts_service/alerts_service'; import { getRuleTypeIdValidLegacyConsumers } from './rule_type_registry_deprecated_consumers'; import { AlertingConfig } from './config'; +const RULE_TYPES_WITH_CUSTOM_COST: Record = { + 'siem.indicatorRule': TaskCost.ExtraLarge, +}; export interface ConstructorOptions { config: AlertingConfig; logger: Logger; @@ -289,6 +293,8 @@ export class RuleTypeRegistry { normalizedRuleType as unknown as UntypedNormalizedRuleType ); + const taskCost: TaskCost | undefined = RULE_TYPES_WITH_CUSTOM_COST[ruleType.id]; + this.taskManager.registerTaskDefinitions({ [`alerting:${ruleType.id}`]: { title: ruleType.name, @@ -310,6 +316,7 @@ export class RuleTypeRegistry { spaceId: schema.string(), consumer: schema.maybe(schema.string()), }), + ...(taskCost ? { cost: taskCost } : {}), }, }); diff --git a/x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap b/x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap new file mode 100644 index 0000000000000..e59912ed91905 --- /dev/null +++ b/x-pack/plugins/task_manager/server/integration_tests/__snapshots__/task_cost_check.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Task cost checks detects tasks with cost definitions 1`] = ` +Array [ + Object { + "cost": 10, + "taskType": "alerting:siem.indicatorRule", + }, +] +`; diff --git a/x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts b/x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts new file mode 100644 index 0000000000000..96678f714ac69 --- /dev/null +++ b/x-pack/plugins/task_manager/server/integration_tests/task_cost_check.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright 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 TestElasticsearchUtils, + type TestKibanaUtils, +} from '@kbn/core-test-helpers-kbn-server'; +import { TaskCost, TaskDefinition } from '../task'; +import { setupTestServers } from './lib'; +import { TaskTypeDictionary } from '../task_type_dictionary'; + +jest.mock('../task_type_dictionary', () => { + const actual = jest.requireActual('../task_type_dictionary'); + return { + ...actual, + TaskTypeDictionary: jest.fn().mockImplementation((opts) => { + return new actual.TaskTypeDictionary(opts); + }), + }; +}); + +// Notify response-ops if a task sets a cost to something other than `Normal` +describe('Task cost checks', () => { + let esServer: TestElasticsearchUtils; + let kibanaServer: TestKibanaUtils; + let taskTypeDictionary: TaskTypeDictionary; + + beforeAll(async () => { + const setupResult = await setupTestServers(); + esServer = setupResult.esServer; + kibanaServer = setupResult.kibanaServer; + + const mockedTaskTypeDictionary = jest.requireMock('../task_type_dictionary'); + expect(mockedTaskTypeDictionary.TaskTypeDictionary).toHaveBeenCalledTimes(1); + taskTypeDictionary = mockedTaskTypeDictionary.TaskTypeDictionary.mock.results[0].value; + }); + + afterAll(async () => { + if (kibanaServer) { + await kibanaServer.stop(); + } + if (esServer) { + await esServer.stop(); + } + }); + + it('detects tasks with cost definitions', async () => { + const taskTypes = taskTypeDictionary.getAllDefinitions(); + const taskTypesWithCost = taskTypes + .map((taskType: TaskDefinition) => + !!taskType.cost ? { taskType: taskType.type, cost: taskType.cost } : null + ) + .filter( + (tt: { taskType: string; cost: TaskCost } | null) => + null != tt && tt.cost !== TaskCost.Normal + ); + expect(taskTypesWithCost).toMatchSnapshot(); + }); +}); From 6ddffb57fb7598b6b0e0593064629651c44d1d5e Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:54:22 +0200 Subject: [PATCH 06/12] Compute dashboard panel selection list lazily (#187797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes https://github.com/elastic/kibana/issues/187587 This PR changes how the dashboard panel selection items get computed, it had previously been computed eagerly, in this implementation panel selection items would only be computed when the user actually clicks the `add panel` button, with it's results cached so that subsequent interactions with the `add panel` button leverages the already computed data. **Notable Mention:** The options presented as the dashboard panel list now only comprise of uiActions specifically registered with the uiAction trigger `ADD_PANEL_TRIGGER` and specific dashboard visualisation types. See https://github.com/elastic/kibana/pull/187797#discussion_r1681320456 to follow the reasoning behind this. That been said adding new panels to the dashboard, would be something along the following lines; ```ts import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; uiActions.attachAction(ADD_PANEL_TRIGGER, ); // alternatively // uiActions.addTriggerAction(ADD_PANEL_TRIGGER, ...someActionDefintion); ```` ### Visuals https://github.com/elastic/kibana/assets/7893459/7c029a64-2cd8-4e3e-af5a-44b6788faa45 ### How to test - Navigate to a dashboard of choice - Slow down your network speed using your browser dev tools, refresh your dashboard, and click on the “Add panel” button as soon as it is available (before the panels have a chance to load). - You should be presented with a loading indicator, that eventually is swapped out for the list of panels available for selection. ### 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] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) --- .../add_panel_action_menu_items.test.ts | 0 .../add_panel_action_menu_items.ts | 4 +- .../dashboard_panel_selection_flyout.test.tsx | 110 +++++ .../dashboard_panel_selection_flyout.tsx | 287 +++++++++++++ .../top_nav/add_new_panel/index.ts | 10 + .../use_get_dashboard_panels.test.ts | 220 ++++++++++ .../add_new_panel/use_get_dashboard_panels.ts | 220 ++++++++++ .../top_nav/editor_menu.test.tsx | 159 ++----- .../dashboard_app/top_nav/editor_menu.tsx | 393 +++--------------- .../open_dashboard_panel_selection_flyout.tsx | 255 ------------ .../services/dashboard/add_panel.ts | 3 + x-pack/plugins/lens/public/plugin.ts | 8 +- .../open_lens_config/create_action.test.tsx | 26 +- .../open_lens_config/create_action.tsx | 9 +- .../open_lens_config/create_action_helpers.ts | 33 +- 15 files changed, 1012 insertions(+), 725 deletions(-) rename src/plugins/dashboard/public/dashboard_app/top_nav/{ => add_new_panel}/add_panel_action_menu_items.test.ts (100%) rename src/plugins/dashboard/public/dashboard_app/top_nav/{ => add_new_panel}/add_panel_action_menu_items.ts (97%) create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts create mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts delete mode 100644 src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.test.ts similarity index 100% rename from src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.test.ts rename to src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.test.ts diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.ts similarity index 97% rename from src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts rename to src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.ts index 4e90d94caa388..f13b6128ed405 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/add_panel_action_menu_items.ts +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/add_panel_action_menu_items.ts @@ -55,7 +55,7 @@ const onAddPanelActionClick = export const getAddPanelActionMenuItemsGroup = ( api: PresentationContainer, actions: Array> | undefined, - closePopover: () => void + onPanelSelected: () => void ) => { const grouped: Record = {}; @@ -72,7 +72,7 @@ export const getAddPanelActionMenuItemsGroup = ( name: actionName, icon: (typeof item.getIconType === 'function' ? item.getIconType(context) : undefined) ?? 'empty', - onClick: onAddPanelActionClick(item, context, closePopover), + onClick: onAddPanelActionClick(item, context, onPanelSelected), 'data-test-subj': `create-action-${actionName}`, description: item?.getDisplayNameTooltip?.(context), order: item.order ?? 0, diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx new file mode 100644 index 0000000000000..17fa7f23f4adb --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.test.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { type ComponentProps } from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { DashboardPanelSelectionListFlyout } from './dashboard_panel_selection_flyout'; +import type { GroupedAddPanelActions } from './add_panel_action_menu_items'; + +const defaultProps: Omit< + ComponentProps, + 'fetchDashboardPanels' +> = { + close: jest.fn(), + paddingSize: 's', +}; + +const renderComponent = ({ + fetchDashboardPanels, +}: Pick, 'fetchDashboardPanels'>) => + render( + + + + ); + +const panelConfiguration: GroupedAddPanelActions[] = [ + { + id: 'panel1', + title: 'App 1', + items: [ + { + icon: 'icon1', + id: 'mockFactory', + name: 'Factory 1', + description: 'Factory 1 description', + 'data-test-subj': 'createNew-mockFactory', + onClick: jest.fn(), + order: 0, + }, + ], + order: 10, + 'data-test-subj': 'dashboardEditorMenu-group1Group', + }, +]; + +describe('DashboardPanelSelectionListFlyout', () => { + it('renders a loading indicator when fetchDashboardPanel has not yielded any value', async () => { + const promiseDelay = 5000; + + renderComponent({ + fetchDashboardPanels: jest.fn( + () => + new Promise((resolve) => { + setTimeout(() => resolve(panelConfiguration), promiseDelay); + }) + ), + }); + + expect( + await screen.findByTestId('dashboardPanelSelectionLoadingIndicator') + ).toBeInTheDocument(); + }); + + it('renders an error indicator when fetchDashboardPanel errors', async () => { + renderComponent({ + fetchDashboardPanels: jest.fn().mockRejectedValue(new Error('simulated error')), + }); + + expect(await screen.findByTestId('dashboardPanelSelectionErrorIndicator')).toBeInTheDocument(); + }); + + it('renders the list of available panels when fetchDashboardPanel resolves a value', async () => { + renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) }); + + expect(await screen.findByTestId(panelConfiguration[0]['data-test-subj']!)).toBeInTheDocument(); + }); + + it('renders a not found message when a user searches for an item that is not in the selection list', async () => { + renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) }); + + expect(await screen.findByTestId(panelConfiguration[0]['data-test-subj']!)).toBeInTheDocument(); + + act(() => { + userEvent.type( + screen.getByTestId('dashboardPanelSelectionFlyout__searchInput'), + 'non existent panel' + ); + }); + + expect(await screen.findByTestId('dashboardPanelSelectionNoPanelMessage')).toBeInTheDocument(); + }); + + it('invokes the close method when the flyout close btn is clicked', async () => { + renderComponent({ fetchDashboardPanels: jest.fn().mockResolvedValue(panelConfiguration) }); + + fireEvent.click(await screen.findByTestId('dashboardPanelSelectionCloseBtn')); + + expect(defaultProps.close).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx new file mode 100644 index 0000000000000..bb23fd6798b11 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n as i18nFn } from '@kbn/i18n'; +import { type EuiFlyoutProps, EuiLoadingChart } from '@elastic/eui'; +import orderBy from 'lodash/orderBy'; +import { + EuiEmptyPrompt, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiBadge, + EuiFormRow, + EuiTitle, + EuiFieldSearch, + useEuiTheme, + EuiListGroup, + EuiListGroupItem, + EuiToolTip, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + type PanelSelectionMenuItem, + type GroupedAddPanelActions, +} from './add_panel_action_menu_items'; + +export interface DashboardPanelSelectionListFlyoutProps { + /** Handler to close flyout */ + close: () => void; + /** Padding for flyout */ + paddingSize: Exclude; + /** Fetches the panels available for a dashboard */ + fetchDashboardPanels: () => Promise; +} + +export const DashboardPanelSelectionListFlyout: React.FC< + DashboardPanelSelectionListFlyoutProps +> = ({ close, paddingSize, fetchDashboardPanels }) => { + const { euiTheme } = useEuiTheme(); + const [{ data: panels, loading, error }, setPanelState] = useState<{ + loading: boolean; + data: GroupedAddPanelActions[] | null; + error: unknown | null; + }>({ loading: true, data: null, error: null }); + + const [searchTerm, setSearchTerm] = useState(''); + const [panelsSearchResult, setPanelsSearchResult] = useState( + panels + ); + + useEffect(() => { + const requestDashboardPanels = () => { + fetchDashboardPanels() + .then((_panels) => + setPanelState((prevState) => ({ + ...prevState, + loading: false, + data: _panels, + })) + ) + .catch((err) => + setPanelState((prevState) => ({ + ...prevState, + loading: false, + error: err, + })) + ); + }; + + requestDashboardPanels(); + }, [fetchDashboardPanels]); + + useEffect(() => { + const _panels = (panels ?? []).slice(0); + + if (!searchTerm) { + return setPanelsSearchResult(_panels); + } + + const q = searchTerm.toLowerCase(); + + setPanelsSearchResult( + orderBy( + _panels.map((panel) => { + const groupSearchMatch = panel.title.toLowerCase().includes(q); + + const [groupSearchMatchAgg, items] = panel.items.reduce( + (acc, cur) => { + const searchMatch = cur.name.toLowerCase().includes(q); + + acc[0] = acc[0] || searchMatch; + acc[1].push({ + ...cur, + isDisabled: !(groupSearchMatch || searchMatch), + }); + + return acc; + }, + [groupSearchMatch, [] as PanelSelectionMenuItem[]] + ); + + return { + ...panel, + isDisabled: !groupSearchMatchAgg, + items, + }; + }), + ['isDisabled'] + ) + ); + }, [panels, searchTerm]); + + return ( + <> + + +

+ +

+
+
+ + + + + + { + setSearchTerm(e.target.value); + }} + aria-label={i18nFn.translate( + 'dashboard.editorMenu.addPanelFlyout.searchLabelText', + { defaultMessage: 'search field for panels' } + )} + className="nsPanelSelectionFlyout__searchInput" + data-test-subj="dashboardPanelSelectionFlyout__searchInput" + /> + + + + + {loading ? ( + } + /> + ) : ( + + {panelsSearchResult?.some(({ isDisabled }) => !isDisabled) ? ( + panelsSearchResult.map( + ({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj }) => + !isDisabled ? ( + + + {typeof title === 'string' ?

{title}

: title} +
+ + {items?.map((item, idx) => { + return ( + + {!item.isDeprecated ? ( + {item.name} + ) : ( + + + {item.name} + + + + + + + + )} + + } + onClick={item?.onClick} + iconType={item.icon} + data-test-subj={item['data-test-subj']} + isDisabled={item.isDisabled} + /> + ); + })} + +
+ ) : null + ) + ) : ( + <> + {Boolean(error) ? ( + + + + } + data-test-subj="dashboardPanelSelectionErrorIndicator" + /> + ) : ( + + + + )} + + )} +
+ )} +
+
+
+ + + + + + + + + + + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts new file mode 100644 index 0000000000000..5b94fba6bfffc --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { useGetDashboardPanels } from './use_get_dashboard_panels'; +export { DashboardPanelSelectionListFlyout } from './dashboard_panel_selection_flyout'; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts new file mode 100644 index 0000000000000..b8ca0cd27aa01 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.test.ts @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import type { PresentationContainer } from '@kbn/presentation-containers'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import { type BaseVisType, VisGroups, VisTypeAlias } from '@kbn/visualizations-plugin/public'; +import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { useGetDashboardPanels } from './use_get_dashboard_panels'; +import { pluginServices } from '../../../services/plugin_services'; + +const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked; + +describe('Get dashboard panels hook', () => { + const defaultHookProps: Parameters[0] = { + api: mockApi, + createNewVisType: jest.fn(), + }; + + type PluginServices = ReturnType; + + let compatibleTriggerActionsRequestSpy: jest.SpyInstance< + ReturnType> + >; + + let dashboardVisualizationGroupGetterSpy: jest.SpyInstance< + ReturnType + >; + + let dashboardVisualizationAliasesGetterSpy: jest.SpyInstance< + ReturnType + >; + + beforeAll(() => { + const _pluginServices = pluginServices.getServices(); + + compatibleTriggerActionsRequestSpy = jest.spyOn( + _pluginServices.uiActions, + 'getTriggerCompatibleActions' + ); + + dashboardVisualizationGroupGetterSpy = jest.spyOn(_pluginServices.visualizations, 'getByGroup'); + + dashboardVisualizationAliasesGetterSpy = jest.spyOn( + _pluginServices.visualizations, + 'getAliases' + ); + }); + + beforeEach(() => { + compatibleTriggerActionsRequestSpy.mockResolvedValue([]); + dashboardVisualizationGroupGetterSpy.mockReturnValue([]); + dashboardVisualizationAliasesGetterSpy.mockReturnValue([]); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('useGetDashboardPanels', () => { + it('hook return value is callable', () => { + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + }); + + it('returns a callable method that yields a cached result if invoked after a prior resolution', async () => { + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + + const firstInvocationResult = await result.current(jest.fn()); + + expect(compatibleTriggerActionsRequestSpy).toHaveBeenCalledWith(ADD_PANEL_TRIGGER, { + embeddable: expect.objectContaining(mockApi), + }); + + const secondInvocationResult = await result.current(jest.fn()); + + expect(firstInvocationResult).toStrictEqual(secondInvocationResult); + + expect(compatibleTriggerActionsRequestSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('augmenting ui action group items with dashboard visualization types', () => { + it.each([ + ['visualizations', VisGroups.PROMOTED], + [COMMON_EMBEDDABLE_GROUPING.legacy.id, VisGroups.LEGACY], + [COMMON_EMBEDDABLE_GROUPING.annotation.id, VisGroups.TOOLS], + ])( + 'includes in the ui action %s group, %s dashboard visualization group types', + async (uiActionGroupId, dashboardVisualizationGroupId) => { + const mockVisualizationsUiAction: Action = { + id: `some-${uiActionGroupId}-action`, + type: '', + order: 10, + grouping: [ + { + id: uiActionGroupId, + order: 1000, + getDisplayName: jest.fn(), + getIconType: jest.fn(), + }, + ], + getDisplayName: jest.fn(() => `Some ${uiActionGroupId} visualization Action`), + getIconType: jest.fn(), + execute: jest.fn(), + isCompatible: jest.fn(() => Promise.resolve(true)), + }; + + const mockDashboardVisualizationType = { + name: dashboardVisualizationGroupId, + title: dashboardVisualizationGroupId, + order: 0, + description: `This is a dummy representation of a ${dashboardVisualizationGroupId} visualization.`, + icon: 'empty', + stage: 'production', + isDeprecated: false, + group: dashboardVisualizationGroupId, + titleInWizard: `Custom ${dashboardVisualizationGroupId} visualization`, + } as BaseVisType; + + compatibleTriggerActionsRequestSpy.mockResolvedValue([mockVisualizationsUiAction]); + + dashboardVisualizationGroupGetterSpy.mockImplementation((group) => { + if (group !== dashboardVisualizationGroupId) return []; + + return [mockDashboardVisualizationType]; + }); + + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + + expect(await result.current(jest.fn())).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: uiActionGroupId, + 'data-test-subj': `dashboardEditorMenu-${uiActionGroupId}Group`, + items: expect.arrayContaining([ + expect.objectContaining({ + // @ts-expect-error ignore passing the required context in this test + 'data-test-subj': `create-action-${mockVisualizationsUiAction.getDisplayName()}`, + }), + expect.objectContaining({ + 'data-test-subj': `visType-${mockDashboardVisualizationType.name}`, + }), + ]), + }), + ]) + ); + } + ); + + it('includes in the ui action visualization group dashboard visualization alias types', async () => { + const mockVisualizationsUiAction: Action = { + id: 'some-vis-action', + type: '', + order: 10, + grouping: [ + { + id: 'visualizations', + order: 1000, + getDisplayName: jest.fn(), + getIconType: jest.fn(), + }, + ], + getDisplayName: jest.fn(() => 'Some visualization Action'), + getIconType: jest.fn(), + execute: jest.fn(), + isCompatible: jest.fn(() => Promise.resolve(true)), + }; + + const mockedAliasVisualizationType: VisTypeAlias = { + name: 'alias visualization', + title: 'Alias Visualization', + order: 0, + description: 'This is a dummy representation of aan aliased visualization.', + icon: 'empty', + stage: 'production', + isDeprecated: false, + }; + + compatibleTriggerActionsRequestSpy.mockResolvedValue([mockVisualizationsUiAction]); + + dashboardVisualizationAliasesGetterSpy.mockReturnValue([mockedAliasVisualizationType]); + + const { result } = renderHook(() => useGetDashboardPanels(defaultHookProps)); + expect(result.current).toBeInstanceOf(Function); + + expect(await result.current(jest.fn())).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: mockVisualizationsUiAction.grouping![0].id, + 'data-test-subj': `dashboardEditorMenu-${ + mockVisualizationsUiAction.grouping![0].id + }Group`, + items: expect.arrayContaining([ + expect.objectContaining({ + // @ts-expect-error ignore passing the required context in this test + 'data-test-subj': `create-action-${mockVisualizationsUiAction.getDisplayName()}`, + }), + expect.objectContaining({ + 'data-test-subj': `visType-${mockedAliasVisualizationType.name}`, + }), + ]), + }), + ]) + ); + }); + }); +}); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts new file mode 100644 index 0000000000000..13f7f7ff12dd0 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/use_get_dashboard_panels.ts @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useMemo, useRef, useCallback } from 'react'; +import type { IconType } from '@elastic/eui'; +import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import { type Subscription, AsyncSubject, from, defer, map, lastValueFrom } from 'rxjs'; +import { EmbeddableFactory, COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { PresentationContainer } from '@kbn/presentation-containers'; +import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public'; + +import { pluginServices } from '../../../services/plugin_services'; +import { + getAddPanelActionMenuItemsGroup, + type PanelSelectionMenuItem, + type GroupedAddPanelActions, +} from './add_panel_action_menu_items'; + +interface UseGetDashboardPanelsArgs { + api: PresentationContainer; + createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void; +} + +export interface FactoryGroup { + id: string; + appName: string; + icon?: IconType; + factories: EmbeddableFactory[]; + order: number; +} + +const sortGroupPanelsByOrder = (panelGroups: T[]): T[] => { + return panelGroups.sort( + // larger number sorted to the top + (panelGroupA, panelGroupB) => panelGroupB.order - panelGroupA.order + ); +}; + +export const useGetDashboardPanels = ({ api, createNewVisType }: UseGetDashboardPanelsArgs) => { + const panelsComputeResultCache = useRef(new AsyncSubject()); + const panelsComputeSubscription = useRef(null); + + const { + uiActions, + visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup }, + } = pluginServices.getServices(); + + const getSortedVisTypesByGroup = (group: VisGroups) => + getVisTypesByGroup(group) + .sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => { + const labelA = 'titleInWizard' in a ? a.titleInWizard || a.title : a.title; + const labelB = 'titleInWizard' in b ? b.titleInWizard || a.title : a.title; + if (labelA < labelB) { + return -1; + } + if (labelA > labelB) { + return 1; + } + return 0; + }) + .filter(({ disableCreate }: BaseVisType) => !disableCreate); + + const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED); + const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS); + const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY); + + const visTypeAliases = getVisTypeAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ) + .filter(({ disableCreate }: VisTypeAlias) => !disableCreate); + + const augmentedCreateNewVisType = useCallback( + (visType: Parameters[0], cb: () => void) => { + const visClickHandler = createNewVisType(visType); + return () => { + visClickHandler(); + cb(); + }; + }, + [createNewVisType] + ); + + const getVisTypeMenuItem = useCallback( + (onClickCb: () => void, visType: BaseVisType): PanelSelectionMenuItem => { + const { + name, + title, + titleInWizard, + description, + icon = 'empty', + isDeprecated, + order, + } = visType; + return { + id: name, + name: titleInWizard || title, + isDeprecated, + icon, + onClick: augmentedCreateNewVisType(visType, onClickCb), + 'data-test-subj': `visType-${name}`, + description, + order, + }; + }, + [augmentedCreateNewVisType] + ); + + const getVisTypeAliasMenuItem = useCallback( + (onClickCb: () => void, visTypeAlias: VisTypeAlias): PanelSelectionMenuItem => { + const { name, title, description, icon = 'empty', order } = visTypeAlias; + + return { + id: name, + name: title, + icon, + onClick: augmentedCreateNewVisType(visTypeAlias, onClickCb), + 'data-test-subj': `visType-${name}`, + description, + order: order ?? 0, + }; + }, + [augmentedCreateNewVisType] + ); + + const addPanelAction$ = useMemo( + () => + defer(() => { + return from( + uiActions?.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, { + embeddable: api, + }) ?? [] + ); + }), + [api, uiActions] + ); + + const computeAvailablePanels = useCallback( + (onPanelSelected: () => void) => { + if (!panelsComputeSubscription.current) { + panelsComputeSubscription.current = addPanelAction$ + .pipe( + map((addPanelActions) => + getAddPanelActionMenuItemsGroup(api, addPanelActions, onPanelSelected) + ), + map((groupedAddPanelAction) => { + return sortGroupPanelsByOrder( + Object.values(groupedAddPanelAction) + ).map((panelGroup) => { + switch (panelGroup.id) { + case 'visualizations': { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + // TODO: actually add grouping to vis type alias so we wouldn't randomly display an unintended item + visTypeAliases.map(getVisTypeAliasMenuItem.bind(null, onPanelSelected)), + promotedVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected)) + ) + ), + }; + } + case COMMON_EMBEDDABLE_GROUPING.legacy.id: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + legacyVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected)) + ) + ), + }; + } + case COMMON_EMBEDDABLE_GROUPING.annotation.id: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder( + (panelGroup.items ?? []).concat( + toolVisTypes.map(getVisTypeMenuItem.bind(null, onPanelSelected)) + ) + ), + }; + } + default: { + return { + ...panelGroup, + items: sortGroupPanelsByOrder(panelGroup.items), + }; + } + } + }); + }) + ) + .subscribe(panelsComputeResultCache.current); + } + }, + [ + api, + addPanelAction$, + getVisTypeMenuItem, + getVisTypeAliasMenuItem, + toolVisTypes, + legacyVisTypes, + promotedVisTypes, + visTypeAliases, + ] + ); + + return useCallback( + (...args: Parameters) => { + computeAvailablePanels(...args); + return lastValueFrom(panelsComputeResultCache.current.asObservable()); + }, + [computeAvailablePanels] + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx index c2b51a5e0eda5..8310019f91b40 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.test.tsx @@ -6,134 +6,53 @@ * Side Public License, v 1. */ -import { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import React, { ComponentProps } from 'react'; +import { render } from '@testing-library/react'; import { PresentationContainer } from '@kbn/presentation-containers'; -import { GroupedAddPanelActions } from './add_panel_action_menu_items'; -import { - FactoryGroup, - mergeGroupedItemsProvider, - getEmbeddableFactoryMenuItemProvider, -} from './editor_menu'; +import { EditorMenu } from './editor_menu'; +import { DashboardAPIContext } from '../dashboard_app'; +import { buildMockDashboard } from '../../mocks'; -describe('mergeGroupedItemsProvider', () => { - const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked; - const closePopoverSpy = jest.fn(); +import { pluginServices } from '../../services/plugin_services'; - const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider( - mockApi, - closePopoverSpy - ); +jest.mock('../../services/plugin_services', () => { + const module = jest.requireActual('../../services/plugin_services'); - const mockFactory = { - id: 'factory1', - type: 'mockFactory', - getDisplayName: () => 'Factory 1', - getDescription: () => 'Factory 1 description', - getIconType: () => 'icon1', - } as unknown as EmbeddableFactory; + const _pluginServices = (module.pluginServices as typeof pluginServices).getServices(); - const factoryGroupMap = { - group1: { - id: 'panel1', - appName: 'App 1', - icon: 'icon1', - order: 10, - factories: [mockFactory], - }, - } as unknown as Record; + jest + .spyOn(_pluginServices.embeddable, 'getEmbeddableFactories') + .mockReturnValue(new Map().values()); + jest.spyOn(_pluginServices.uiActions, 'getTriggerCompatibleActions').mockResolvedValue([]); + jest.spyOn(_pluginServices.visualizations, 'getByGroup').mockReturnValue([]); + jest.spyOn(_pluginServices.visualizations, 'getAliases').mockReturnValue([]); - const groupedAddPanelAction = { - group1: { - id: 'panel2', - title: 'Panel 2', - icon: 'icon2', - order: 10, - items: [ - { - id: 'addPanelActionId', - order: 0, - }, - ], + return { + ...module, + pluginServices: { + ...module.pluginServices, + getServices: jest.fn().mockReturnValue(_pluginServices), }, - } as unknown as Record; - - it('should merge factoryGroupMap and groupedAddPanelAction correctly', () => { - const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - factoryGroupMap, - groupedAddPanelAction - ); - - expect(groupedPanels).toEqual([ - { - id: 'panel1', - title: 'App 1', - items: [ - { - icon: 'icon1', - name: 'Factory 1', - id: 'mockFactory', - description: 'Factory 1 description', - 'data-test-subj': 'createNew-mockFactory', - onClick: expect.any(Function), - order: 0, - }, - { - id: 'addPanelActionId', - order: 0, - }, - ], - 'data-test-subj': 'dashboardEditorMenu-group1Group', - order: 10, - }, - ]); - }); - - it('should handle missing factoryGroup correctly', () => { - const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - {}, - groupedAddPanelAction - ); - - expect(groupedPanels).toEqual([ - { - id: 'panel2', - icon: 'icon2', - title: 'Panel 2', - items: [ - { - id: 'addPanelActionId', - order: 0, - }, - ], - order: 10, - }, - ]); - }); - - it('should handle missing groupedAddPanelAction correctly', () => { - const groupedPanels = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - factoryGroupMap, - {} - ); + }; +}); - expect(groupedPanels).toEqual([ - { - id: 'panel1', - title: 'App 1', - items: [ - { - icon: 'icon1', - id: 'mockFactory', - name: 'Factory 1', - description: 'Factory 1 description', - 'data-test-subj': 'createNew-mockFactory', - onClick: expect.any(Function), - order: 0, - }, - ], - order: 10, - 'data-test-subj': 'dashboardEditorMenu-group1Group', +const mockApi = { addNewPanel: jest.fn() } as unknown as jest.Mocked; + +describe('editor menu', () => { + const defaultProps: ComponentProps = { + api: mockApi, + createNewVisType: jest.fn(), + }; + + it('renders without crashing', async () => { + render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); }, - ]); + }); }); }); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index ba7811dfec360..c7902dd632f95 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -8,354 +8,79 @@ import './editor_menu.scss'; -import React, { useEffect, useMemo, useState, useRef } from 'react'; -import { type IconType } from '@elastic/eui'; +import React, { useEffect, useCallback, type ComponentProps } from 'react'; + import { i18n } from '@kbn/i18n'; -import { type Action, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { ToolbarButton } from '@kbn/shared-ux-button-toolbar'; -import { PresentationContainer } from '@kbn/presentation-containers'; -import { type BaseVisType, VisGroups, type VisTypeAlias } from '@kbn/visualizations-plugin/public'; -import { EmbeddableFactory, COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; + +import { useGetDashboardPanels, DashboardPanelSelectionListFlyout } from './add_new_panel'; import { pluginServices } from '../../services/plugin_services'; -import { - getAddPanelActionMenuItemsGroup, - type PanelSelectionMenuItem, - type GroupedAddPanelActions, -} from './add_panel_action_menu_items'; -import { openDashboardPanelSelectionFlyout } from './open_dashboard_panel_selection_flyout'; -import type { DashboardServices } from '../../services/types'; import { useDashboardAPI } from '../dashboard_app'; -export interface FactoryGroup { - id: string; - appName: string; - icon?: IconType; - factories: EmbeddableFactory[]; - order: number; -} - -interface UnwrappedEmbeddableFactory { - factory: EmbeddableFactory; - isEditable: boolean; -} - -export type GetEmbeddableFactoryMenuItem = ReturnType; - -export const getEmbeddableFactoryMenuItemProvider = - (api: PresentationContainer, closePopover: () => void) => - (factory: EmbeddableFactory): PanelSelectionMenuItem => { - const icon = factory?.getIconType ? factory.getIconType() : 'empty'; - - return { - id: factory.type, - name: factory.getDisplayName(), - icon, - description: factory.getDescription?.(), - onClick: async () => { - closePopover(); - api.addNewPanel({ panelType: factory.type }, true); - }, - 'data-test-subj': `createNew-${factory.type}`, - order: factory.order ?? 0, - }; - }; - -const sortGroupPanelsByOrder = (panelGroups: T[]): T[] => { - return panelGroups.sort( - // larger number sorted to the top - (panelGroupA, panelGroupB) => panelGroupB.order - panelGroupA.order - ); -}; - -export const mergeGroupedItemsProvider = - (getEmbeddableFactoryMenuItem: GetEmbeddableFactoryMenuItem) => - ( - factoryGroupMap: Record, - groupedAddPanelAction: Record - ) => { - const panelGroups: GroupedAddPanelActions[] = []; - - new Set(Object.keys(factoryGroupMap).concat(Object.keys(groupedAddPanelAction))).forEach( - (groupId) => { - const dataTestSubj = `dashboardEditorMenu-${groupId}Group`; - - const factoryGroup = factoryGroupMap[groupId]; - const addPanelGroup = groupedAddPanelAction[groupId]; - - if (factoryGroup && addPanelGroup) { - panelGroups.push({ - id: factoryGroup.id, - title: factoryGroup.appName, - 'data-test-subj': dataTestSubj, - order: factoryGroup.order, - items: [ - ...factoryGroup.factories.map(getEmbeddableFactoryMenuItem), - ...(addPanelGroup?.items ?? []), - ], - }); - } else if (factoryGroup) { - panelGroups.push({ - id: factoryGroup.id, - title: factoryGroup.appName, - 'data-test-subj': dataTestSubj, - order: factoryGroup.order, - items: factoryGroup.factories.map(getEmbeddableFactoryMenuItem), - }); - } else if (addPanelGroup) { - panelGroups.push(addPanelGroup); - } - } - ); - - return panelGroups; - }; - -interface EditorMenuProps { - api: PresentationContainer; +interface EditorMenuProps + extends Pick[0], 'api' | 'createNewVisType'> { isDisabled?: boolean; - /** Handler for creating new visualization of a specified type */ - createNewVisType: (visType: BaseVisType | VisTypeAlias) => () => void; } export const EditorMenu = ({ createNewVisType, isDisabled, api }: EditorMenuProps) => { - const isMounted = useRef(false); - const flyoutRef = useRef>(); - const dashboard = useDashboardAPI(); - - useEffect(() => { - isMounted.current = true; - - return () => { - isMounted.current = false; - flyoutRef.current?.close(); - }; - }, []); + const dashboardAPI = useDashboardAPI(); const { - embeddable, - visualizations: { getAliases: getVisTypeAliases, getByGroup: getVisTypesByGroup }, - uiActions, + overlays, + analytics, + settings: { i18n: i18nStart, theme }, } = pluginServices.getServices(); - const [unwrappedEmbeddableFactories, setUnwrappedEmbeddableFactories] = useState< - UnwrappedEmbeddableFactory[] - >([]); - - const [addPanelActions, setAddPanelActions] = useState> | undefined>( - undefined - ); - - const embeddableFactories = useMemo( - () => Array.from(embeddable.getEmbeddableFactories()), - [embeddable] - ); - - useEffect(() => { - Promise.all( - embeddableFactories.map>(async (factory) => ({ - factory, - isEditable: await factory.isEditable(), - })) - ).then((factories) => { - setUnwrappedEmbeddableFactories(factories); - }); - }, [embeddableFactories]); - - const getSortedVisTypesByGroup = (group: VisGroups) => - getVisTypesByGroup(group) - .sort((a: BaseVisType | VisTypeAlias, b: BaseVisType | VisTypeAlias) => { - const labelA = 'titleInWizard' in a ? a.titleInWizard || a.title : a.title; - const labelB = 'titleInWizard' in b ? b.titleInWizard || a.title : a.title; - if (labelA < labelB) { - return -1; - } - if (labelA > labelB) { - return 1; - } - return 0; - }) - .filter(({ disableCreate }: BaseVisType) => !disableCreate); - - const promotedVisTypes = getSortedVisTypesByGroup(VisGroups.PROMOTED); - const toolVisTypes = getSortedVisTypesByGroup(VisGroups.TOOLS); - const legacyVisTypes = getSortedVisTypesByGroup(VisGroups.LEGACY); - - const visTypeAliases = getVisTypeAliases() - .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => - a === b ? 0 : a ? -1 : 1 - ) - .filter(({ disableCreate }: VisTypeAlias) => !disableCreate); - - const factories = unwrappedEmbeddableFactories.filter( - ({ isEditable, factory: { type, canCreateNew, isContainerType } }) => - isEditable && !isContainerType && canCreateNew() && type !== 'visualization' - ); - - const factoryGroupMap: Record = {}; - - // Retrieve ADD_PANEL_TRIGGER actions - useEffect(() => { - async function loadPanelActions() { - const registeredActions = await uiActions?.getTriggerCompatibleActions?.(ADD_PANEL_TRIGGER, { - embeddable: api, - }); - - if (isMounted.current) { - setAddPanelActions(registeredActions); - } - } - loadPanelActions(); - }, [uiActions, api]); - - factories.forEach(({ factory }) => { - const { grouping } = factory; - - if (grouping) { - grouping.forEach((group) => { - if (factoryGroupMap[group.id]) { - factoryGroupMap[group.id].factories.push(factory); - } else { - factoryGroupMap[group.id] = { - id: group.id, - appName: group.getDisplayName - ? group.getDisplayName({ embeddable: dashboard }) - : group.id, - icon: group.getIconType?.({ embeddable: dashboard }), - factories: [factory], - order: group.order ?? 0, - }; - } - }); - } else { - const fallbackGroup = COMMON_EMBEDDABLE_GROUPING.other; - - if (!factoryGroupMap[fallbackGroup.id]) { - factoryGroupMap[fallbackGroup.id] = { - id: fallbackGroup.id, - appName: fallbackGroup.getDisplayName - ? fallbackGroup.getDisplayName({ embeddable: dashboard }) - : fallbackGroup.id, - icon: fallbackGroup.getIconType?.({ embeddable: dashboard }) || 'empty', - factories: [], - order: fallbackGroup.order ?? 0, - }; - } - - factoryGroupMap[fallbackGroup.id].factories.push(factory); - } + const fetchDashboardPanels = useGetDashboardPanels({ + api, + createNewVisType, }); - const augmentedCreateNewVisType = ( - visType: Parameters[0], - cb: () => void - ) => { - const visClickHandler = createNewVisType(visType); + useEffect(() => { + // ensure opened dashboard is closed if a navigation event happens; return () => { - visClickHandler(); - cb(); + dashboardAPI.clearOverlays(); }; - }; - - const getVisTypeMenuItem = ( - onClickCb: () => void, - visType: BaseVisType - ): PanelSelectionMenuItem => { - const { - name, - title, - titleInWizard, - description, - icon = 'empty', - isDeprecated, - order, - } = visType; - return { - id: name, - name: titleInWizard || title, - isDeprecated, - icon, - onClick: augmentedCreateNewVisType(visType, onClickCb), - 'data-test-subj': `visType-${name}`, - description, - order, - }; - }; - - const getVisTypeAliasMenuItem = ( - onClickCb: () => void, - visTypeAlias: VisTypeAlias - ): PanelSelectionMenuItem => { - const { name, title, description, icon = 'empty', order } = visTypeAlias; - - return { - id: name, - name: title, - icon, - onClick: augmentedCreateNewVisType(visTypeAlias, onClickCb), - 'data-test-subj': `visType-${name}`, - description, - order: order ?? 0, - }; - }; - - const getEditorMenuPanels = (closeFlyout: () => void): GroupedAddPanelActions[] => { - const getEmbeddableFactoryMenuItem = getEmbeddableFactoryMenuItemProvider(api, closeFlyout); - - const groupedAddPanelAction = getAddPanelActionMenuItemsGroup( - api, - addPanelActions, - closeFlyout - ); - - const initialPanelGroups = mergeGroupedItemsProvider(getEmbeddableFactoryMenuItem)( - factoryGroupMap, - groupedAddPanelAction - ); - - // enhance panel groups - return sortGroupPanelsByOrder(initialPanelGroups).map((panelGroup) => { - switch (panelGroup.id) { - case 'visualizations': { - return { - ...panelGroup, - items: sortGroupPanelsByOrder( - (panelGroup.items ?? []).concat( - // TODO: actually add grouping to vis type alias so we wouldn't randomly display an unintended item - visTypeAliases.map(getVisTypeAliasMenuItem.bind(null, closeFlyout)), - promotedVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) - ) - ), - }; - } - case COMMON_EMBEDDABLE_GROUPING.legacy.id: { - return { - ...panelGroup, - items: sortGroupPanelsByOrder( - (panelGroup.items ?? []).concat( - legacyVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) - ) - ), - }; - } - case COMMON_EMBEDDABLE_GROUPING.annotation.id: { - return { - ...panelGroup, - items: sortGroupPanelsByOrder( - (panelGroup.items ?? []).concat( - toolVisTypes.map(getVisTypeMenuItem.bind(null, closeFlyout)) - ) - ), - }; - } - default: { - return { - ...panelGroup, - items: sortGroupPanelsByOrder(panelGroup.items), - }; - } - } - }); - }; + }, [dashboardAPI]); + + const openDashboardPanelSelectionFlyout = useCallback( + function openDashboardPanelSelectionFlyout() { + const flyoutPanelPaddingSize: ComponentProps< + typeof DashboardPanelSelectionListFlyout + >['paddingSize'] = 'l'; + + const mount = toMountPoint( + React.createElement(function () { + return ( + + ); + }), + { analytics, theme, i18n: i18nStart } + ); + + dashboardAPI.openOverlay( + overlays.openFlyout(mount, { + size: 'm', + maxWidth: 500, + paddingSize: flyoutPanelPaddingSize, + 'aria-labelledby': 'addPanelsFlyout', + 'data-test-subj': 'dashboardPanelSelectionFlyout', + onClose(overlayRef) { + dashboardAPI.clearOverlays(); + overlayRef.close(); + }, + }) + ); + }, + [analytics, theme, i18nStart, dashboardAPI, overlays, fetchDashboardPanels] + ); return ( { - flyoutRef.current = openDashboardPanelSelectionFlyout({ - getPanels: getEditorMenuPanels, - }); - }} + onClick={openDashboardPanelSelectionFlyout} size="s" /> ); diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx deleted file mode 100644 index 8bd8dffc67c97..0000000000000 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/open_dashboard_panel_selection_flyout.tsx +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useEffect, useState, useRef } from 'react'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { i18n as i18nFn } from '@kbn/i18n'; -import orderBy from 'lodash/orderBy'; -import { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiForm, - EuiBadge, - EuiFormRow, - EuiTitle, - EuiFieldSearch, - useEuiTheme, - type EuiFlyoutProps, - EuiListGroup, - EuiListGroupItem, - EuiToolTip, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { pluginServices } from '../../services/plugin_services'; -import type { DashboardServices } from '../../services/types'; -import type { GroupedAddPanelActions, PanelSelectionMenuItem } from './add_panel_action_menu_items'; - -interface OpenDashboardPanelSelectionFlyoutArgs { - getPanels: (closePopover: () => void) => GroupedAddPanelActions[]; - flyoutPanelPaddingSize?: Exclude; -} - -interface Props extends Pick { - /** Handler to close flyout */ - close: () => void; - /** Padding for flyout */ - paddingSize: Exclude; -} - -export function openDashboardPanelSelectionFlyout({ - getPanels, - flyoutPanelPaddingSize = 'l', -}: OpenDashboardPanelSelectionFlyoutArgs) { - const { - overlays, - analytics, - settings: { i18n, theme }, - } = pluginServices.getServices(); - // eslint-disable-next-line prefer-const - let flyoutRef: ReturnType; - - const mount = toMountPoint( - React.createElement(function () { - const closeFlyout = () => flyoutRef.close(); - return ( - - ); - }), - { analytics, theme, i18n } - ); - - flyoutRef = overlays.openFlyout(mount, { - size: 'm', - maxWidth: 500, - paddingSize: flyoutPanelPaddingSize, - 'aria-labelledby': 'addPanelsFlyout', - 'data-test-subj': 'dashboardPanelSelectionFlyout', - }); - - return flyoutRef; -} - -export const DashboardPanelSelectionListFlyout: React.FC = ({ - close, - getPanels, - paddingSize, -}) => { - const { euiTheme } = useEuiTheme(); - const panels = useRef(getPanels(close)); - const [searchTerm, setSearchTerm] = useState(''); - const [panelsSearchResult, setPanelsSearchResult] = useState( - panels.current - ); - - useEffect(() => { - if (!searchTerm) { - return setPanelsSearchResult(panels.current); - } - - const q = searchTerm.toLowerCase(); - - setPanelsSearchResult( - orderBy( - panels.current.map((panel) => { - const groupSearchMatch = panel.title.toLowerCase().includes(q); - - const [groupSearchMatchAgg, items] = panel.items.reduce( - (acc, cur) => { - const searchMatch = cur.name.toLowerCase().includes(q); - - acc[0] = acc[0] || searchMatch; - acc[1].push({ - ...cur, - isDisabled: !(groupSearchMatch || searchMatch), - }); - - return acc; - }, - [groupSearchMatch, [] as PanelSelectionMenuItem[]] - ); - - return { - ...panel, - isDisabled: !groupSearchMatchAgg, - items, - }; - }), - ['isDisabled'] - ) - ); - }, [searchTerm]); - - return ( - <> - - -

- -

-
-
- - - - - - { - setSearchTerm(e.target.value); - }} - aria-label={i18nFn.translate( - 'dashboard.editorMenu.addPanelFlyout.searchLabelText', - { defaultMessage: 'search field for panels' } - )} - className="nsPanelSelectionFlyout__searchInput" - data-test-subj="dashboardPanelSelectionFlyout__searchInput" - /> - - - - - - {panelsSearchResult.some(({ isDisabled }) => !isDisabled) ? ( - panelsSearchResult.map( - ({ id, title, items, isDisabled, ['data-test-subj']: dataTestSubj }) => - !isDisabled ? ( - - - {typeof title === 'string' ?

{title}

: title} -
- - {items?.map((item, idx) => { - return ( - - {!item.isDeprecated ? ( - {item.name} - ) : ( - - - {item.name} - - - - - - - - )} - - } - onClick={item?.onClick} - iconType={item.icon} - data-test-subj={item['data-test-subj']} - isDisabled={item.isDisabled} - /> - ); - })} - -
- ) : null - ) - ) : ( - - - - )} -
-
-
-
- - - - - - - - - - - ); -}; diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index 16b283f2b5c53..a279b33c8bc23 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -53,6 +53,9 @@ export class DashboardAddPanelService extends FtrService { this.log.debug('DashboardAddPanel.clickEditorMenuButton'); await this.testSubjects.click('dashboardEditorMenuButton'); await this.testSubjects.existOrFail('dashboardPanelSelectionFlyout'); + await this.retry.try(async () => { + return await this.testSubjects.exists('dashboardPanelSelectionList'); + }); } async expectEditorMenuClosed() { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ba615678c334e..df37dd9707b04 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -648,7 +648,13 @@ export class LensPlugin { ); // Displays the add ESQL panel in the dashboard add Panel menu - const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core); + const createESQLPanelAction = new CreateESQLPanelAction(startDependencies, core, async () => { + if (!this.editorFrameService) { + await this.initDependenciesForApi(); + } + + return this.editorFrameService!; + }); startDependencies.uiActions.addTriggerAction(ADD_PANEL_TRIGGER, createESQLPanelAction); const discoverLocator = startDependencies.share?.url.locators.get('DISCOVER_APP_LOCATOR'); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx index 97b59a93829e3..63844b1d6d3ea 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.test.tsx @@ -6,9 +6,10 @@ */ import type { CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; +import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; import type { LensPluginStartDependencies } from '../../plugin'; +import type { EditorFrameService } from '../../editor_frame_service'; import { createMockStartDependencies } from '../../editor_frame_service/mocks'; -import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks'; import { CreateESQLPanelAction } from './create_action'; describe('create Lens panel action', () => { @@ -16,9 +17,22 @@ describe('create Lens panel action', () => { const mockStartDependencies = createMockStartDependencies() as unknown as LensPluginStartDependencies; const mockPresentationContainer = getMockPresentationContainer(); + + const mockEditorFrameService = { + loadVisualizations: jest.fn(), + loadDatasources: jest.fn(), + } as unknown as EditorFrameService; + + const mockGetEditorFrameService = jest.fn(() => Promise.resolve(mockEditorFrameService)); + describe('compatibility check', () => { it('is incompatible if ui setting for ES|QL is off', async () => { - const configurablePanelAction = new CreateESQLPanelAction(mockStartDependencies, core); + const configurablePanelAction = new CreateESQLPanelAction( + mockStartDependencies, + core, + mockGetEditorFrameService + ); + const isCompatible = await configurablePanelAction.isCompatible({ embeddable: mockPresentationContainer, }); @@ -36,7 +50,13 @@ describe('create Lens panel action', () => { }, }, } as CoreStart; - const createESQLAction = new CreateESQLPanelAction(mockStartDependencies, updatedCore); + + const createESQLAction = new CreateESQLPanelAction( + mockStartDependencies, + updatedCore, + mockGetEditorFrameService + ); + const isCompatible = await createESQLAction.isCompatible({ embeddable: mockPresentationContainer, }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx index f1d58f9702fb4..6fb9310158082 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action.tsx @@ -11,6 +11,7 @@ import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { apiIsPresentationContainer } from '@kbn/presentation-containers'; import { COMMON_VISUALIZATION_GROUPING } from '@kbn/visualizations-plugin/public'; import type { LensPluginStartDependencies } from '../../plugin'; +import type { EditorFrameService } from '../../editor_frame_service'; const ACTION_CREATE_ESQL_CHART = 'ACTION_CREATE_ESQL_CHART'; @@ -25,7 +26,8 @@ export class CreateESQLPanelAction implements Action { constructor( protected readonly startDependencies: LensPluginStartDependencies, - protected readonly core: CoreStart + protected readonly core: CoreStart, + protected readonly getEditorFrameService: () => Promise ) {} public getDisplayName(): string { @@ -41,18 +43,21 @@ export class CreateESQLPanelAction implements Action { public async isCompatible({ embeddable }: EmbeddableApiContext) { if (!apiIsPresentationContainer(embeddable)) return false; - // compatible only when ES|QL advanced setting is enabled const { isCreateActionCompatible } = await getAsyncHelpers(); + return isCreateActionCompatible(this.core); } public async execute({ embeddable }: EmbeddableApiContext) { if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); const { executeCreateAction } = await getAsyncHelpers(); + const editorFrameService = await this.getEditorFrameService(); + executeCreateAction({ deps: this.startDependencies, core: this.core, api: embeddable, + editorFrameService, }); } } diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts index 65ac6ef69aee8..8768bc721480d 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/create_action_helpers.ts @@ -21,6 +21,7 @@ import { suggestionsApi } from '../../lens_suggestions_api'; import { generateId } from '../../id_generator'; import { executeEditAction } from './edit_action_helpers'; import { Embeddable } from '../../embeddable'; +import type { EditorFrameService } from '../../editor_frame_service'; // datasourceMap and visualizationMap setters/getters export const [getVisualizationMap, setVisualizationMap] = createGetterSetter< @@ -31,7 +32,7 @@ export const [getDatasourceMap, setDatasourceMap] = createGetterSetter< Record> >('DatasourceMap', false); -export function isCreateActionCompatible(core: CoreStart) { +export async function isCreateActionCompatible(core: CoreStart) { return core.uiSettings.get(ENABLE_ESQL); } @@ -39,13 +40,13 @@ export async function executeCreateAction({ deps, core, api, + editorFrameService, }: { deps: LensPluginStartDependencies; core: CoreStart; api: PresentationContainer; + editorFrameService: EditorFrameService; }) { - const isCompatibleAction = isCreateActionCompatible(core); - const getFallbackDataView = async () => { const indexName = await getIndexForESQLQuery({ dataViews: deps.dataViews }); if (!indexName) return null; @@ -53,13 +54,33 @@ export async function executeCreateAction({ return dataView; }; - const dataView = await getFallbackDataView(); + const [isCompatibleAction, dataView] = await Promise.all([ + isCreateActionCompatible(core), + getFallbackDataView(), + ]); if (!isCompatibleAction || !dataView) { throw new IncompatibleActionError(); } - const visualizationMap = getVisualizationMap(); - const datasourceMap = getDatasourceMap(); + + let visualizationMap = getVisualizationMap(); + let datasourceMap = getDatasourceMap(); + + if (!visualizationMap || !datasourceMap) { + [visualizationMap, datasourceMap] = await Promise.all([ + editorFrameService.loadVisualizations(), + editorFrameService.loadDatasources(), + ]); + + if (!visualizationMap && !datasourceMap) { + throw new IncompatibleActionError(); + } + + // persist for retrieval elsewhere + setDatasourceMap(datasourceMap); + setVisualizationMap(visualizationMap); + } + const defaultIndex = dataView.getIndexPattern(); const defaultEsqlQuery = { From 754de3be4f1f09e5868384c6dee76d3428159c2d Mon Sep 17 00:00:00 2001 From: Jedr Blaszyk Date: Fri, 26 Jul 2024 14:04:03 +0200 Subject: [PATCH 07/12] [Search/Connector] Make minute-level scheduling default option in ConnectorCronEditor (#189250) ## Summary Enable minute level scheduling in UI for all connector sync jobs. This fixes the bug with minute level scheduling not present for incremental sync jobs. It seems that during [this migration](https://github.com/elastic/kibana/commit/bb5ca2e187c6c302625c38ccd85ae1965c47fdbc#diff-328630e75e33bdb823bad243330681942ab5a391c5b83cad7f3186c4ca651e9dL219) the minute level scheduling for incremental syncs was lost somehow. As of now the `ConnectorCronEditor` is only used for connector sync jobs, all connector sync jobs support minute-level scheduling so I deleted the `MINUTE` from default `frequencyBlockList` in the component. Screenshot 2024-07-26 at 10 32 16 --- .../components/scheduling/connector_cron_editor.tsx | 2 +- .../components/scheduling/full_content.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx b/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx index 629f2b432fd11..d5c8572dfa595 100644 --- a/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx +++ b/packages/kbn-search-connectors/components/scheduling/connector_cron_editor.tsx @@ -26,7 +26,7 @@ interface ConnectorCronEditorProps { export const ConnectorCronEditor: React.FC = ({ dataTelemetryIdPrefix, disabled = false, - frequencyBlockList = ['MINUTE'], + frequencyBlockList = [], hasSyncTypeChanges, onReset, onSave, diff --git a/packages/kbn-search-connectors/components/scheduling/full_content.tsx b/packages/kbn-search-connectors/components/scheduling/full_content.tsx index 3a453605f82e2..76051923ff033 100644 --- a/packages/kbn-search-connectors/components/scheduling/full_content.tsx +++ b/packages/kbn-search-connectors/components/scheduling/full_content.tsx @@ -216,9 +216,6 @@ export const ConnectorContentScheduling: React.FC { setScheduling({ From 1457428d7f307ff6944349b407be177bba8f1929 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:09:05 +0200 Subject: [PATCH 08/12] [Search] Add index errors in Search Index page (#188682) ## Summary This adds an error callout to the index pages in Search if the mappings contain a semantic text field that references a non-existent inference ID, or an inference ID without a model that has started. --- .../components/search_index/index_error.tsx | 197 ++++++++++++++++++ .../components/search_index/search_index.tsx | 2 + .../ml_api_service/inference_models.ts | 13 ++ .../ml/server/routes/inference_models.ts | 37 ++++ 4 files changed, 249 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx new file mode 100644 index 0000000000000..ddfa1e32b0ba4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_error.tsx @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + InferenceServiceSettings, + MappingProperty, + MappingPropertyBase, + MappingTypeMapping, +} from '@elastic/elasticsearch/lib/api/types'; + +import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { LocalInferenceServiceSettings } from '@kbn/ml-trained-models-utils/src/constants/trained_models'; + +import { KibanaLogic } from '../../../shared/kibana'; +import { mappingsWithPropsApiLogic } from '../../api/mappings/mappings_logic'; + +export interface IndexErrorProps { + indexName: string; +} + +interface SemanticTextProperty extends MappingPropertyBase { + inference_id: string; + type: 'semantic_text'; +} + +const parseMapping = (mappings: MappingTypeMapping) => { + const fields = mappings.properties; + if (!fields) { + return []; + } + return getSemanticTextFields(fields, ''); +}; + +const getSemanticTextFields = ( + fields: Record, + path: string +): Array<{ path: string; source: SemanticTextProperty }> => { + return Object.entries(fields).flatMap(([key, value]) => { + const currentPath: string = path ? `${path}.${key}` : key; + const currentField: Array<{ path: string; source: SemanticTextProperty }> = + // @ts-expect-error because semantic_text type isn't incorporated in API type yet + value.type === 'semantic_text' ? [{ path: currentPath, source: value }] : []; + if (hasProperties(value)) { + const childSemanticTextFields: Array<{ path: string; source: SemanticTextProperty }> = + value.properties ? getSemanticTextFields(value.properties, currentPath) : []; + return [...currentField, ...childSemanticTextFields]; + } + return currentField; + }); +}; + +function hasProperties(field: MappingProperty): field is MappingPropertyBase { + return !!(field as MappingPropertyBase).properties; +} + +function isLocalModel(model: InferenceServiceSettings): model is LocalInferenceServiceSettings { + return Boolean((model as LocalInferenceServiceSettings).service_settings.model_id); +} + +export const IndexError: React.FC = ({ indexName }) => { + const { makeRequest: makeMappingRequest } = useActions(mappingsWithPropsApiLogic(indexName)); + const { data } = useValues(mappingsWithPropsApiLogic(indexName)); + const { ml } = useValues(KibanaLogic); + const [errors, setErrors] = useState< + Array<{ error: string; field: { path: string; source: SemanticTextProperty } }> + >([]); + + const [showErrors, setShowErrors] = useState(false); + + useEffect(() => { + makeMappingRequest({ indexName }); + }, [indexName]); + + useEffect(() => { + const mappings = data?.mappings; + if (!mappings || !ml) { + return; + } + + const semanticTextFields = parseMapping(mappings); + const fetchErrors = async () => { + const trainedModelStats = await ml?.mlApi?.trainedModels.getTrainedModelStats(); + const endpoints = await ml?.mlApi?.inferenceModels.getAllInferenceEndpoints(); + if (!trainedModelStats || !endpoints) { + return []; + } + + const semanticTextFieldsWithErrors = semanticTextFields + .map((field) => { + const model = endpoints.endpoints.find( + (endpoint) => endpoint.model_id === field.source.inference_id + ); + if (!model) { + return { + error: i18n.translate( + 'xpack.enterpriseSearch.indexOverview.indexErrors.missingModelError', + { + defaultMessage: 'Model not found for inference endpoint {inferenceId}', + values: { + inferenceId: field.source.inference_id as string, + }, + } + ), + field, + }; + } + if (isLocalModel(model)) { + const modelId = model.service_settings.model_id; + const modelStats = trainedModelStats?.trained_model_stats.find( + (value) => value.model_id === modelId + ); + if (!modelStats || modelStats.deployment_stats?.state !== 'started') { + return { + error: i18n.translate( + 'xpack.enterpriseSearch.indexOverview.indexErrors.missingModelError', + { + defaultMessage: + 'Model {modelId} for inference endpoint {inferenceId} in field {fieldName} has not been started', + values: { + fieldName: field.path, + inferenceId: field.source.inference_id as string, + modelId, + }, + } + ), + field, + }; + } + } + return { error: '', field }; + }) + .filter((value) => !!value.error); + setErrors(semanticTextFieldsWithErrors); + }; + + if (semanticTextFields.length) { + fetchErrors(); + } + }, [data]); + return errors.length > 0 ? ( + + {showErrors && ( + <> +

+ {i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.body', { + defaultMessage: 'Found errors in the following fields:', + })} + {errors.map(({ field, error }) => ( +

  • + {field.path}: {error} +
  • + ))} +

    + setShowErrors(false)} + > + {i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.hideErrorsLabel', { + defaultMessage: 'Hide full error', + })} + + + )} + {!showErrors && ( + setShowErrors(true)} + > + {i18n.translate('xpack.enterpriseSearch.indexOverview.indexErrors.showErrorsLabel', { + defaultMessage: 'Show full error', + })} + + )} +
    + ) : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx index 7c13a3f524ccf..538cc1c575fc9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/search_index.tsx @@ -40,6 +40,7 @@ import { CrawlerConfiguration } from './crawler/crawler_configuration/crawler_co import { SearchIndexDomainManagement } from './crawler/domain_management/domain_management'; import { NoConnectorRecord } from './crawler/no_connector_record'; import { SearchIndexDocuments } from './documents'; +import { IndexError } from './index_error'; import { SearchIndexIndexMappings } from './index_mappings'; import { IndexNameLogic } from './index_name_logic'; import { IndexViewLogic } from './index_view_logic'; @@ -239,6 +240,7 @@ export const SearchIndex: React.FC = () => { rightSideItems: getHeaderActions(index), }} > + ({ + path: `${ML_INTERNAL_BASE_PATH}/_inference/all`, + method: 'GET', + version: '1', + }); + return result; + }, }; } diff --git a/x-pack/plugins/ml/server/routes/inference_models.ts b/x-pack/plugins/ml/server/routes/inference_models.ts index 29f687ede932d..cb12d87e2b6fc 100644 --- a/x-pack/plugins/ml/server/routes/inference_models.ts +++ b/x-pack/plugins/ml/server/routes/inference_models.ts @@ -7,6 +7,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/server'; import { schema } from '@kbn/config-schema'; import type { InferenceModelConfig, InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; +import type { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; import type { RouteInitialization } from '../types'; import { createInferenceSchema } from './schemas/inference_schema'; import { modelsProvider } from '../models/model_management'; @@ -63,4 +64,40 @@ export function inferenceModelRoutes( } ) ); + /** + * @apiGroup TrainedModels + * + * @api {put} /internal/ml/_inference/:taskType/:inferenceId Create Inference Endpoint + * @apiName CreateInferenceEndpoint + * @apiDescription Create Inference Endpoint + */ + router.versioned + .get({ + path: `${ML_INTERNAL_BASE_PATH}/_inference/all`, + access: 'internal', + options: { + tags: ['access:ml:canGetTrainedModels'], + }, + }) + .addVersion( + { + version: '1', + validate: {}, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, response }) => { + try { + const body = await client.asCurrentUser.transport.request<{ + models: InferenceAPIConfigResponse[]; + }>({ + method: 'GET', + path: `/_inference/_all`, + }); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } From 1b84a248724b0924edafa02891d7f8911a44c84e Mon Sep 17 00:00:00 2001 From: Joe McElroy Date: Fri, 26 Jul 2024 13:14:49 +0100 Subject: [PATCH 09/12] [Search] [Playground] Enable Gemini Connector on ES3 (#189267) ## Summary Enable Gemini connector on ES3 search projects so playground can use. ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### 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) --- config/serverless.es.yml | 2 +- .../test/functional/page_objects/search_playground_page.ts | 7 +++++++ .../search/search_playground/playground_overview.ts | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 5dd773912c3a0..62e201955d9c8 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -46,7 +46,7 @@ telemetry.labels.serverless: search # Alerts and LLM config xpack.actions.enabledActionTypes: - ['.email', '.index', '.slack', '.jira', '.webhook', '.teams', '.gen-ai', '.bedrock'] + ['.email', '.index', '.slack', '.jira', '.webhook', '.teams', '.gen-ai', '.bedrock', '.gemini'] # Customize empty page state for analytics apps no_data_page.analyticsNoDataPageFlavor: 'serverless_search' diff --git a/x-pack/test/functional/page_objects/search_playground_page.ts b/x-pack/test/functional/page_objects/search_playground_page.ts index 97e53e87ed2f9..8e59c7cc3e37a 100644 --- a/x-pack/test/functional/page_objects/search_playground_page.ts +++ b/x-pack/test/functional/page_objects/search_playground_page.ts @@ -54,6 +54,13 @@ export function SearchPlaygroundPageProvider({ getService }: FtrProviderContext) await testSubjects.existOrFail('connectLLMButton'); }, + async expectPlaygroundLLMConnectorOptionsExists() { + await testSubjects.existOrFail('create-connector-flyout'); + await testSubjects.existOrFail('.gemini-card'); + await testSubjects.existOrFail('.bedrock-card'); + await testSubjects.existOrFail('.gen-ai-card'); + }, + async expectPlaygroundStartChatPageIndexButtonExists() { await testSubjects.existOrFail('createIndexButton'); }, diff --git a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts index e50e27c54fd01..44169d1e8f379 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_playground/playground_overview.ts @@ -203,5 +203,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('has embedded console', async () => { await testHasEmbeddedConsole(pageObjects); }); + + describe('connectors enabled on serverless search', () => { + it('has all LLM connectors', async () => { + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectOpenConnectorPagePlayground(); + await pageObjects.searchPlayground.PlaygroundStartChatPage.expectPlaygroundLLMConnectorOptionsExists(); + }); + }); }); } From e1f20116cbf88247dbfe38d1e9a0a9f9059998f9 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:33:14 +0100 Subject: [PATCH 10/12] [Search] Fix copy nit in semantic search guide (#189249) - We use "Start by" twice, removes the second instance - Tweaks inference endpoint copy --- .../semantic_search_guide/semantic_search_guide.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/semantic_search/components/semantic_search_guide/semantic_search_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/semantic_search/components/semantic_search_guide/semantic_search_guide.tsx index 1b373da7a9ff2..62d34115127b3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/semantic_search/components/semantic_search_guide/semantic_search_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/semantic_search/components/semantic_search_guide/semantic_search_guide.tsx @@ -236,7 +236,7 @@ export const SemanticSearchGuide: React.FC = () => {

    @@ -245,7 +245,7 @@ export const SemanticSearchGuide: React.FC = () => {

    @@ -293,7 +293,7 @@ export const SemanticSearchGuide: React.FC = () => {

    semantic_text }} />

    From 9bc57412bb84d9108e80acc62c4dc6d1753c5731 Mon Sep 17 00:00:00 2001 From: Jill Guyonnet Date: Fri, 26 Jul 2024 13:40:09 +0100 Subject: [PATCH 11/12] [Fleet] Fix namespaces property of created agent policies (#189199) ## Summary I found a small bug while working on https://github.com/elastic/kibana/issues/185040: when agent policies are created, there should be a root-level `namespaces` property, which is currently missing. `GET .fleet-policies/_mapping` contains a `namespaces` property with `keyword` type that was added in https://github.com/elastic/elasticsearch. Note: I was looking into removing the existing `data.namespaces` property, however I don't see any issues with it. It is coming from [here](https://github.com/nchaulet/kibana/blob/f77e4d243fca87a87eeae1409f27876cc7ea0836/x-pack/plugins/fleet/server/services/agent_policy.ts#L1140), i.e. the `data` property is generated from the full agent policy which already has a `namespaces` property. ### 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 --- x-pack/plugins/fleet/common/types/models/agent_policy.ts | 4 ++++ .../plugins/fleet/server/services/agent_policy.test.ts | 9 ++++++++- x-pack/plugins/fleet/server/services/agent_policy.ts | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index bc70714b79158..1468d2ac5b11e 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -193,6 +193,10 @@ export interface FleetServerPolicy { * The coordinator index of the policy */ coordinator_idx: number; + /** + * The namespaces of the policy + */ + namespaces?: string[]; /** * The opaque payload. */ diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index dd63d09c9d7c5..a5dbbc6b233b3 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -1230,6 +1230,7 @@ describe('Agent policy', () => { mockedGetFullAgentPolicy.mockResolvedValue({ id: 'policy123', revision: 1, + namespaces: ['mySpace'], inputs: [ { id: 'input-123', @@ -1282,10 +1283,16 @@ describe('Agent policy', () => { }), expect.objectContaining({ '@timestamp': expect.anything(), - data: { id: 'policy123', inputs: [{ id: 'input-123' }], revision: 1 }, + data: { + id: 'policy123', + inputs: [{ id: 'input-123' }], + revision: 1, + namespaces: ['mySpace'], + }, default_fleet_server: false, policy_id: 'policy123', revision_idx: 1, + namespaces: ['mySpace'], }), ], refresh: 'wait_for', diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 36a62af378c31..d243ef8b60e16 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -1137,6 +1137,7 @@ class AgentPolicyService { '@timestamp': new Date().toISOString(), revision_idx: fullPolicy.revision, coordinator_idx: 0, + namespaces: fullPolicy.namespaces, data: fullPolicy as unknown as FleetServerPolicy['data'], policy_id: fullPolicy.id, default_fleet_server: policy.is_default_fleet_server === true, From 218146ee693e6f8c3945d321bc494776dd7e6710 Mon Sep 17 00:00:00 2001 From: Maxim Palenov Date: Fri, 26 Jul 2024 14:59:17 +0200 Subject: [PATCH 12/12] [Security Solution] Auto-bundle Endpoint Management API OpenAPI specs (#188853) **Addresses**: https://github.com/elastic/kibana/issues/184428 ## Summary This PR adds scripts for automatic bundling of Endpoint Management API OpenAPI specs as a part of PR pipeline. Corresponding result bundles are automatically committed to the Security Solution plugin `x-pack/plugins/security_solution` in the `docs/openapi/ess/` and `docs/openapi/serverless` folders (similar to https://github.com/elastic/kibana/pull/186384). --- .../security_solution_openapi_bundling.sh | 5 + ...agement_api_2023_10_31.bundled.schema.yaml | 938 ++++++++++++++++++ ...agement_api_2023_10_31.bundled.schema.yaml | 866 ++++++++++++++++ x-pack/plugins/security_solution/package.json | 3 +- .../openapi/bundle_endpoint_management.js | 44 + 5 files changed, 1855 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml create mode 100644 x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml create mode 100644 x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js diff --git a/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh b/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh index b17a1589190a5..ba4411b7d5ef4 100755 --- a/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh +++ b/.buildkite/scripts/steps/openapi_bundling/security_solution_openapi_bundling.sh @@ -23,6 +23,11 @@ check_for_changed_files "yarn openapi:bundle:entity-analytics" true echo -e "\n[Security Solution OpenAPI Bundling] Lists API\n" +echo -e "\n[Security Solution OpenAPI Bundling] Endpoint Management API\n" + +(cd x-pack/plugins/security_solution && yarn openapi:bundle:endpoint-management) +check_for_changed_files "yarn openapi:bundle:endpoint-management" true + (cd packages/kbn-securitysolution-lists-common && yarn openapi:bundle) check_for_changed_files "yarn openapi:bundle" true diff --git a/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml new file mode 100644 index 0000000000000..22979d62e0933 --- /dev/null +++ b/x-pack/plugins/security_solution/docs/openapi/ess/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -0,0 +1,938 @@ +openapi: 3.0.3 +info: + description: Interact with and manage endpoints running the Elastic Defend integration. + title: Security Solution Endpoint Management API (Elastic Cloud and self-hosted) + version: '2023-10-31' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/endpoint/action: + get: + operationId: EndpointGetActionsList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/EndpointActionListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions List schema + '/api/endpoint/action_log/{agent_id}': + get: + operationId: EndpointGetActionAuditLog + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestQuery' + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get action audit log schema + /api/endpoint/action_status: + get: + operationId: EndpointGetActionsStatus + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agent_ids: + $ref: '#/components/schemas/AgentIds' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions status schema + '/api/endpoint/action/{action_id}': + get: + operationId: EndpointGetActionsDetails + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/DetailsRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action details schema + '/api/endpoint/action/{action_id}/file/{file_id}/download`': + get: + operationId: EndpointFileDownload + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileDownloadRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Download schema + '/api/endpoint/action/{action_id}/file/{file_id}`': + get: + operationId: EndpointFileInfo + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileInfoRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Info schema + /api/endpoint/action/execute: + post: + operationId: EndpointExecuteAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Execute Action + /api/endpoint/action/get_file: + post: + operationId: EndpointGetFileAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get File Action + /api/endpoint/action/isolate: + post: + operationId: EndpointIsolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Isolate host Action + /api/endpoint/action/kill_process: + post: + operationId: EndpointKillProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Kill process Action + /api/endpoint/action/running_procs: + post: + operationId: EndpointGetRunningProcessesAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Running Processes Action + /api/endpoint/action/scan: + post: + operationId: EndpointScanAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ScanActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Scan Action + /api/endpoint/action/state: + get: + operationId: EndpointGetActionsState + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action State schema + /api/endpoint/action/suspend_process: + post: + operationId: EndpointSuspendProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Suspend process Action + /api/endpoint/action/unisolate: + post: + operationId: EndpointUnisolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Unisolate host Action + /api/endpoint/action/upload: + post: + operationId: EndpointUploadAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FileUploadActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Upload Action + /api/endpoint/isolate: + post: + operationId: EndpointIsolateRedirect + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + '308': + description: Permanent Redirect + headers: + Location: + description: Permanently redirects to "/api/endpoint/action/isolate" + schema: + example: /api/endpoint/action/isolate + type: string + summary: Permanently redirects to a new location + /api/endpoint/metadata: + get: + operationId: GetEndpointMetadataList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/ListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata List schema + '/api/endpoint/metadata/{id}': + get: + operationId: GetEndpointMetadata + parameters: + - in: path + name: query + required: true + schema: + type: object + properties: + id: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata schema + /api/endpoint/metadata/transforms: + get: + operationId: GetEndpointMetadataTransform + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata Transform schema + /api/endpoint/policy_response: + get: + operationId: GetPolicyResponse + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agentId: + $ref: '#/components/schemas/AgentId' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Policy Response schema + /api/endpoint/policy/summaries: + get: + operationId: GetAgentPolicySummary + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + package_name: + type: string + policy_id: + nullable: true + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Agent Policy Summary schema + '/api/endpoint/protection_updates_note/{package_policy_id}': + get: + operationId: GetProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Get Protection Updates Note schema + post: + operationId: CreateUpdateProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + note: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Create Update Protection Updates Note schema + '/api/endpoint/suggestions/{suggestion_type}': + post: + operationId: GetEndpointSuggestions + parameters: + - in: path + name: suggestion_type + required: true + schema: + enum: + - eventFilters + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + field: + type: string + fieldMeta: {} + filters: {} + query: + type: string + required: + - parameters + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get suggestions + /api/endpoint/unisolate: + post: + operationId: EndpointUnisolateRedirect + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + '308': + description: Permanent Redirect + headers: + Location: + description: Permanently redirects to "/api/endpoint/action/unisolate" + schema: + example: /api/endpoint/action/unisolate + type: string + summary: Permanently redirects to a new location +components: + schemas: + AgentId: + description: Agent ID + type: string + AgentIds: + minLength: 1 + oneOf: + - items: + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + - minLength: 1 + type: string + AlertIds: + description: A list of alerts ids. + items: + $ref: '#/components/schemas/NonEmptyString' + minItems: 1 + type: array + AuditLogRequestParams: + type: object + properties: + agent_id: + $ref: '#/components/schemas/AgentId' + AuditLogRequestQuery: + type: object + properties: + end_date: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + page_size: + $ref: '#/components/schemas/PageSize' + start_date: + $ref: '#/components/schemas/StartDate' + CaseIds: + description: Case IDs to be updated (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + Command: + description: The command to be executed (cannot be an empty string) + enum: + - isolate + - unisolate + - kill-process + - suspend-process + - running-processes + - get-file + - execute + - upload + minLength: 1 + type: string + Commands: + items: + $ref: '#/components/schemas/Command' + type: array + Comment: + description: Optional comment + type: string + DetailsRequestParams: + type: object + properties: + action_id: + type: string + EndDate: + description: End date + type: string + EndpointActionListRequestQuery: + type: object + properties: + agentIds: + $ref: '#/components/schemas/AgentIds' + commands: + $ref: '#/components/schemas/Commands' + endDate: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + startDate: + $ref: '#/components/schemas/StartDate' + types: + $ref: '#/components/schemas/Types' + userIds: + $ref: '#/components/schemas/UserIds' + withOutputs: + $ref: '#/components/schemas/WithOutputs' + EndpointIds: + description: List of endpoint IDs (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + ExecuteActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + command: + $ref: '#/components/schemas/Command' + timeout: + $ref: '#/components/schemas/Timeout' + required: + - command + required: + - parameters + FileDownloadRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileInfoRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileUploadActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + file: + format: binary + type: string + parameters: + type: object + properties: + overwrite: + default: false + type: boolean + required: + - parameters + - file + GetFileActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + ListRequestQuery: + type: object + properties: + hostStatuses: + items: + enum: + - healthy + - offline + - updating + - inactive + - unenrolled + type: string + type: array + kuery: + nullable: true + type: string + page: + default: 0 + description: Page number + minimum: 0 + type: integer + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + sortDirection: + enum: + - asc + - desc + nullable: true + type: string + sortField: + enum: + - enrolled_at + - metadata.host.hostname + - host_status + - metadata.Endpoint.policy.applied.name + - metadata.Endpoint.policy.applied.status + - metadata.host.os.name + - metadata.host.ip + - metadata.agent.version + - last_checkin + type: string + required: + - hostStatuses + NonEmptyString: + description: A string that is not empty and does not contain only whitespace + minLength: 1 + pattern: ^(?! *$).+$ + type: string + Page: + default: 1 + description: Page number + minimum: 1 + type: integer + PageSize: + default: 10 + description: Number of items per page + maximum: 100 + minimum: 1 + type: integer + Parameters: + description: Optional parameters object + type: object + ProcessActionSchemas: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + oneOf: + - type: object + properties: + pid: + minimum: 1 + type: integer + - type: object + properties: + entity_id: + minLength: 1 + type: string + required: + - parameters + ProtectionUpdatesNoteResponse: + type: object + properties: + note: + type: string + ScanActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + StartDate: + description: Start date + type: string + SuccessResponse: + type: object + properties: {} + Timeout: + description: The maximum timeout value in milliseconds (optional) + minimum: 1 + type: integer + Type: + enum: + - automated + - manual + type: string + Types: + items: + $ref: '#/components/schemas/Type' + maxLength: 2 + minLength: 1 + type: array + UserIds: + description: User IDs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + WithOutputs: + description: With Outputs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml new file mode 100644 index 0000000000000..99627f8bd8a9e --- /dev/null +++ b/x-pack/plugins/security_solution/docs/openapi/serverless/security_solution_endpoint_management_api_2023_10_31.bundled.schema.yaml @@ -0,0 +1,866 @@ +openapi: 3.0.3 +info: + description: Interact with and manage endpoints running the Elastic Defend integration. + title: Security Solution Endpoint Management API (Elastic Cloud Serverless) + version: '2023-10-31' +servers: + - url: 'http://{kibana_host}:{port}' + variables: + kibana_host: + default: localhost + port: + default: '5601' +paths: + /api/endpoint/action: + get: + operationId: EndpointGetActionsList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/EndpointActionListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions List schema + '/api/endpoint/action_log/{agent_id}': + get: + operationId: EndpointGetActionAuditLog + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestQuery' + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/AuditLogRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get action audit log schema + /api/endpoint/action_status: + get: + operationId: EndpointGetActionsStatus + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agent_ids: + $ref: '#/components/schemas/AgentIds' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Actions status schema + '/api/endpoint/action/{action_id}': + get: + operationId: EndpointGetActionsDetails + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/DetailsRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action details schema + '/api/endpoint/action/{action_id}/file/{file_id}/download`': + get: + operationId: EndpointFileDownload + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileDownloadRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Download schema + '/api/endpoint/action/{action_id}/file/{file_id}`': + get: + operationId: EndpointFileInfo + parameters: + - in: path + name: query + required: true + schema: + $ref: '#/components/schemas/FileInfoRequestParams' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: File Info schema + /api/endpoint/action/execute: + post: + operationId: EndpointExecuteAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Execute Action + /api/endpoint/action/get_file: + post: + operationId: EndpointGetFileAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get File Action + /api/endpoint/action/isolate: + post: + operationId: EndpointIsolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Isolate host Action + /api/endpoint/action/kill_process: + post: + operationId: EndpointKillProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Kill process Action + /api/endpoint/action/running_procs: + post: + operationId: EndpointGetRunningProcessesAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Running Processes Action + /api/endpoint/action/scan: + post: + operationId: EndpointScanAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ScanActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Scan Action + /api/endpoint/action/state: + get: + operationId: EndpointGetActionsState + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Action State schema + /api/endpoint/action/suspend_process: + post: + operationId: EndpointSuspendProcessAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessActionSchemas' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Suspend process Action + /api/endpoint/action/unisolate: + post: + operationId: EndpointUnisolateHostAction + requestBody: + content: + application/json: + schema: + type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Unisolate host Action + /api/endpoint/action/upload: + post: + operationId: EndpointUploadAction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FileUploadActionRequestBody' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Upload Action + /api/endpoint/metadata: + get: + operationId: GetEndpointMetadataList + parameters: + - in: query + name: query + required: true + schema: + $ref: '#/components/schemas/ListRequestQuery' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata List schema + '/api/endpoint/metadata/{id}': + get: + operationId: GetEndpointMetadata + parameters: + - in: path + name: query + required: true + schema: + type: object + properties: + id: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata schema + /api/endpoint/metadata/transforms: + get: + operationId: GetEndpointMetadataTransform + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Metadata Transform schema + /api/endpoint/policy_response: + get: + operationId: GetPolicyResponse + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + agentId: + $ref: '#/components/schemas/AgentId' + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Policy Response schema + /api/endpoint/policy/summaries: + get: + operationId: GetAgentPolicySummary + parameters: + - in: query + name: query + required: true + schema: + type: object + properties: + package_name: + type: string + policy_id: + nullable: true + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get Agent Policy Summary schema + '/api/endpoint/protection_updates_note/{package_policy_id}': + get: + operationId: GetProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Get Protection Updates Note schema + post: + operationId: CreateUpdateProtectionUpdatesNote + parameters: + - in: path + name: package_policy_id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + note: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProtectionUpdatesNoteResponse' + description: OK + summary: Create Update Protection Updates Note schema + '/api/endpoint/suggestions/{suggestion_type}': + post: + operationId: GetEndpointSuggestions + parameters: + - in: path + name: suggestion_type + required: true + schema: + enum: + - eventFilters + type: string + requestBody: + content: + application/json: + schema: + type: object + properties: + field: + type: string + fieldMeta: {} + filters: {} + query: + type: string + required: + - parameters + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: OK + summary: Get suggestions +components: + schemas: + AgentId: + description: Agent ID + type: string + AgentIds: + minLength: 1 + oneOf: + - items: + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + - minLength: 1 + type: string + AlertIds: + description: A list of alerts ids. + items: + $ref: '#/components/schemas/NonEmptyString' + minItems: 1 + type: array + AuditLogRequestParams: + type: object + properties: + agent_id: + $ref: '#/components/schemas/AgentId' + AuditLogRequestQuery: + type: object + properties: + end_date: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + page_size: + $ref: '#/components/schemas/PageSize' + start_date: + $ref: '#/components/schemas/StartDate' + CaseIds: + description: Case IDs to be updated (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + Command: + description: The command to be executed (cannot be an empty string) + enum: + - isolate + - unisolate + - kill-process + - suspend-process + - running-processes + - get-file + - execute + - upload + minLength: 1 + type: string + Commands: + items: + $ref: '#/components/schemas/Command' + type: array + Comment: + description: Optional comment + type: string + DetailsRequestParams: + type: object + properties: + action_id: + type: string + EndDate: + description: End date + type: string + EndpointActionListRequestQuery: + type: object + properties: + agentIds: + $ref: '#/components/schemas/AgentIds' + commands: + $ref: '#/components/schemas/Commands' + endDate: + $ref: '#/components/schemas/EndDate' + page: + $ref: '#/components/schemas/Page' + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + startDate: + $ref: '#/components/schemas/StartDate' + types: + $ref: '#/components/schemas/Types' + userIds: + $ref: '#/components/schemas/UserIds' + withOutputs: + $ref: '#/components/schemas/WithOutputs' + EndpointIds: + description: List of endpoint IDs (cannot contain empty strings) + items: + minLength: 1 + type: string + minItems: 1 + type: array + ExecuteActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + command: + $ref: '#/components/schemas/Command' + timeout: + $ref: '#/components/schemas/Timeout' + required: + - command + required: + - parameters + FileDownloadRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileInfoRequestParams: + type: object + properties: + action_id: + type: string + file_id: + type: string + required: + - action_id + - file_id + FileUploadActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + file: + format: binary + type: string + parameters: + type: object + properties: + overwrite: + default: false + type: boolean + required: + - parameters + - file + GetFileActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + ListRequestQuery: + type: object + properties: + hostStatuses: + items: + enum: + - healthy + - offline + - updating + - inactive + - unenrolled + type: string + type: array + kuery: + nullable: true + type: string + page: + default: 0 + description: Page number + minimum: 0 + type: integer + pageSize: + default: 10 + description: Number of items per page + maximum: 10000 + minimum: 1 + type: integer + sortDirection: + enum: + - asc + - desc + nullable: true + type: string + sortField: + enum: + - enrolled_at + - metadata.host.hostname + - host_status + - metadata.Endpoint.policy.applied.name + - metadata.Endpoint.policy.applied.status + - metadata.host.os.name + - metadata.host.ip + - metadata.agent.version + - last_checkin + type: string + required: + - hostStatuses + NonEmptyString: + description: A string that is not empty and does not contain only whitespace + minLength: 1 + pattern: ^(?! *$).+$ + type: string + Page: + default: 1 + description: Page number + minimum: 1 + type: integer + PageSize: + default: 10 + description: Number of items per page + maximum: 100 + minimum: 1 + type: integer + Parameters: + description: Optional parameters object + type: object + ProcessActionSchemas: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + oneOf: + - type: object + properties: + pid: + minimum: 1 + type: integer + - type: object + properties: + entity_id: + minLength: 1 + type: string + required: + - parameters + ProtectionUpdatesNoteResponse: + type: object + properties: + note: + type: string + ScanActionRequestBody: + allOf: + - type: object + properties: + alert_ids: + $ref: '#/components/schemas/AlertIds' + case_ids: + $ref: '#/components/schemas/CaseIds' + comment: + $ref: '#/components/schemas/Comment' + endpoint_ids: + $ref: '#/components/schemas/EndpointIds' + parameters: + $ref: '#/components/schemas/Parameters' + - type: object + properties: + parameters: + type: object + properties: + path: + type: string + required: + - path + required: + - parameters + StartDate: + description: Start date + type: string + SuccessResponse: + type: object + properties: {} + Timeout: + description: The maximum timeout value in milliseconds (optional) + minimum: 1 + type: integer + Type: + enum: + - automated + - manual + type: string + Types: + items: + $ref: '#/components/schemas/Type' + maxLength: 2 + minLength: 1 + type: array + UserIds: + description: User IDs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + WithOutputs: + description: With Outputs + oneOf: + - items: + minLength: 1 + type: string + minItems: 1 + type: array + - minLength: 1 + type: string + securitySchemes: + BasicAuth: + scheme: basic + type: http +security: + - BasicAuth: [] +tags: ! '' diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index cd7eb82b11a92..98575c1f48c26 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -32,6 +32,7 @@ "openapi:generate:debug": "node --inspect-brk scripts/openapi/generate", "openapi:bundle:detections": "node scripts/openapi/bundle_detections", "openapi:bundle:timeline": "node scripts/openapi/bundle_timeline", - "openapi:bundle:entity-analytics": "node scripts/openapi/bundle_entity_analytics" + "openapi:bundle:entity-analytics": "node scripts/openapi/bundle_entity_analytics", + "openapi:bundle:endpoint-management": "node scripts/openapi/bundle_endpoint_management" } } diff --git a/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js new file mode 100644 index 0000000000000..d4d994b993057 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/bundle_endpoint_management.js @@ -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. + */ + +require('../../../../../src/setup_node_env'); +const { bundle } = require('@kbn/openapi-bundler'); +const { join, resolve } = require('path'); + +const ROOT = resolve(__dirname, '../..'); + +(async () => { + await bundle({ + sourceGlob: join(ROOT, 'common/api/endpoint/**/*.schema.yaml'), + outputFilePath: join( + ROOT, + 'docs/openapi/serverless/security_solution_endpoint_management_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['serverless'], + specInfo: { + title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)', + description: 'Interact with and manage endpoints running the Elastic Defend integration.', + }, + }, + }); + + await bundle({ + sourceGlob: join(ROOT, 'common/api/endpoint/**/*.schema.yaml'), + outputFilePath: join( + ROOT, + 'docs/openapi/ess/security_solution_endpoint_management_api_{version}.bundled.schema.yaml' + ), + options: { + includeLabels: ['ess'], + specInfo: { + title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)', + description: 'Interact with and manage endpoints running the Elastic Defend integration.', + }, + }, + }); +})();