diff --git a/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/bulk.json b/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/bulk.json index 863a6e6189d77..d63bba4943ed2 100644 --- a/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/bulk.json +++ b/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/bulk.json @@ -5,6 +5,7 @@ "filter_path": [], "human": "__flag__", "pretty": "__flag__", + "list_executed_pipelines": "__flag__", "pipeline": "", "refresh": [ "true", @@ -25,7 +26,8 @@ "all", "index-setting" ], - "require_alias": "__flag__" + "require_alias": "__flag__", + "require_data_stream": "__flag__" }, "methods": [ "POST", diff --git a/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json b/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json index a192a36c17057..84ab3b335ef23 100644 --- a/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json +++ b/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.get_trained_models.json @@ -16,6 +16,7 @@ "total_feature_importance", "definition_status" ], + "include_model_definition": "__flag__", "size": [ "100" ], diff --git a/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.put_job.json b/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.put_job.json index 34a86000a4306..682741fb85bdc 100644 --- a/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.put_job.json +++ b/src/platform/plugins/shared/console/server/lib/spec_definitions/json/generated/ml.put_job.json @@ -4,7 +4,17 @@ "error_trace": "__flag__", "filter_path": [], "human": "__flag__", - "pretty": "__flag__" + "pretty": "__flag__", + "allow_no_indices": "__flag__", + "expand_wildcards": [ + "all", + "open", + "closed", + "hidden", + "none" + ], + "ignore_throttled": "__flag__", + "ignore_unavailable": "__flag__" }, "methods": [ "PUT" diff --git a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts index 33363dea05e76..68d409abbbd9c 100644 --- a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.test.ts @@ -18,6 +18,8 @@ const commonEntityFields: EntityInstance = { } as EntityInstance['entity'], }; +type Entity = { [key: string]: any } & { entityIdentityFields: { [key: string]: string[] } }; + describe('EntityClient', () => { let entityClient: EntityClient; @@ -27,127 +29,103 @@ describe('EntityClient', () => { describe('asKqlFilter', () => { it('should return the kql filter', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, - service: { - name: 'my-service', - }, + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['service.name', 'service.environment'] }, + type: 'service', + ['service.name']: 'my-service', }; - const result = entityClient.asKqlFilter(entityLatest); + const result = entityClient.asKqlFilter({ entity }); expect(result).toEqual('service.name: "my-service"'); }); - it('should return the kql filter when an indentity field value contain special characters', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['host.name', 'foo.bar'], - }, - host: { - name: 'my-host:some-value:some-other-value', - }, + it('should return the kql filter when an identity field value contain special characters', () => { + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['host.name', 'foo.bar'] }, + type: 'service', + ['host.name']: 'my-host:some-value:some-other-value', }; - const result = entityClient.asKqlFilter(entityLatest); + const result = entityClient.asKqlFilter({ entity }); expect(result).toEqual('host.name: "my-host:some-value:some-other-value"'); }); - it('should return the kql filter when indentity_fields is composed by multiple fields', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, - service: { - name: 'my-service', - environment: 'staging', - }, + it('should return the kql filter when identity_fields is composed by multiple fields', () => { + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['service.name', 'service.environment'] }, + type: 'service', + ['service.name']: 'my-service', + ['service.environment']: 'staging', }; - const result = entityClient.asKqlFilter(entityLatest); + const result = entityClient.asKqlFilter({ entity }); expect(result).toEqual('(service.name: "my-service" AND service.environment: "staging")'); }); it('should ignore fields that are not present in the entity', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['host.name', 'foo.bar'], - }, - host: { - name: 'my-host', - }, + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['host.name', 'foo.bar'] }, + ['host.name']: 'my-host', }; - const result = entityClient.asKqlFilter(entityLatest); + const result = entityClient.asKqlFilter({ entity }); expect(result).toEqual('host.name: "my-host"'); }); }); describe('getIdentityFieldsValue', () => { it('should return identity fields values', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, - service: { - name: 'my-service', - }, + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['service.name', 'service.environment'] }, + type: 'service', + ['service.name']: 'my-service', }; - expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ + expect(entityClient.getIdentityFieldsValue({ entity })).toEqual({ 'service.name': 'my-service', }); }); - it('should return identity fields values when indentity_fields is composed by multiple fields', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, - service: { - name: 'my-service', - environment: 'staging', - }, + it('should return identity fields values when identity_fields is composed by multiple fields', () => { + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['service.name', 'service.environment'] }, + type: 'service', + ['service.name']: 'my-service', + ['service.environment']: 'staging', }; - expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ + expect(entityClient.getIdentityFieldsValue({ entity })).toEqual({ 'service.name': 'my-service', 'service.environment': 'staging', }); }); it('should return identity fields when field is in the root', () => { - const entityLatest: EntityInstance = { - entity: { - ...commonEntityFields.entity, - identity_fields: ['name'], - type: 'service', - }, + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: { source1: ['name'] }, + type: 'service', name: 'foo', }; - expect(entityClient.getIdentityFieldsValue(entityLatest)).toEqual({ + expect(entityClient.getIdentityFieldsValue({ entity })).toEqual({ name: 'foo', }); }); it('should throw an error when identity fields are missing', () => { - const entityLatest: EntityInstance = { - ...commonEntityFields, + const entity: Entity = { + ...commonEntityFields.entity, + entityIdentityFields: {}, }; - expect(() => entityClient.getIdentityFieldsValue(entityLatest)).toThrow( + expect(() => entityClient.getIdentityFieldsValue({ entity })).toThrow( 'Identity fields are missing' ); }); diff --git a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts index 43530b27df7f7..bff8521a8b33d 100644 --- a/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts +++ b/x-pack/platform/plugins/shared/entity_manager/public/lib/entity_client.ts @@ -13,8 +13,7 @@ import { isHttpFetchError, } from '@kbn/server-route-repository-client'; import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; -import type { EntityDefinition, EntityInstance, EntityMetadata } from '@kbn/entities-schema'; -import { castArray } from 'lodash'; +import type { EntityDefinition } from '@kbn/entities-schema'; import type { EntityDefinitionWithState } from '../../server/lib/entities/types'; import { DisableManagedEntityResponse, @@ -106,12 +105,12 @@ export class EntityClient { } } - asKqlFilter( - entityInstance: { - entity: Pick; - } & Required - ) { - const identityFieldsValue = this.getIdentityFieldsValue(entityInstance); + asKqlFilter({ + entity, + }: { + entity: { [key: string]: any } & { entityIdentityFields: { [key: string]: string[] } }; + }) { + const identityFieldsValue = this.getIdentityFieldsValue({ entity }); const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => { return nodeTypes.function.buildNode('is', identityField, `"${value}"`); @@ -124,26 +123,22 @@ export class EntityClient { return toKqlExpression(kqlExpression); } - getIdentityFieldsValue( - entityInstance: { - entity: Pick; - } & Required - ) { - const { identity_fields: identityFields } = entityInstance.entity; - - if (!identityFields) { + getIdentityFieldsValue({ + entity, + }: { + entity: { [key: string]: any } & { entityIdentityFields: { [key: string]: string[] } }; + }): Record { + const { entityIdentityFields: identityFields } = entity; + if (!Object.keys(identityFields || {}).length) { throw new Error('Identity fields are missing'); } - return castArray(identityFields).reduce((acc, field) => { - const value = field.split('.').reduce((obj: any, part: string) => { - return obj && typeof obj === 'object' ? (obj as Record)[part] : undefined; - }, entityInstance); - - if (value) { - acc[field] = value; - } - + return Object.values(identityFields).reduce((acc: Record, fields) => { + fields.forEach((field) => { + if (entity?.[field]) { + acc[field] = entity[field]; + } + }); return acc; }, {} as Record); } diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.test.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.test.ts new file mode 100644 index 0000000000000..e377230b38671 --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright 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 { EntityV2 } from '@kbn/entities-schema'; +import { readSourceDefinitions } from './source_definition'; +import { loggerMock } from '@kbn/logging-mocks'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import type { EntitySourceDefinition } from '../types'; +import { UnknownEntityType } from '../errors/unknown_entity_type'; +import { identityFieldsBySource } from './identity_fields_by_source'; + +const readSourceDefinitionsMock = readSourceDefinitions as jest.Mock; +jest.mock('./source_definition', () => ({ + readSourceDefinitions: jest.fn(), +})); +const esClientMock = elasticsearchServiceMock.createClusterClient(); +const logger = loggerMock.create(); + +describe('identityFieldsBySource', () => { + it('throws if no sources are found for the type', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + }; + + const sources: EntitySourceDefinition[] = []; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect( + identityFieldsBySource(instance['entity.type'], esClientMock, logger) + ).rejects.toThrowError(UnknownEntityType); + }); + + it('returns the correct identity fields with a single source', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect( + identityFieldsBySource(instance['entity.type'], esClientMock, logger) + ).resolves.toEqual({ + my_source: ['host.name'], + }); + }); + + it('returns the correct identity fields with multiple sources', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source_host', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + { + id: 'my_source_os', + type_id: 'my_type', + identity_fields: ['host.os'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect( + identityFieldsBySource(instance['entity.type'], esClientMock, logger) + ).resolves.toEqual({ + my_source_host: ['host.name'], + my_source_os: ['host.os'], + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.ts new file mode 100644 index 0000000000000..786e65b0f1717 --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/identity_fields_by_source.ts @@ -0,0 +1,36 @@ +/* + * Copyright 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 { EntityV2 } from '@kbn/entities-schema'; +import { Logger } from '@kbn/core/server'; +import { readSourceDefinitions } from './source_definition'; +import { InternalClusterClient } from '../types'; +import { UnknownEntityType } from '../errors/unknown_entity_type'; + +export async function identityFieldsBySource( + type: EntityV2['entity.type'], + clusterClient: InternalClusterClient, + logger: Logger +) { + const sources = await readSourceDefinitions(clusterClient, logger, { + type, + }); + + if (sources.length === 0) { + throw new UnknownEntityType(`No sources found for type ${type}`); + } + + const identityFields: { [key: string]: string[] } = {}; + + sources.forEach((source) => { + const { id, identity_fields: fields } = source; + + identityFields[id] = fields; + }); + + return identityFields; +} diff --git a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts index 0d1463dde1318..8e19aa8821fc7 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts +++ b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts @@ -40,6 +40,7 @@ import { installBuiltInDefinitions } from './lib/v2/definitions/install_built_in import { disableManagedEntityDiscovery } from './lib/entities/uninstall_entity_definition'; import { installEntityManagerTemplates } from './lib/manage_index_templates'; import { instanceAsFilter } from './lib/v2/definitions/instance_as_filter'; +import { identityFieldsBySource } from './lib/v2/definitions/identity_fields_by_source'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface EntityManagerServerPluginSetup {} @@ -47,6 +48,7 @@ export interface EntityManagerServerPluginStart { getScopedClient: (options: { request: KibanaRequest }) => Promise; v2: { instanceAsFilter: typeof instanceAsFilter; + identityFieldsBySource: typeof identityFieldsBySource; }; } @@ -194,6 +196,7 @@ export class EntityManagerServerPlugin }, v2: { instanceAsFilter, + identityFieldsBySource, }, }; } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx index e944277f26ca6..c2c82da25a28e 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/service_map/index.tsx @@ -110,7 +110,7 @@ export function ServiceMap({ const { onPageReady } = usePerformanceContext(); const { - data = { elements: [] }, + data = { elements: [], nodesCount: 0, tracesCount: 0 }, status, error, } = useFetcher( @@ -187,6 +187,12 @@ export function ServiceMap({ if (status === FETCH_STATUS.SUCCESS) { onPageReady({ + customMetrics: { + key1: 'num_of_nodes', + value1: data.nodesCount, + key2: 'num_of_traces', + value2: data.tracesCount, + }, meta: { rangeFrom: start, rangeTo: end }, }); } diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts index debc88d3ddefd..901d6572f0ca5 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/get_service_map.ts @@ -32,6 +32,11 @@ export interface IEnvOptions { kuery?: string; } +export interface ServiceMapTelemetry { + tracesCount: number; + nodesCount: number; +} + async function getConnectionData({ config, apmEventClient, @@ -63,6 +68,8 @@ async function getConnectionData({ const init = { connections: [], discoveredServices: [], + tracesCount: 0, + servicesCount: 0, }; if (!traceIds.length) { @@ -99,16 +106,17 @@ async function getConnectionData({ logger.debug('Merged responses'); - return mergedResponses; + return { ...mergedResponses, tracesCount: traceIds.length }; }); } export type ConnectionsResponse = Awaited>; export type ServicesResponse = Awaited>; +export type ServiceMapResponse = TransformServiceMapResponse & ServiceMapTelemetry; export function getServiceMap( options: IEnvOptions & { maxNumberOfServices: number } -): Promise { +): Promise { return withApmSpan('get_service_map', async () => { const { logger } = options; const anomaliesPromise = getServiceAnomalies( @@ -137,8 +145,10 @@ export function getServiceMap( }, }); - logger.debug('Transformed service map response'); - - return transformedResponse; + return { + ...transformedResponse, + tracesCount: connectionData.tracesCount, + nodesCount: transformedResponse.nodesCount, + }; }); } diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts index 53deaba0864e1..be417b04c0e1d 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/group_resource_nodes.ts @@ -39,6 +39,7 @@ interface GroupedEdge { export interface GroupResourceNodesResponse { elements: Array; + nodesCount: number; } export function groupResourceNodes(responseData: { @@ -151,5 +152,6 @@ export function groupResourceNodes(responseData: { return { elements: [...ungroupedNodes, ...groupedNodes, ...ungroupedEdges, ...groupedEdges], + nodesCount: ungroupedNodes.length, }; } diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts index 100e65fb62d13..78a71d943f09d 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/route.ts @@ -23,7 +23,7 @@ import { environmentRt, rangeRt, kueryRt } from '../default_api_types'; import { getServiceGroup } from '../service_groups/get_service_group'; import { offsetRt } from '../../../common/comparison_rt'; import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; -import type { TransformServiceMapResponse } from './transform_service_map_responses'; +import type { ServiceMapResponse } from './get_service_map'; const serviceMapRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/service-map', @@ -39,7 +39,7 @@ const serviceMapRoute = createApmServerRoute({ ]), }), security: { authz: { requiredPrivileges: ['apm'] } }, - handler: async (resources): Promise => { + handler: async (resources): Promise => { const { config, context, params, logger } = resources; if (!config.serviceMapEnabled) { throw Boom.notFound(); diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts index 26b203d945eed..4bc0519217ab1 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/service_map/transform_service_map_responses.test.ts @@ -73,6 +73,7 @@ describe('transformServiceMapResponses', () => { }, ], anomalies, + tracesCount: 10, }; const { elements } = transformServiceMapResponses({ response }); @@ -106,6 +107,7 @@ describe('transformServiceMapResponses', () => { }, ], anomalies, + tracesCount: 10, }; const { elements } = transformServiceMapResponses({ response }); @@ -165,6 +167,7 @@ describe('transformServiceMapResponses', () => { }, ], anomalies, + tracesCount: 10, }; const { elements } = transformServiceMapResponses({ response }); @@ -203,6 +206,7 @@ describe('transformServiceMapResponses', () => { }, ], anomalies, + tracesCount: 10, }; const { elements } = transformServiceMapResponses({ response }); @@ -228,6 +232,7 @@ describe('transformServiceMapResponses', () => { }, ], anomalies, + tracesCount: 10, }; const { elements } = transformServiceMapResponses({ response }); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index ac782beb012d4..310ff912d503a 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -17,10 +17,13 @@ import { useHostCountContext } from '../hooks/use_host_count'; import { FlyoutWrapper } from './host_details_flyout/flyout_wrapper'; import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../constants'; import { FilterAction } from './table/filter_action'; +import { useUnifiedSearchContext } from '../hooks/use_unified_search'; export const HostsTable = () => { const { loading } = useHostsViewContext(); - const { loading: hostCountLoading } = useHostCountContext(); + const { loading: hostCountLoading, count } = useHostCountContext(); + const { searchCriteria } = useUnifiedSearchContext(); + const { onPageReady } = usePerformanceContext(); const { @@ -40,9 +43,16 @@ export const HostsTable = () => { useEffect(() => { if (!loading && !hostCountLoading) { - onPageReady(); + onPageReady({ + customMetrics: { + key1: 'num_of_hosts', + value1: count, + key2: `max_hosts_per_page`, + value2: searchCriteria.limit, + }, + }); } - }, [loading, hostCountLoading, onPageReady]); + }, [loading, hostCountLoading, onPageReady, count, searchCriteria]); return ( <> diff --git a/x-pack/solutions/observability/plugins/inventory/common/entities.ts b/x-pack/solutions/observability/plugins/inventory/common/entities.ts index b006fa0c7f6d8..8a22f12279c7d 100644 --- a/x-pack/solutions/observability/plugins/inventory/common/entities.ts +++ b/x-pack/solutions/observability/plugins/inventory/common/entities.ts @@ -35,11 +35,8 @@ export type EntityGroup = { export type InventoryEntity = { entityId: string; entityType: string; - entityIdentityFields: string | string[]; entityDisplayName: string; - entityDefinitionId: string; entityLastSeenTimestamp: string; - entityDefinitionVersion: string; - entitySchemaVersion: string; + entityIdentityFields: Record; alertsCount?: number; } & EntityMetadata; diff --git a/x-pack/solutions/observability/plugins/inventory/common/utils/check_entity_type.ts b/x-pack/solutions/observability/plugins/inventory/common/utils/check_entity_type.ts new file mode 100644 index 0000000000000..168683e342693 --- /dev/null +++ b/x-pack/solutions/observability/plugins/inventory/common/utils/check_entity_type.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 type { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import type { InventoryEntity } from '../entities'; + +export const isBuiltinEntityOfType = ( + type: (typeof BUILT_IN_ENTITY_TYPES)[keyof typeof BUILT_IN_ENTITY_TYPES], + entity: InventoryEntity +): boolean => { + return entity.entityType === type; +}; diff --git a/x-pack/solutions/observability/plugins/inventory/common/utils/entity_type_guards.ts b/x-pack/solutions/observability/plugins/inventory/common/utils/entity_type_guards.ts deleted file mode 100644 index f9ace49b20d3a..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/common/utils/entity_type_guards.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AgentName } from '@kbn/elastic-agent-utils'; -import type { InventoryEntity } from '../entities'; - -interface BuiltinEntityMap { - host: InventoryEntity & { cloud?: { provider?: string[] } }; - container: InventoryEntity & { cloud?: { provider?: string[] } }; - service: InventoryEntity & { - agent?: { name: AgentName[] }; - service?: { environment?: string | string[] | null }; - }; -} - -export const isBuiltinEntityOfType = ( - type: T, - entity: InventoryEntity -): entity is BuiltinEntityMap[T] => { - return entity.entityType === type; -}; diff --git a/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts index e86c673c2a6b7..aa1fce49d221d 100644 --- a/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/solutions/observability/plugins/inventory/e2e/cypress/e2e/home.cy.ts @@ -87,13 +87,13 @@ describe.skip('Home page', () => { cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); cy.contains('host'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_host').click(); cy.wait('@getEntities'); cy.contains('service'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_service').click(); cy.wait('@getEntities'); cy.contains('container'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_container').click(); cy.wait('@getEntities'); cy.contains('server1'); cy.contains('synth-node-trace-logs'); @@ -151,21 +151,19 @@ describe.skip('Home page', () => { }).as('getEEMStatus'); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); - cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEntitiesTypes'); cy.wait('@getEEMStatus'); - cy.getByTestSubj('entityTypes_multiSelect_filter').click(); - cy.getByTestSubj('entityTypes_multiSelect_filter_selection_service').click(); - cy.wait('@getGroups'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click(); + cy.getByTestSubj('entityType_multiSelect_filter').click(); + cy.getByTestSubj('entityType_multiSelect_filter_selection_service').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_service').click(); cy.wait('@getEntities'); cy.get('server1').should('not.exist'); cy.contains('synth-node-trace-logs'); cy.contains('foo').should('not.exist'); - cy.getByTestSubj('entityTypes_multiSelect_filter').click(); - cy.getByTestSubj('entityTypes_multiSelect_filter_selection_service').click(); - cy.getByTestSubj('inventoryGroupTitle_entity.type_service').should('not.exist'); + cy.getByTestSubj('entityType_multiSelect_filter').click(); + cy.getByTestSubj('entityType_multiSelect_filter_selection_service').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_service').should('not.exist'); }); it('Filters entities by host type', () => { @@ -174,21 +172,19 @@ describe.skip('Home page', () => { }).as('getEEMStatus'); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); - cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEntitiesTypes'); cy.wait('@getEEMStatus'); - cy.getByTestSubj('entityTypes_multiSelect_filter').click(); - cy.getByTestSubj('entityTypes_multiSelect_filter_selection_host').click(); - cy.wait('@getGroups'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click(); + cy.getByTestSubj('entityType_multiSelect_filter').click(); + cy.getByTestSubj('entityType_multiSelect_filter_selection_host').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_host').click(); cy.wait('@getEntities'); cy.contains('server1'); cy.contains('synth-node-trace-logs').should('not.exist'); cy.contains('foo').should('not.exist'); - cy.getByTestSubj('entityTypes_multiSelect_filter').click(); - cy.getByTestSubj('entityTypes_multiSelect_filter_selection_host').click(); - cy.getByTestSubj('inventoryGroupTitle_entity.type_host').should('not.exist'); + cy.getByTestSubj('entityType_multiSelect_filter').click(); + cy.getByTestSubj('entityType_multiSelect_filter_selection_host').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_host').should('not.exist'); }); it('Filters entities by container type', () => { @@ -197,21 +193,19 @@ describe.skip('Home page', () => { }).as('getEEMStatus'); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); - cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEntitiesTypes'); cy.wait('@getEEMStatus'); - cy.getByTestSubj('entityTypes_multiSelect_filter').click(); - cy.getByTestSubj('entityTypes_multiSelect_filter_selection_container').click(); - cy.wait('@getGroups'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click(); + cy.getByTestSubj('entityType_multiSelect_filter').click(); + cy.getByTestSubj('entityType_multiSelect_filter_selection_container').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_container').click(); cy.wait('@getEntities'); cy.contains('server1').should('not.exist'); cy.contains('synth-node-trace-logs').should('not.exist'); cy.contains('foo'); - cy.getByTestSubj('entityTypes_multiSelect_filter').click(); - cy.getByTestSubj('entityTypes_multiSelect_filter_selection_container').click(); - cy.getByTestSubj('inventoryGroupTitle_entity.type_container').should('not.exist'); + cy.getByTestSubj('entityType_multiSelect_filter').click(); + cy.getByTestSubj('entityType_multiSelect_filter_selection_container').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_container').should('not.exist'); }); it('Navigates to discover with actions button in the entities list', () => { @@ -222,7 +216,7 @@ describe.skip('Home page', () => { cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); cy.contains('container'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click(); + cy.getByTestSubj('inventoryGroupTitle_entityType_container').click(); cy.wait('@getEntities'); // cy.getByTestSubj('inventoryEntityActionsButton').click(); cy.getByTestSubj('inventoryEntityActionsButton-foo').click(); diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx index 0957fa4da8aea..cfdf6fee83170 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.test.tsx @@ -47,17 +47,12 @@ describe('AlertsBadge', () => { it('render alerts badge for a host entity', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), - entityType: 'host', + entityType: 'built_in_hosts_from_ecs_data', entityDisplayName: 'foo', - entityIdentityFields: 'host.name', - entityDefinitionId: 'host', + entityIdentityFields: { source1: ['host.name'] }, alertsCount: 1, - host: { - name: 'foo', - }, - cloud: { - provider: null, - }, + 'host.name': 'foo', + 'cloud.provider': null, }; mockAsKqlFilter.mockReturnValue('host.name: "foo"'); @@ -70,20 +65,12 @@ describe('AlertsBadge', () => { it('render alerts badge for a service entity', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), - entityType: 'service', + entityType: 'built_in_services_from_ecs_data', entityDisplayName: 'foo', - entityIdentityFields: 'service.name', - entityDefinitionId: 'service', - service: { - name: 'bar', - }, - agent: { - name: 'node', - }, - cloud: { - provider: null, - }, - + entityIdentityFields: { source1: ['service.name'] }, + 'service.name': 'bar', + 'agent.name': 'node', + 'cloud.provider': null, alertsCount: 5, }; mockAsKqlFilter.mockReturnValue('service.name: "bar"'); @@ -97,20 +84,13 @@ describe('AlertsBadge', () => { it('render alerts badge for a service entity with multiple identity fields', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), - entityType: 'service', + entityType: 'built_in_services_from_ecs_data', entityDisplayName: 'foo', - entityIdentityFields: ['service.name', 'service.environment'], - entityDefinitionId: 'service', - service: { - name: 'bar', - environment: 'prod', - }, - agent: { - name: 'node', - }, - cloud: { - provider: null, - }, + entityIdentityFields: { source1: ['service.name', 'service.environment'] }, + 'service.name': 'bar', + 'service.environment': 'prod', + 'agent.name': 'node', + 'cloud.provider': null, alertsCount: 2, }; diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx index 301dcb63d1a17..b8376b34b9975 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -22,10 +22,7 @@ export function AlertsBadge({ entity }: { entity: InventoryEntity }) { const activeAlertsHref = basePath.prepend( `/app/observability/alerts?_a=${rison.encode({ kuery: entityManager.entityClient.asKqlFilter({ - entity: { - identity_fields: entity.entityIdentityFields, - }, - ...entity, + entity, }), status: 'active', })}` diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx index 29a862646c4c4..b5e0506d6d72e 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -21,7 +21,7 @@ describe('EntityName', () => { entityId: '1', entityType: 'service', entityDisplayName: 'entity_name', - entityIdentityFields: ['service.name', 'service.environment'], + entityIdentityFields: { source1: ['service.name', 'service.environment'] }, entityDefinitionId: 'entity_definition_id', entitySchemaVersion: '1', entityDefinitionVersion: '1', diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx index bf97d5b4f1c38..7ad14e66153e4 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/index.tsx @@ -75,7 +75,7 @@ export function EntitiesGrid({ } const columnEntityTableId = columnId as EntityColumnIds; - const entityType = entity.entityType; + const { entityType } = entity; switch (columnEntityTableId) { case 'alertsCount': diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts index 1048b18f82e91..6245cd66b25e4 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entities_grid/mock/entities_mock.ts @@ -41,7 +41,7 @@ const getEntityLatest = ( entityId: generateId(), entityDefinitionId: faker.string.uuid(), entityDefinitionVersion: '1.0.0', - entityIdentityFields: indentityFieldsPerType[entityType], + entityIdentityFields: { source1: indentityFieldsPerType[entityType] }, entitySchemaVersion: '1.0.0', ...overrides, }); @@ -62,11 +62,11 @@ const alertsMock: InventoryEntity[] = [ ]; const hostsMock = Array.from({ length: 20 }, () => - getEntityLatest('host', { cloud: { provider: 'gcp' } }) + getEntityLatest('host', { 'cloud.provider': 'gcp' }) ); const containersMock = Array.from({ length: 20 }, () => getEntityLatest('container')); const servicesMock = Array.from({ length: 20 }, () => - getEntityLatest('service', { agent: { name: 'java' } }) + getEntityLatest('service', { 'agent.name': 'java' }) ); export const entitiesMock = [ diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx index 747124808df2e..bf41fb9766761 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/entity_group_accordion.test.tsx @@ -12,15 +12,16 @@ import { EntityGroupAccordion } from '.'; describe('EntityGroupAccordion', () => { it('renders with correct values', () => { const props = { - groupBy: 'entity.type', groups: [ { count: 5999, - 'entity.type': 'host', + 'entity.type': 'built_in_hosts_from_ecs_data', + label: 'Hosts', }, { count: 2001, - 'entity.type': 'service', + 'entity.type': 'built_in_services_from_ecs_data', + label: 'Services', }, ], }; @@ -28,11 +29,13 @@ describe('EntityGroupAccordion', () => { ); - expect(screen.getByText(props.groups[0]['entity.type'])).toBeInTheDocument(); - const container = screen.getByTestId('entityCountBadge_entity.type_host'); + expect(screen.getByText(props.groups[0].label)).toBeInTheDocument(); + const container = screen.getByTestId( + 'entityCountBadge_entityType_built_in_hosts_from_ecs_data' + ); expect(within(container).getByText('Entities:')).toBeInTheDocument(); expect(within(container).getByText(props.groups[0].count)).toBeInTheDocument(); }); diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx index c2f280cb05912..71965a817c273 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/grouped_entities_grid.tsx @@ -49,7 +49,7 @@ export function GroupedEntitiesGrid({ groupValue }: Props) { sortDirection, sortField, kuery, - entityTypes: groupValue?.length ? JSON.stringify([groupValue]) : undefined, + entityType: groupValue, }, }, signal, diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx index fa365625474b0..d1f95e794ea1b 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_group_accordion/index.tsx @@ -17,13 +17,13 @@ const ENTITIES_COUNT_BADGE = i18n.translate( ); export interface Props { - groupBy: string; groupValue: string; + groupLabel: string; groupCount: number; isLoading?: boolean; } -export function EntityGroupAccordion({ groupBy, groupValue, groupCount, isLoading }: Props) { +export function EntityGroupAccordion({ groupValue, groupLabel, groupCount, isLoading }: Props) { const { euiTheme } = useEuiTheme(); const [open, setOpen] = useState(false); @@ -41,17 +41,17 @@ export function EntityGroupAccordion({ groupBy, groupValue, groupCount, isLoadin `} > -

{groupValue}

+

{groupLabel}

} buttonElement="div" extraAction={ diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx index 239d441b5d4e6..73cd654eafd05 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/entity_icon/index.tsx @@ -10,8 +10,10 @@ import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-ic import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import { castArray } from 'lodash'; +import { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import type { AgentName } from '@kbn/elastic-agent-utils'; import type { InventoryEntity } from '../../../common/entities'; -import { isBuiltinEntityOfType } from '../../../common/utils/entity_type_guards'; +import { isBuiltinEntityOfType } from '../../../common/utils/check_entity_type'; interface EntityIconProps { entity: InventoryEntity; @@ -20,8 +22,11 @@ interface EntityIconProps { export function EntityIcon({ entity }: EntityIconProps) { const defaultIconSize = euiThemeVars.euiSizeL; - if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) { - const cloudProvider = castArray(entity.cloud?.provider)[0]; + if ( + isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.HOST_V2, entity) || + isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.CONTAINER_V2, entity) + ) { + const cloudProvider = castArray(entity['cloud.provider'] as string)[0]; return ( ; + if (isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.SERVICE_V2, entity)) { + return ( + + ); } if (entity.entityType.startsWith('k8s')) { diff --git a/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx b/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx index 1245e25c3d35b..94ddaddea465e 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/components/search_bar/entity_types_multi_select.tsx @@ -40,11 +40,12 @@ export function EntityTypesMultiSelect() { const items = useMemo( () => value?.entityTypes.map((type): EuiSelectableOption => { - const checked = selectedEntityTypes?.[type]; + const checked = selectedEntityTypes?.[type.id]; return { - label: type, + label: type.display_name, + key: type.id, checked, - 'data-test-subj': `entityTypes_multiSelect_filter_selection_${type}`, + 'data-test-subj': `entityTypes_multiSelect_filter_selection_${type.id}`, }; }) || [], [selectedEntityTypes, value?.entityTypes] @@ -88,7 +89,7 @@ export function EntityTypesMultiSelect() { hasActiveFilters={!!items.find((item) => item.checked === 'on')} numActiveFilters={items.filter((item) => item.checked === 'on').length} > - {i18n.translate('xpack.inventory.entityTypesMultSelect.typeFilterButtonLabel', { + {i18n.translate('xpack.inventory.entityTypesMultiSelect.typeFilterButtonLabel', { defaultMessage: 'Type', })} @@ -102,13 +103,13 @@ export function EntityTypesMultiSelect() { searchable searchProps={{ placeholder: i18n.translate( - 'xpack.inventory.entityTypesMultSelect.euiSelectable.placeholder', + 'xpack.inventory.entityTypesMultiSelect.euiSelectable.placeholder', { defaultMessage: 'Filter types' } ), compressed: true, }} aria-label={i18n.translate( - 'xpack.inventory.entityTypesMultSelect.euiSelectable.typeLabel', + 'xpack.inventory.entityTypesMultiSelect.euiSelectable.typeLabel', { defaultMessage: 'Entity type' } )} options={items} @@ -116,20 +117,23 @@ export function EntityTypesMultiSelect() { handleEntityTypeChecked( newOptions .filter((item) => item.checked) - .reduce((acc, curr) => ({ ...acc, [curr.label]: curr.checked! }), {}) + .reduce((acc, curr) => { + acc[curr.key as string] = curr.checked!; + return acc; + }, {} as EntityType) ); }} isLoading={loading} loadingMessage={i18n.translate( - 'xpack.inventory.entityTypesMultSelect.euiSelectable.loading', + 'xpack.inventory.entityTypesMultiSelect.euiSelectable.loading', { defaultMessage: 'Loading types' } )} emptyMessage={i18n.translate( - 'xpack.inventory.entityTypesMultSelect.euiSelectable.empty', + 'xpack.inventory.entityTypesMultiSelect.euiSelectable.empty', { defaultMessage: 'No types available' } )} noMatchesMessage={i18n.translate( - 'xpack.inventory.entityTypesMultSelect.euiSelectable.notFound', + 'xpack.inventory.entityTypesMultiSelect.euiSelectable.notFound', { defaultMessage: 'No types found' } )} > diff --git a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts index f05cdcf21cb2e..858b8ff60882f 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -61,14 +61,10 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for host entity', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), - entityType: 'host', - entityIdentityFields: ['host.name'], - host: { - name: 'host-1', - }, - cloud: { - provider: null, - }, + entityType: 'built_in_hosts_from_ecs_data', + entityIdentityFields: { source1: ['host.name'] }, + 'host.name': 'host-1', + 'cloud.provider': null, }; mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' }); @@ -84,14 +80,10 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for container entity', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), - entityType: 'container', - entityIdentityFields: ['container.id'], - container: { - id: 'container-1', - }, - cloud: { - provider: null, - }, + entityType: 'built_in_containers_from_ecs_data', + entityIdentityFields: { source1: ['container.id'] }, + 'container.id': 'container-1', + 'cloud.provider': null, }; mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' }); @@ -110,15 +102,11 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for service entity', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), - entityType: 'service', - entityIdentityFields: ['service.name'], - agent: { - name: 'node', - }, - service: { - name: 'service-1', - environment: 'prod', - }, + entityType: 'built_in_services_from_ecs_data', + entityIdentityFields: { source1: ['service.name'] }, + 'service.name': 'service-1', + 'agent.name': 'node', + 'service.environment': 'prod', }; mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' }); mockGetRedirectUrl.mockReturnValue('service-overview-url'); @@ -134,27 +122,40 @@ describe('useDetailViewRedirect', () => { [ [ - BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.ecs, + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.ecs, 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', ], - [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'], + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.semconv, 'kubernetes_otel-cluster-overview'], [ - BUILT_IN_ENTITY_TYPES.KUBERNETES.CRONJOB.ecs, + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CRON_JOB.ecs, 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', ], [ - BUILT_IN_ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs, + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DAEMON_SET.ecs, 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', ], [ - BUILT_IN_ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs, + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DEPLOYMENT.ecs, 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', ], - [BUILT_IN_ENTITY_TYPES.KUBERNETES.JOB.ecs, 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013'], - [BUILT_IN_ENTITY_TYPES.KUBERNETES.NODE.ecs, 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013'], - [BUILT_IN_ENTITY_TYPES.KUBERNETES.POD.ecs, 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013'], [ - BUILT_IN_ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.JOB.ecs, + 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.NODE.ecs, + 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.POD.ecs, + 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.SERVICE, + 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013', + ], + [ + BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.STATEFUL_SET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', ], ].forEach(([entityType, dashboardId]) => { @@ -162,10 +163,8 @@ describe('useDetailViewRedirect', () => { const entity: InventoryEntity = { ...(commonEntityFields as InventoryEntity), entityType, - entityIdentityFields: ['some.field'], - some: { - field: 'some-value', - }, + entityIdentityFields: { source1: ['some.field'] }, + 'some.field': 'some-value', }; mockAsKqlFilter.mockReturnValue('kql-query'); diff --git a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts index ec193ae9dfcad..4ba96c0e5f7d6 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_detail_view_redirect.ts @@ -14,24 +14,25 @@ import { import { useCallback } from 'react'; import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; -import { castArray } from 'lodash'; -import { isBuiltinEntityOfType } from '../../common/utils/entity_type_guards'; +import { isBuiltinEntityOfType } from '../../common/utils/check_entity_type'; import type { InventoryEntity } from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { - [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.ecs]: 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.CRONJOB.ecs]: 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.DAEMONSET.ecs]: + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.ecs]: + 'kubernetes-f4dc26db-1b53-4ea2-a78b-1bfab8ea267c', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CLUSTER.semconv]: 'kubernetes_otel-cluster-overview', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.CRON_JOB.ecs]: + 'kubernetes-0a672d50-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DAEMON_SET.ecs]: 'kubernetes-85879010-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.DEPLOYMENT.ecs]: + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.DEPLOYMENT.ecs]: 'kubernetes-5be46210-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.SERVICE.ecs]: 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013', - [BUILT_IN_ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs]: + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.JOB.ecs]: 'kubernetes-9bf990a0-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.NODE.ecs]: 'kubernetes-b945b7b0-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.POD.ecs]: 'kubernetes-3d4d9290-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.SERVICE]: 'kubernetes-ff1b3850-bcb1-11ec-b64f-7dd6e8e82013', + [BUILT_IN_ENTITY_TYPES.KUBERNETES_V2.STATEFUL_SET.ecs]: 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013', }; @@ -48,21 +49,23 @@ export const useDetailViewRedirect = () => { const getDetailViewRedirectUrl = useCallback( (entity: InventoryEntity) => { const identityFieldsValue = entityManager.entityClient.getIdentityFieldsValue({ - entity: { - identity_fields: entity.entityIdentityFields, - }, - ...entity, + entity, }); - const identityFields = castArray(entity.entityIdentityFields); + const identityFields = Object.keys(identityFieldsValue || {}); - if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) { + if ( + isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.HOST_V2, entity) || + isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.CONTAINER_V2, entity) + ) { return assetDetailsLocator?.getRedirectUrl({ assetId: identityFieldsValue[identityFields[0]], - assetType: entity.entityType, + assetType: isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.HOST_V2, entity) + ? 'host' + : 'container', }); } - if (isBuiltinEntityOfType('service', entity)) { + if (isBuiltinEntityOfType(BUILT_IN_ENTITY_TYPES.SERVICE_V2, entity)) { return serviceOverviewLocator?.getRedirectUrl({ serviceName: identityFieldsValue[identityFields[0]], }); @@ -75,7 +78,7 @@ export const useDetailViewRedirect = () => { const getDashboardRedirectUrl = useCallback( (entity: InventoryEntity) => { - const type = entity.entityType; + const { entityType: type } = entity; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; return dashboardId @@ -84,10 +87,7 @@ export const useDetailViewRedirect = () => { query: { language: 'kuery', query: entityManager.entityClient.asKqlFilter({ - entity: { - identity_fields: entity.entityIdentityFields, - }, - ...entity, + entity, }), }, }) diff --git a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts index dc9f5bf4a4740..141b6d2fc97bc 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts +++ b/x-pack/solutions/observability/plugins/inventory/public/hooks/use_discover_redirect.ts @@ -15,7 +15,7 @@ export const useDiscoverRedirect = (entity: InventoryEntity) => { services: { share, application, entityManager }, } = useKibana(); const { entityDefinitions, isEntityDefinitionLoading } = useFetchEntityDefinition( - entity.entityDefinitionId + entity.entityDefinitionId as string ); const title = useMemo( @@ -33,10 +33,7 @@ export const useDiscoverRedirect = (entity: InventoryEntity) => { const getDiscoverEntitiesRedirectUrl = useCallback(() => { const entityKqlFilter = entity ? entityManager.entityClient.asKqlFilter({ - entity: { - identity_fields: entity.entityIdentityFields, - }, - ...entity, + entity, }) : ''; diff --git a/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx b/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx index 6eab905a40692..49fe1a965d94d 100644 --- a/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx +++ b/x-pack/solutions/observability/plugins/inventory/public/pages/inventory_page/index.tsx @@ -5,8 +5,6 @@ * 2.0. */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { flattenObject } from '@kbn/observability-utils-common/object/flatten_object'; import React from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; import { EntitiesSummary } from '../../components/entities_summary'; @@ -28,19 +26,11 @@ export function InventoryPage() { query: { kuery }, } = useInventoryParams('/'); const { entityTypes } = useInventoryDecodedQueryParams(); - - const { - value = { groupBy: ENTITY_TYPE, groups: [], entitiesCount: 0 }, - refresh, - loading, - } = useInventoryAbortableAsync( + const { value, refresh, loading } = useInventoryAbortableAsync( ({ signal }) => { const { entityTypesOff, entityTypesOn } = groupEntityTypesByStatus(entityTypes); - return inventoryAPIClient.fetch('GET /internal/inventory/entities/group_by/{field}', { + return inventoryAPIClient.fetch('GET /internal/inventory/entities/types', { params: { - path: { - field: ENTITY_TYPE, - }, query: { includeEntityTypes: entityTypesOn.length ? JSON.stringify(entityTypesOn) : undefined, excludeEntityTypes: entityTypesOff.length ? JSON.stringify(entityTypesOff) : undefined, @@ -62,21 +52,23 @@ export function InventoryPage() { <> - + - {value.groups.map((group) => { - const groupValue = flattenObject(group)[value.groupBy]; + {value?.entityTypes.map((entityType) => { return ( ); diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_groups.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_groups.ts deleted file mode 100644 index ead3109060d13..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_groups.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; -import { kqlQuery } from '@kbn/observability-plugin/server'; -import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; -import { - ENTITIES_LATEST_ALIAS, - MAX_NUMBER_OF_ENTITIES, - type EntityGroup, -} from '../../../common/entities'; -import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; - -export async function getEntityGroupsBy({ - inventoryEsClient, - field, - kuery, - includeEntityTypes = [], - excludeEntityTypes = [], -}: { - inventoryEsClient: ObservabilityElasticsearchClient; - field: string; - includeEntityTypes?: string[]; - excludeEntityTypes?: string[]; - kuery?: string; -}): Promise { - const from = `FROM ${ENTITIES_LATEST_ALIAS}`; - const where = [getBuiltinEntityDefinitionIdESQLWhereClause()]; - const params: ScalarValue[] = []; - - if (includeEntityTypes.length) { - where.push(`WHERE ${ENTITY_TYPE} IN (${includeEntityTypes.map(() => '?').join()})`); - params.push(...includeEntityTypes); - } - - if (excludeEntityTypes.length) { - where.push(`WHERE ${ENTITY_TYPE} NOT IN (${excludeEntityTypes.map(() => '?').join()})`); - params.push(...excludeEntityTypes); - } - - const group = `STATS count = COUNT(*) by ${field}`; - const sort = `SORT ${field} asc`; - const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`; - const query = [from, ...where, group, sort, limit].join(' | '); - - const { hits } = await inventoryEsClient.esql( - 'get_entities_groups', - { - query, - filter: { bool: { filter: kqlQuery(kuery) } }, - params, - }, - { transform: 'plain' } - ); - - return hits; -} diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_types.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_types.ts deleted file mode 100644 index c1f7894a178b1..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_entity_types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; -import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; -import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; - -export async function getEntityTypes({ - inventoryEsClient, -}: { - inventoryEsClient: ObservabilityElasticsearchClient; -}) { - const entityTypesEsqlResponse = await inventoryEsClient.esql< - { - 'entity.type': string; - }, - { transform: 'plain' } - >( - 'get_entity_types', - { - query: `FROM ${ENTITIES_LATEST_ALIAS} - | ${getBuiltinEntityDefinitionIdESQLWhereClause()} - | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE} - `, - }, - { transform: 'plain' } - ); - - return entityTypesEsqlResponse.hits.map((response) => response['entity.type']); -} diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts index 33bdc5e8a00a5..45603c297da36 100644 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts +++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.test.ts @@ -6,21 +6,22 @@ */ import { getGroupByTermsAgg } from './get_group_by_terms_agg'; -import type { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +type Fields = Record; describe('getGroupByTermsAgg', () => { it('should return an empty object when fields is empty', () => { - const fields: IdentityFieldsPerEntityType = new Map(); + const fields: Fields = {}; const result = getGroupByTermsAgg(fields); expect(result).toEqual({}); }); it('should correctly generate aggregation structure for service, host, and container entity types', () => { - const fields: IdentityFieldsPerEntityType = new Map([ - ['service', ['service.name', 'service.environment']], - ['host', ['host.name']], - ['container', ['container.id', 'foo.bar']], - ]); + const fields: Fields = { + service: ['service.name', 'service.environment'], + host: ['host.name'], + container: ['container.id', 'foo.bar'], + }; const result = getGroupByTermsAgg(fields); @@ -58,7 +59,10 @@ describe('getGroupByTermsAgg', () => { }); }); it('should override maxSize when provided', () => { - const fields: IdentityFieldsPerEntityType = new Map([['host', ['host.name']]]); + const fields: Fields = { + host: ['host.name'], + }; + const result = getGroupByTermsAgg(fields, 10); expect(result.host.composite.size).toBe(10); }); diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts index 71c6e8901c1b7..705c9ea43a642 100644 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts +++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_group_by_terms_agg.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; +import type { AggregationsCompositeAggregation } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize = 500) => { - return Array.from(fields).reduce((acc, [entityType, identityFields]) => { - acc[entityType] = { +export const getGroupByTermsAgg = (fields: { [key: string]: string[] }, maxSize = 500) => { + return Object.entries(fields).reduce((acc, [sourceId, identityFields]) => { + acc[sourceId] = { composite: { size: maxSize, sources: identityFields.map((field) => ({ @@ -22,5 +22,5 @@ export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize }, }; return acc; - }, {} as Record); + }, {} as Record); }; diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identify_fields.test.ts deleted file mode 100644 index 8b6b3b109352c..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identify_fields.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { InventoryEntity } from '../../../common/entities'; -import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; - -const commonEntityFields: Partial = { - entityLastSeenTimestamp: '2023-10-09T00:00:00Z', - entityId: '1', - entityDisplayName: 'entity_name', - entityDefinitionId: 'entity_definition_id', - alertsCount: 3, -}; - -describe('getIdentityFields', () => { - it('should return an empty Map when no entities are provided', () => { - const result = getIdentityFieldsPerEntityType([]); - expect(result.size).toBe(0); - }); - it('should return a Map with unique entity types and their respective identity fields', () => { - const serviceEntity: InventoryEntity = { - ...(commonEntityFields as InventoryEntity), - entityIdentityFields: ['service.name', 'service.environment'], - entityType: 'service', - agent: { - name: 'node', - }, - service: { - name: 'my-service', - }, - }; - - const hostEntity: InventoryEntity = { - ...(commonEntityFields as InventoryEntity), - entityIdentityFields: ['host.name'], - entityType: 'host', - cloud: { - provider: null, - }, - host: { - name: 'my-host', - }, - }; - - const containerEntity: InventoryEntity = { - ...(commonEntityFields as InventoryEntity), - entityIdentityFields: ['container.id'], - entityType: 'container', - host: { - name: 'my-host', - }, - cloud: { - provider: null, - }, - container: { - id: '123', - }, - }; - - const mockEntities = [serviceEntity, hostEntity, containerEntity]; - const result = getIdentityFieldsPerEntityType(mockEntities); - - expect(result.size).toBe(3); - - expect(result.get('service')).toEqual(['service.name', 'service.environment']); - expect(result.get('host')).toEqual(['host.name']); - expect(result.get('container')).toEqual(['container.id']); - }); -}); diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts deleted file mode 100644 index 06070b66bad1f..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { castArray } from 'lodash'; -import type { InventoryEntity } from '../../../common/entities'; - -export type IdentityFieldsPerEntityType = Map; - -export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntity[]) => { - const identityFieldsPerEntityType = new Map(); - - latestEntities.forEach((entity) => - identityFieldsPerEntityType.set(entity.entityType, castArray(entity.entityIdentityFields)) - ); - - return identityFieldsPerEntityType; -}; diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities.ts deleted file mode 100644 index 83f576220d12a..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; -import { kqlQuery } from '@kbn/observability-plugin/server'; -import { - ENTITY_DISPLAY_NAME, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; -import { unflattenObject } from '@kbn/observability-utils-common/object/unflatten_object'; -import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; -import { - ENTITIES_LATEST_ALIAS, - MAX_NUMBER_OF_ENTITIES, - type EntityColumnIds, - type InventoryEntity, -} from '../../../common/entities'; -import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; - -type EntitySortableColumnIds = Extract< - EntityColumnIds, - 'entityLastSeenTimestamp' | 'entityDisplayName' | 'entityType' ->; -const SORT_FIELDS_TO_ES_FIELDS: Record = { - entityLastSeenTimestamp: ENTITY_LAST_SEEN, - entityDisplayName: ENTITY_DISPLAY_NAME, - entityType: ENTITY_TYPE, -} as const; - -export async function getLatestEntities({ - inventoryEsClient, - sortDirection, - sortField, - kuery, - entityTypes, -}: { - inventoryEsClient: ObservabilityElasticsearchClient; - sortDirection: 'asc' | 'desc'; - sortField: EntityColumnIds; - kuery?: string; - entityTypes?: string[]; -}): Promise { - // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. - const entitiesSortField = - SORT_FIELDS_TO_ES_FIELDS[sortField as EntitySortableColumnIds] ?? ENTITY_LAST_SEEN; - - const from = `FROM ${ENTITIES_LATEST_ALIAS}`; - const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()]; - const params: ScalarValue[] = []; - - if (entityTypes) { - where.push(`WHERE ${ENTITY_TYPE} IN (${entityTypes.map(() => '?').join()})`); - params.push(...entityTypes); - } - - const sort = `SORT ${entitiesSortField} ${sortDirection}`; - const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`; - - const query = [from, ...where, sort, limit].join(' | '); - - const latestEntitiesEsqlResponse = await inventoryEsClient.esql< - { - 'entity.id': string; - 'entity.type': string; - 'entity.definition_id': string; - 'entity.display_name': string; - 'entity.identity_fields': string | string[]; - 'entity.last_seen_timestamp': string; - 'entity.definition_version': string; - 'entity.schema_version': string; - } & Record, - { transform: 'plain' } - >( - 'get_latest_entities', - { - query, - filter: { bool: { filter: kqlQuery(kuery) } }, - params, - }, - { transform: 'plain' } - ); - - return latestEntitiesEsqlResponse.hits.map((latestEntity) => { - Object.keys(latestEntity).forEach((key) => { - const keyOfObject = key as keyof typeof latestEntity; - // strip out multi-field aliases - if (keyOfObject.endsWith('.text') || keyOfObject.endsWith('.keyword')) { - delete latestEntity[keyOfObject]; - } - }); - - const { entity, ...metadata } = unflattenObject(latestEntity); - - return { - entityId: entity.id, - entityType: entity.type, - entityDefinitionId: entity.definition_id, - entityDisplayName: entity.display_name, - entityIdentityFields: entity.identity_fields, - entityLastSeenTimestamp: entity.last_seen_timestamp, - entityDefinitionVersion: entity.definition_version, - entitySchemaVersion: entity.schema_version, - ...metadata, - }; - }); -} diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts index c7291e772470b..e1959b9ba2d90 100644 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts +++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/get_latest_entities_alerts.ts @@ -9,7 +9,6 @@ import { termQuery } from '@kbn/observability-plugin/server'; import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import type { AlertsClient } from '../../lib/create_alerts_client/create_alerts_client'; import { getGroupByTermsAgg } from './get_group_by_terms_agg'; -import type { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; interface Bucket { key: Record; @@ -20,12 +19,12 @@ type EntityTypeBucketsAggregation = Record; export async function getLatestEntitiesAlerts({ alertsClient, - identityFieldsPerEntityType, + identityFieldsBySource, }: { alertsClient: AlertsClient; - identityFieldsPerEntityType: IdentityFieldsPerEntityType; -}): Promise> { - if (identityFieldsPerEntityType.size === 0) { + identityFieldsBySource: Record; +}): Promise> { + if (Object.keys(identityFieldsBySource).length === 0) { return []; } @@ -41,22 +40,23 @@ export async function getLatestEntitiesAlerts({ const response = await alertsClient.search({ ...filter, - aggs: getGroupByTermsAgg(identityFieldsPerEntityType), + aggs: getGroupByTermsAgg(identityFieldsBySource), }); const aggregations = response.aggregations as EntityTypeBucketsAggregation; - const alerts = Array.from(identityFieldsPerEntityType).flatMap(([entityType]) => { - const entityAggregation = aggregations?.[entityType]; + const alerts = Object.keys(identityFieldsBySource) + .map((sourceId) => { + const entityAggregation = aggregations?.[sourceId]; - const buckets = entityAggregation.buckets ?? []; + const buckets = entityAggregation.buckets ?? []; - return buckets.map((bucket: Bucket) => ({ - alertsCount: bucket.doc_count, - entityType, - ...bucket.key, - })); - }); + return buckets.map((bucket: Bucket) => ({ + alertsCount: bucket.doc_count, + ...bucket.key, + })); + }) + .flat(); return alerts; } diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts index 78d79105da6de..33899e8593795 100644 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts +++ b/x-pack/solutions/observability/plugins/inventory/server/routes/entities/route.ts @@ -4,40 +4,64 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; -import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { joinByKey } from '@kbn/observability-utils-common/array/join_by_key'; -import { createObservabilityEsClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; import * as t from 'io-ts'; import { orderBy } from 'lodash'; -import type { InventoryEntity } from '../../../common/entities'; -import { entityColumnIdsRt } from '../../../common/entities'; -import { createAlertsClient } from '../../lib/create_alerts_client/create_alerts_client'; +import moment from 'moment'; +import { DATA_STREAM_TYPE } from '@kbn/dataset-quality-plugin/common/es_fields'; +import { joinByKey } from '@kbn/observability-utils-common/array/join_by_key'; +import { BUILT_IN_ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import { + type InventoryEntity, + entityColumnIdsRt, + MAX_NUMBER_OF_ENTITIES, +} from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; -import { getEntityGroupsBy } from './get_entity_groups'; -import { getEntityTypes } from './get_entity_types'; -import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; -import { getLatestEntities } from './get_latest_entities'; +import { createAlertsClient } from '../../lib/create_alerts_client/create_alerts_client'; import { getLatestEntitiesAlerts } from './get_latest_entities_alerts'; export const getEntityTypesRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/entities/types', + params: t.partial({ + query: t.partial({ + includeEntityTypes: jsonRt.pipe(t.array(t.string)), + excludeEntityTypes: jsonRt.pipe(t.array(t.string)), + kuery: t.string, + }), + }), security: { authz: { requiredPrivileges: ['inventory'], }, }, - handler: async ({ context, logger }) => { - const coreContext = await context.core; - const inventoryEsClient = createObservabilityEsClient({ - client: coreContext.elasticsearch.client.asCurrentUser, - logger, - plugin: `@kbn/${INVENTORY_APP_ID}-plugin`, + handler: async ({ plugins, request, params }) => { + const entityManagerStart = await plugins.entityManager.start(); + + const entityManagerClient = await entityManagerStart.getScopedClient({ request }); + const { includeEntityTypes, excludeEntityTypes, kuery } = params?.query ?? {}; + + const rawEntityTypes = await entityManagerClient.v2.readTypeDefinitions(); + const hasIncludedEntityTypes = (includeEntityTypes ?? []).length > 0; + const entityTypes = rawEntityTypes.filter((entityType) => + hasIncludedEntityTypes + ? includeEntityTypes?.includes(entityType.id) + : !excludeEntityTypes?.includes(entityType.id) + ); + const entityCount = await entityManagerClient.v2.countEntities({ + start: moment().subtract(15, 'm').toISOString(), + end: moment().toISOString(), + types: entityTypes.map((entityType) => entityType.id), + filters: kuery ? [kuery] : undefined, }); - const entityTypes = await getEntityTypes({ inventoryEsClient }); - return { entityTypes }; + const entityTypesWithCount = entityTypes + .map((entityType) => ({ + ...entityType, + count: entityCount.types[entityType.id], + })) + .filter((entityType) => entityType.count > 0); + + return { entityTypes: entityTypesWithCount, totalEntities: entityCount.total }; }, }); @@ -48,10 +72,10 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ t.type({ sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + entityType: t.string, }), t.partial({ kuery: t.string, - entityTypes: jsonRt.pipe(t.array(t.string)), }), ]), }), @@ -62,42 +86,60 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ }, handler: async ({ params, - context, - logger, plugins, request, + logger, + context, }): Promise<{ entities: InventoryEntity[] }> => { - const coreContext = await context.core; - const inventoryEsClient = createObservabilityEsClient({ - client: coreContext.elasticsearch.client.asCurrentUser, - logger, - plugin: `@kbn/${INVENTORY_APP_ID}-plugin`, - }); + const entityManagerStart = await plugins.entityManager.start(); + const { client: clusterClient } = (await context.core).elasticsearch; - const { sortDirection, sortField, kuery, entityTypes } = params.query; + const { sortDirection, sortField, kuery, entityType } = params.query; - const [alertsClient, latestEntities] = await Promise.all([ + const [entityManagerClient, alertsClient] = await Promise.all([ + entityManagerStart.getScopedClient({ request }), createAlertsClient({ plugins, request }), - getLatestEntities({ - inventoryEsClient, - sortDirection, - sortField, - kuery, - entityTypes, - }), ]); - const identityFieldsPerEntityType = getIdentityFieldsPerEntityType(latestEntities); + const METADATA_BY_TYPE: { [key: string]: string[] } = { + default: [DATA_STREAM_TYPE], + [BUILT_IN_ENTITY_TYPES.CONTAINER_V2]: [DATA_STREAM_TYPE, 'cloud.provider'], + [BUILT_IN_ENTITY_TYPES.HOST_V2]: [DATA_STREAM_TYPE, 'cloud.provider'], + [BUILT_IN_ENTITY_TYPES.SERVICE_V2]: [DATA_STREAM_TYPE, 'agent.name'], + }; + + const [{ entities: rawEntities }, identityFieldsBySource] = await Promise.all([ + entityManagerClient.v2.searchEntities({ + start: moment().subtract(15, 'm').toISOString(), + end: moment().toISOString(), + limit: MAX_NUMBER_OF_ENTITIES, + type: entityType, + metadata_fields: METADATA_BY_TYPE[entityType] || METADATA_BY_TYPE.default, + filters: kuery ? [kuery] : [], + }), + entityManagerStart.v2.identityFieldsBySource(entityType, clusterClient, logger), + ]); const alerts = await getLatestEntitiesAlerts({ - identityFieldsPerEntityType, + identityFieldsBySource, alertsClient, }); + const entities: InventoryEntity[] = rawEntities.map((entity) => { + return { + entityId: entity['entity.id'], + entityType: entity['entity.type'], + entityDisplayName: entity['entity.display_name'], + entityLastSeenTimestamp: entity['entity.last_seen_timestamp'] as string, + entityIdentityFields: identityFieldsBySource, + ...entity, + }; + }); + const joined = joinByKey( - [...latestEntities, ...alerts] as InventoryEntity[], - [...identityFieldsPerEntityType.values()].flat() - ).filter((latestEntity) => latestEntity.entityId); + [...entities, ...alerts] as InventoryEntity[], + [...Object.values(identityFieldsBySource)].flat() + ).filter((latestEntity: InventoryEntity) => latestEntity.entityId); return { entities: @@ -112,50 +154,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ }, }); -export const groupEntitiesByRoute = createInventoryServerRoute({ - endpoint: 'GET /internal/inventory/entities/group_by/{field}', - params: t.intersection([ - t.type({ path: t.type({ field: t.literal(ENTITY_TYPE) }) }), - t.partial({ - query: t.partial({ - includeEntityTypes: jsonRt.pipe(t.array(t.string)), - excludeEntityTypes: jsonRt.pipe(t.array(t.string)), - kuery: t.string, - }), - }), - ]), - security: { - authz: { - requiredPrivileges: ['inventory'], - }, - }, - handler: async ({ params, context, logger }) => { - const coreContext = await context.core; - const inventoryEsClient = createObservabilityEsClient({ - client: coreContext.elasticsearch.client.asCurrentUser, - logger, - plugin: `@kbn/${INVENTORY_APP_ID}-plugin`, - }); - - const { field } = params.path; - const { kuery, includeEntityTypes, excludeEntityTypes } = params.query ?? {}; - - const groups = await getEntityGroupsBy({ - inventoryEsClient, - field, - kuery, - includeEntityTypes, - excludeEntityTypes, - }); - - const entitiesCount = groups.reduce((acc, group) => acc + group.count, 0); - - return { groupBy: field, groups, entitiesCount }; - }, -}); - export const entitiesRoutes = { ...listLatestEntitiesRoute, ...getEntityTypesRoute, - ...groupEntitiesByRoute, }; diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/get_has_data.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/get_has_data.ts deleted file mode 100644 index c3fd3971f09d2..0000000000000 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/get_has_data.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { Logger } from '@kbn/core/server'; -import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; -import { getBuiltinEntityDefinitionIdESQLWhereClause } from '../entities/query_helper'; -import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; - -export async function getHasData({ - inventoryEsClient, - logger, -}: { - inventoryEsClient: ObservabilityElasticsearchClient; - logger: Logger; -}) { - try { - const esqlResults = await inventoryEsClient.esql<{ _count: number }, { transform: 'plain' }>( - 'get_has_data', - { - query: `FROM ${ENTITIES_LATEST_ALIAS} - | ${getBuiltinEntityDefinitionIdESQLWhereClause()} - | STATS _count = COUNT(*) - | LIMIT 1`, - }, - { transform: 'plain' } - ); - - const totalCount = esqlResults.hits[0]._count; - - return { hasData: totalCount > 0 }; - } catch (e) { - logger.error(e); - return { hasData: false }; - } -} diff --git a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts b/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts index 4f4e4e81f6225..b8ed2aa19ee09 100644 --- a/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts +++ b/x-pack/solutions/observability/plugins/inventory/server/routes/has_data/route.ts @@ -4,10 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { createObservabilityEsClient } from '@kbn/observability-utils-server/es/client/create_observability_es_client'; -import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; +import moment from 'moment'; import { createInventoryServerRoute } from '../create_inventory_server_route'; -import { getHasData } from './get_has_data'; export const hasDataRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/has_data', @@ -16,18 +14,16 @@ export const hasDataRoute = createInventoryServerRoute({ requiredPrivileges: ['inventory'], }, }, - handler: async ({ context, logger }) => { - const coreContext = await context.core; - const inventoryEsClient = createObservabilityEsClient({ - client: coreContext.elasticsearch.client.asCurrentUser, - logger, - plugin: `@kbn/${INVENTORY_APP_ID}-plugin`, - }); + handler: async ({ plugins, request }) => { + const entityManagerStart = await plugins.entityManager.start(); + const entityManagerClient = await entityManagerStart.getScopedClient({ request }); - return getHasData({ - inventoryEsClient, - logger, + const { total } = await entityManagerClient.v2.countEntities({ + start: moment().subtract(15, 'm').toISOString(), + end: moment().toISOString(), }); + + return { hasData: total > 0 }; }, }); diff --git a/x-pack/solutions/observability/plugins/inventory/tsconfig.json b/x-pack/solutions/observability/plugins/inventory/tsconfig.json index 555d0d44b03dc..9305282d47d43 100644 --- a/x-pack/solutions/observability/plugins/inventory/tsconfig.json +++ b/x-pack/solutions/observability/plugins/inventory/tsconfig.json @@ -52,15 +52,13 @@ "@kbn/spaces-plugin", "@kbn/cloud-plugin", "@kbn/observability-utils-browser", - "@kbn/observability-utils-server", - "@kbn/observability-utils-common", "@kbn/storybook", "@kbn/dashboard-plugin", "@kbn/deeplinks-analytics", "@kbn/react-hooks", - "@kbn/observability-utils-common", "@kbn/observability-utils-browser", - "@kbn/observability-utils-server", - "@kbn/kibana-utils-plugin" + "@kbn/kibana-utils-plugin", + "@kbn/dataset-quality-plugin", + "@kbn/observability-utils-common", ] } diff --git a/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts b/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts index 222780a1fc31a..77832801dcd80 100644 --- a/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts +++ b/x-pack/solutions/observability/plugins/observability_shared/common/entity/entity_types.ts @@ -10,11 +10,30 @@ const createKubernetesEntity = (base: T) => ({ semconv: `k8s.${base}.semconv` as const, }); +const createKubernetesV2Entity = (base: T) => ({ + ecs: `built_in_kubernetes_${base}_ecs` as const, + semconv: `built_in_kubernetes_${base}_semconv` as const, +}); + export const BUILT_IN_ENTITY_TYPES = { + HOST_V2: 'built_in_hosts_from_ecs_data', + CONTAINER_V2: 'built_in_containers_from_ecs_data', + SERVICE_V2: 'built_in_services_from_ecs_data', + KUBERNETES_V2: { + CLUSTER: createKubernetesV2Entity('cluster'), + CRON_JOB: createKubernetesV2Entity('cron_job'), + DAEMON_SET: createKubernetesV2Entity('daemon_set'), + DEPLOYMENT: createKubernetesV2Entity('deployment'), + JOB: createKubernetesV2Entity('job'), + NODE: createKubernetesV2Entity('node'), + POD: createKubernetesV2Entity('pod'), + REPLICA_SET: createKubernetesV2Entity('replica_set'), + STATEFUL_SET: createKubernetesV2Entity('stateful_set'), + SERVICE: 'built_in_kubernetes_service_ecs', + }, HOST: 'host', CONTAINER: 'container', SERVICE: 'service', - SERVICE_V2: 'built_in_services_from_ecs_data', KUBERNETES: { CLUSTER: createKubernetesEntity('cluster'), CONTAINER: createKubernetesEntity('container'), diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx index 58a8f7b779a15..bbffd25b4f8f8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_header_buttons.tsx @@ -25,10 +25,9 @@ export const AddPrebuiltRulesHeaderButtons = () => { const { state: { selectedRules, - loadingRules, isRefetching, isUpgradingSecurityPackages, - isInstallingAllRules, + isAnyRuleInstalling, hasRulesToInstall, }, actions: { installAllRules, installSelectedRules }, @@ -39,8 +38,7 @@ export const AddPrebuiltRulesHeaderButtons = () => { const numberOfSelectedRules = selectedRules.length ?? 0; const shouldDisplayInstallSelectedRulesButton = numberOfSelectedRules > 0; - const isRuleInstalling = loadingRules.length > 0 || isInstallingAllRules; - const isRequestInProgress = isRuleInstalling || isRefetching || isUpgradingSecurityPackages; + const isRequestInProgress = isAnyRuleInstalling || isRefetching || isUpgradingSecurityPackages; const [isOverflowPopoverOpen, setOverflowPopover] = useBoolean(false); @@ -81,7 +79,7 @@ export const AddPrebuiltRulesHeaderButtons = () => { data-test-subj="installSelectedRulesButton" > {i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)} - {isRuleInstalling && } + {isAnyRuleInstalling && } @@ -116,7 +114,7 @@ export const AddPrebuiltRulesHeaderButtons = () => { aria-label={i18n.INSTALL_ALL_ARIA_LABEL} > {i18n.INSTALL_ALL} - {isRuleInstalling && } + {isAnyRuleInstalling && } diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx index 4d239458e6a57..bbed7dcd2961f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_install_button.tsx @@ -19,7 +19,10 @@ import React, { useCallback, useMemo } from 'react'; import useBoolean from 'react-use/lib/useBoolean'; import type { Rule } from '../../../../rule_management/logic'; import type { RuleSignatureId } from '../../../../../../common/api/detection_engine'; -import { type AddPrebuiltRulesTableActions } from './add_prebuilt_rules_table_context'; +import { + useAddPrebuiltRulesTableContext, + type AddPrebuiltRulesTableActions, +} from './add_prebuilt_rules_table_context'; import * as i18n from './translations'; export interface PrebuiltRulesInstallButtonProps { @@ -28,7 +31,6 @@ export interface PrebuiltRulesInstallButtonProps { installOneRule: AddPrebuiltRulesTableActions['installOneRule']; loadingRules: RuleSignatureId[]; isDisabled: boolean; - isInstallingAllRules: boolean; } export const PrebuiltRulesInstallButton = ({ @@ -37,8 +39,10 @@ export const PrebuiltRulesInstallButton = ({ installOneRule, loadingRules, isDisabled, - isInstallingAllRules, }: PrebuiltRulesInstallButtonProps) => { + const { + state: { isInstallingAllRules }, + } = useAddPrebuiltRulesTableContext(); const isRuleInstalling = loadingRules.includes(ruleId) || isInstallingAllRules; const isInstallButtonDisabled = isRuleInstalling || isDisabled; const [isPopoverOpen, setPopover] = useBoolean(false); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx index dca4809001971..cf5ba8aa5967b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/add_prebuilt_rules_table_context.tsx @@ -61,11 +61,14 @@ export interface AddPrebuiltRulesTableState { * package in background */ isUpgradingSecurityPackages: boolean; - /** * Is true when performing Install All Rules mutation */ isInstallingAllRules: boolean; + /** + * Is true when any rule is currently being installed + */ + isAnyRuleInstalling: boolean; /** * List of rule IDs that are currently being upgraded */ @@ -145,6 +148,8 @@ export const AddPrebuiltRulesTableContextProvider = ({ }), }); + const isAnyRuleInstalling = loadingRules.length > 0 || isInstallingAllRules; + const { mutateAsync: installAllRulesRequest } = usePerformInstallAllRules(); const { mutateAsync: installSpecificRulesRequest } = usePerformInstallSpecificRules(); @@ -281,6 +286,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ isRefetching, isUpgradingSecurityPackages, isInstallingAllRules, + isAnyRuleInstalling, selectedRules, lastUpdated: dataUpdatedAt, }, @@ -297,6 +303,7 @@ export const AddPrebuiltRulesTableContextProvider = ({ isRefetching, isUpgradingSecurityPackages, isInstallingAllRules, + isAnyRuleInstalling, selectedRules, dataUpdatedAt, actions, diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx index 4e15de4011fab..8501f595839c7 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/add_prebuilt_rules_table/use_add_prebuilt_rules_table_columns.tsx @@ -110,8 +110,7 @@ const INTEGRATIONS_COLUMN: TableColumn = { const createInstallButtonColumn = ( installOneRule: AddPrebuiltRulesTableActions['installOneRule'], loadingRules: RuleSignatureId[], - isDisabled: boolean, - isInstallingAllRules: boolean + isDisabled: boolean ): TableColumn => ({ field: 'rule_id', name: , @@ -122,7 +121,6 @@ const createInstallButtonColumn = ( installOneRule={installOneRule} loadingRules={loadingRules} isDisabled={isDisabled} - isInstallingAllRules={isInstallingAllRules} /> ), width: '10%', @@ -166,23 +164,9 @@ export const useAddPrebuiltRulesTableColumns = (): TableColumn[] => { width: '12%', }, ...(hasCRUDPermissions - ? [ - createInstallButtonColumn( - installOneRule, - loadingRules, - isDisabled, - isInstallingAllRules - ), - ] + ? [createInstallButtonColumn(installOneRule, loadingRules, isDisabled)] : []), ], - [ - hasCRUDPermissions, - installOneRule, - loadingRules, - isDisabled, - showRelatedIntegrations, - isInstallingAllRules, - ] + [hasCRUDPermissions, installOneRule, loadingRules, isDisabled, showRelatedIntegrations] ); };