From 3d0f6b9c5b8ebfd02e1ee4bba83341bfe66a4801 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Mon, 7 Oct 2024 15:29:32 +0300 Subject: [PATCH 01/23] Fetch alerts count for entities --- .../inventory/common/entities.ts | 2 + .../inventory/common/es_fields/entities.ts | 1 + .../inventory/kibana.jsonc | 1 + .../create_alerts_client.ts | 47 +++++++++++ .../entities/get_group_by_terms_agg.test.ts | 77 +++++++++++++++++++ .../routes/entities/get_group_by_terms_agg.ts | 26 +++++++ .../entities/get_identify_fields.test.ts | 40 ++++++++++ .../get_identity_fields_per_entity_type.ts | 28 +++++++ .../entities/get_inventory_entities_list.ts | 32 ++++++++ .../entities/get_latest_entities_alerts.ts | 51 ++++++++++++ .../inventory/server/routes/entities/route.ts | 18 ++++- .../inventory/server/types.ts | 6 ++ 12 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts create mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 5dec3420ee005..780b474a94fe3 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -19,6 +19,7 @@ import { ENTITY_ID, ENTITY_LAST_SEEN, ENTITY_TYPE, + ENTITY_IDENTITY_FIELDS, } from './es_fields/entities'; export const entityTypeRt = t.union([ @@ -77,6 +78,7 @@ interface BaseEntity { [ENTITY_TYPE]: EntityType; [ENTITY_DISPLAY_NAME]: string; [ENTITY_DEFINITION_ID]: string; + [ENTITY_IDENTITY_FIELDS]: string | string[]; } /** diff --git a/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts b/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts index 9b619dddbb2df..8baecabea6ef7 100644 --- a/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts @@ -10,3 +10,4 @@ export const ENTITY_ID = 'entity.id'; export const ENTITY_TYPE = 'entity.type'; export const ENTITY_DISPLAY_NAME = 'entity.displayName'; export const ENTITY_DEFINITION_ID = 'entity.definitionId'; +export const ENTITY_IDENTITY_FIELDS = 'entity.identityFields'; diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc index f60cf36183b24..789ab4fa670dc 100644 --- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -16,6 +16,7 @@ "features", "unifiedSearch", "data", + "ruleRegistry", "share" ], "requiredBundles": ["kibanaReact"], diff --git a/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts b/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts new file mode 100644 index 0000000000000..150e946fd98d6 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/lib/create_alerts_client.ts/create_alerts_client.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import { InventoryRouteHandlerResources } from '../../routes/types'; + +export type AlertsClient = Awaited>; + +export async function createAlertsClient({ + plugins, + request, +}: Pick) { + const ruleRegistryPluginStart = await plugins.ruleRegistry.start(); + const alertsClient = await ruleRegistryPluginStart.getRacClientWithRequest(request); + const alertsIndices = await alertsClient.getAuthorizedAlertsIndices([ + 'logs', + 'infrastructure', + 'apm', + 'slo', + 'observability', + ]); + + if (!alertsIndices || isEmpty(alertsIndices)) { + throw Error('No alert indices exist'); + } + type RequiredParams = ESSearchRequest & { + size: number; + track_total_hits: boolean | number; + }; + + return { + search( + searchParams: TParams + ): Promise> { + return alertsClient.find({ + ...searchParams, + index: alertsIndices.join(','), + }) as Promise; + }, + }; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts new file mode 100644 index 0000000000000..09dbee6dfd26a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EntityType } from '../../../common/entities'; +import { getGroupByTermsAgg } from './get_group_by_terms_agg'; + +describe('getGroupByTermsAgg', () => { + it('should return an empty object when no fields are provided', () => { + const result = getGroupByTermsAgg([]); + expect(result).toEqual({}); + }); + + it('should return a valid aggregation structure for a single field', () => { + const fields = new Map(['host' as EntityType, 'host.name']); + const result = getGroupByTermsAgg(fields); + expect(result).toEqual({ + host: { + terms: { + field: 'host.name', + size: 500, + }, + }, + }); + }); + + // it('should return a valid aggregation structure for multiple fields', () => { + // const result = getGroupByTermsAgg(['host.name', 'service.name']); + // expect(result).toEqual({ + // 'host.name': { + // terms: { + // field: 'host.name', + // size: 500, + // }, + // }, + // 'service.name': { + // terms: { + // field: 'service.name', + // size: 500, + // }, + // }, + // }); + // }); + + // it('should allow overriding the default maxSize value', () => { + // const result = getGroupByTermsAgg(['host.name'], 100); + // expect(result).toEqual({ + // 'host.name': { + // terms: { + // field: 'host.name', + // size: 100, + // }, + // }, + // }); + // }); + + // it('should apply maxSize to all fields', () => { + // const result = getGroupByTermsAgg(['host.name', 'service.name'], 200); + // expect(result).toEqual({ + // 'host.name': { + // terms: { + // field: 'host.name', + // size: 200, + // }, + // }, + // 'service.name': { + // terms: { + // field: 'service.name', + // size: 200, + // }, + // }, + // }); + // }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts new file mode 100644 index 0000000000000..96ab3eb24444a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +export const getGroupByTermsAgg = (fields: IdentityFieldsPerEntityType, maxSize = 500) => { + return Array.from(fields).reduce((acc, [entityType, identityFields]) => { + acc[entityType] = { + composite: { + size: maxSize, + sources: identityFields.map((field) => ({ + [field]: { + terms: { + field, + }, + }, + })), + }, + }; + return acc; + }, {} as Record); +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts new file mode 100644 index 0000000000000..2fe2988839c63 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EntityType } from '../../../common/entities'; // Adjust the import path accordingly +import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '../../../common/es_fields/entities'; +import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +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 mockEntities = [ + { + [ENTITY_TYPE]: 'service' as EntityType, + [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], + }, + { + [ENTITY_TYPE]: 'host' as EntityType, + [ENTITY_IDENTITY_FIELDS]: 'host.name', + }, + { + [ENTITY_TYPE]: 'container' as EntityType, + [ENTITY_IDENTITY_FIELDS]: 'contaier.id', + }, + ]; + 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('contaier.id'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts new file mode 100644 index 0000000000000..891f149168c95 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Entity, EntityType } from '../../../common/entities'; +import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '../../../common/es_fields/entities'; + +export type IdentityFieldsPerEntityType = Map; + +export const getIdentityFieldsPerEntityType = ( + entities: Array> +) => { + const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); + + entities.map((entity) => + identityFieldsPerEntityType.set( + entity[ENTITY_TYPE], + Array.isArray(entity[ENTITY_IDENTITY_FIELDS]) + ? entity[ENTITY_IDENTITY_FIELDS] + : [entity[ENTITY_IDENTITY_FIELDS]] + ) + ); + + return identityFieldsPerEntityType; +}; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts new file mode 100644 index 0000000000000..e2a5b357de4a2 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; +import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { + ENTITIES_LATEST_ALIAS, + MAX_NUMBER_OF_ENTITIES, + type EntityType, + Entity, +} from '../../../common/entities'; + +export async function getInventoryEntitiesList({ + inventoryEsClient, + sortDirection, + sortField, + entityTypes, + kuery, + alertsClient, +}: { + inventoryEsClient: ObservabilityElasticsearchClient; + sortDirection: 'asc' | 'desc'; + sortField: string; + entityTypes?: EntityType[]; + kuery?: string; + alertsClient: AlertsClient; +}) {} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts new file mode 100644 index 0000000000000..36afda2465f25 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; +import { AlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; +import { getGroupByTermsAgg } from './get_group_by_terms_agg'; +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; + +export async function getLatestEntitiesAlerts({ + alertsClient, + kuery, + identityFieldsPerEntityType, +}: { + alertsClient: AlertsClient; + kuery?: string; + identityFieldsPerEntityType: IdentityFieldsPerEntityType; +}) { + if (identityFieldsPerEntityType.size === 0) { + return []; + } + + const filter = { + size: 0, + track_total_hits: false, + query: { + bool: { + filter: [...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), ...kqlQuery(kuery)], + }, + }, + }; + + const response = await alertsClient.search({ + ...filter, + aggs: getGroupByTermsAgg(identityFieldsPerEntityType), + }); + + const alerts = Array.from(identityFieldsPerEntityType).flatMap(([field, value]) => { + const buckets = response.aggregations?.[field]?.buckets ?? []; + return buckets.map((bucket: { key: Record; doc_count: number }) => ({ + alertsCount: bucket.doc_count, + ...bucket.key, + })); + }); + + return alerts; +} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index beef1b068ed15..b09ff9114d69c 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -6,12 +6,16 @@ */ import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; +import { merge } from 'lodash'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; import { entityTypeRt } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; +import { createAlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; +import { getLatestEntitiesAlerts } from './get_latest_entities_alerts'; +import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; export const getEntityTypesRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/entities/types', @@ -48,7 +52,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ options: { tags: ['access:inventory'], }, - handler: async ({ params, context, logger }) => { + handler: async ({ params, context, logger, plugins, request }) => { const coreContext = await context.core; const inventoryEsClient = createObservabilityEsClient({ client: coreContext.elasticsearch.client.asCurrentUser, @@ -58,6 +62,8 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ const { sortDirection, sortField, entityTypes, kuery } = params.query; + const alertsClient = await createAlertsClient({ plugins, request }); + const latestEntities = await getLatestEntities({ inventoryEsClient, sortDirection, @@ -66,7 +72,15 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ kuery, }); - return { entities: latestEntities }; + const identityFieldsPerEntityType = getIdentityFieldsPerEntityType(latestEntities); + + const alerts = await getLatestEntitiesAlerts({ + identityFieldsPerEntityType, + alertsClient, + kuery, + }); + + return { entities: merge(latestEntities, alerts) }; }, }); diff --git a/x-pack/plugins/observability_solution/inventory/server/types.ts b/x-pack/plugins/observability_solution/inventory/server/types.ts index 05f75561674c6..d3d5ef0fb7f60 100644 --- a/x-pack/plugins/observability_solution/inventory/server/types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/types.ts @@ -14,6 +14,10 @@ import type { DataViewsServerPluginStart, } from '@kbn/data-views-plugin/server'; import { FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { + RuleRegistryPluginStartContract, + RuleRegistryPluginSetupContract, +} from '@kbn/rule-registry-plugin/server'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ConfigSchema {} @@ -23,12 +27,14 @@ export interface InventorySetupDependencies { inference: InferenceServerSetup; dataViews: DataViewsServerPluginSetup; features: FeaturesPluginSetup; + ruleRegistry: RuleRegistryPluginSetupContract; } export interface InventoryStartDependencies { entityManager: EntityManagerServerPluginStart; inference: InferenceServerStart; dataViews: DataViewsServerPluginStart; + ruleRegistry: RuleRegistryPluginStartContract; } export interface InventoryServerSetup {} From 391eb208de2cd5d25468b7a7d9d649913ce3e583 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Mon, 7 Oct 2024 15:30:06 +0300 Subject: [PATCH 02/23] Show alerts count --- .../inventory/common/entities.ts | 1 + .../inventory/common/utils/join_by_key.ts | 43 +++++++ .../components/entities_grid/grid_columns.tsx | 120 ++++++++++++++++++ .../public/components/entities_grid/index.tsx | 118 ++++++----------- .../inventory/server/routes/entities/route.ts | 14 +- 5 files changed, 217 insertions(+), 79 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 780b474a94fe3..9d5e934982bed 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -79,6 +79,7 @@ interface BaseEntity { [ENTITY_DISPLAY_NAME]: string; [ENTITY_DEFINITION_ID]: string; [ENTITY_IDENTITY_FIELDS]: string | string[]; + alertsCount?: number; } /** diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts new file mode 100644 index 0000000000000..9bc90ee5e91dd --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { castArray, merge } from 'lodash'; +import stableStringify from 'json-stable-stringify'; +import { UnionToIntersection, ValuesType } from 'utility-types'; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends ArrayOrSingle, + W extends JoinedReturnType, + X extends (a: T, b: T) => ValuesType +>(items: T[], key: V, mergeFn: X): W; + +export function joinByKey( + items: Array>, + key: string | string[], + mergeFn: Function = (a: Record, b: Record) => merge({}, a, b) +) { + const keys = castArray(key); + // Create a map to quickly query the key of group. + const map = new Map(); + items.forEach((current) => { + // The key of the map is a stable JSON string of the values from given keys. + // We need stable JSON string to support plain object values. + const stableKey = stableStringify(keys.map((k) => current[k])); + + if (map.has(stableKey)) { + const item = map.get(stableKey); + // delete and set the key to put it last + map.delete(stableKey); + map.set(stableKey, mergeFn(item, current)); + } else { + map.set(stableKey, { ...current }); + } + }); + return [...map.values()]; +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx new file mode 100644 index 0000000000000..c4564d6a417d4 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 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 { EuiButtonIcon, EuiDataGridColumn, EuiToolTip } from '@elastic/eui'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + ENTITY_DISPLAY_NAME, + ENTITY_LAST_SEEN, + ENTITY_TYPE, +} from '../../../common/es_fields/entities'; + +const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel', { + defaultMessage: 'Alerts', +}); + +const alertsTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip', { + defaultMessage: 'Active alerts', +}); + +const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { + defaultMessage: 'Entity name', +}); +const entityNameTooltip = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.entityNameTooltip', + { + defaultMessage: 'Name of the entity (entity.displayName)', + } +); + +const entityTypeLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeLabel', { + defaultMessage: 'Type', +}); +const entityTypeTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeTooltip', { + defaultMessage: 'Type of entity (entity.type)', +}); + +const entityLastSeenLabel = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenLabel', + { + defaultMessage: 'Last seen', + } +); +const entityLastSeenToolip = i18n.translate( + 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenTooltip', + { + defaultMessage: 'Timestamp of last received data for entity (entity.lastSeenTimestamp)', + } +); + +export type EntityColumnIds = + | typeof ENTITY_DISPLAY_NAME + | typeof ENTITY_LAST_SEEN + | typeof ENTITY_TYPE + | 'alertsCount'; + +const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( + <> + {title} + + + + +); + +export const getColumns = ({ + showAlertsColumn, +}: { + showAlertsColumn: boolean; +}): EuiDataGridColumn[] => { + return [ + ...(showAlertsColumn + ? [ + { + id: 'alertsCount', + displayAsText: alertsLabel, + isSortable: true, + display: , + initialWidth: 100, + }, + ] + : []), + { + id: ENTITY_DISPLAY_NAME, + // keep it for accessibility purposes + displayAsText: entityNameLabel, + display: , + isSortable: true, + }, + { + id: ENTITY_TYPE, + // keep it for accessibility purposes + displayAsText: entityTypeLabel, + display: , + isSortable: true, + }, + { + id: ENTITY_LAST_SEEN, + // keep it for accessibility purposes + displayAsText: entityLastSeenLabel, + display: ( + + ), + defaultSortDirection: 'desc', + isSortable: true, + schema: 'datetime', + }, + ]; +}; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index caf83c0976cd6..3ac2454b6cd2e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -5,10 +5,9 @@ * 2.0. */ import { - EuiButtonIcon, + EuiBadge, EuiDataGrid, EuiDataGridCellValueElementProps, - EuiDataGridColumn, EuiDataGridSorting, EuiLink, EuiLoadingSpinner, @@ -26,8 +25,8 @@ import { } from '@kbn/observability-shared-plugin/common'; import { last } from 'lodash'; -import React, { useCallback, useState } from 'react'; -import { EntityType } from '../../../common/entities'; +import React, { useCallback, useMemo } from 'react'; +import { Entity, EntityType } from '../../../common/entities'; import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, @@ -37,82 +36,13 @@ import { APIReturnType } from '../../api'; import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; import { parseServiceParams } from '../../utils/parse_service_params'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; +import { EntityColumnIds, getColumns } from './grid_columns'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; type LatestEntities = InventoryEntitiesAPIReturnType['entities']; type LatestEntity = LatestEntities extends Array ? Entity : never; -export type EntityColumnIds = - | typeof ENTITY_DISPLAY_NAME - | typeof ENTITY_LAST_SEEN - | typeof ENTITY_TYPE; - -const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( - <> - {title} - - - - -); - -const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { - defaultMessage: 'Entity name', -}); -const entityTypeLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.typeLabel', { - defaultMessage: 'Type', -}); -const entityLastSeenLabel = i18n.translate( - 'xpack.inventory.entitiesGrid.euiDataGrid.lastSeenLabel', - { - defaultMessage: 'Last seen', - } -); - -const columns: EuiDataGridColumn[] = [ - { - id: ENTITY_DISPLAY_NAME, - // keep it for accessibility purposes - displayAsText: entityNameLabel, - display: ( - - ), - isSortable: true, - }, - { - id: ENTITY_TYPE, - // keep it for accessibility purposes - displayAsText: entityTypeLabel, - display: ( - - ), - isSortable: true, - }, - { - id: ENTITY_LAST_SEEN, - // keep it for accessibility purposes - displayAsText: entityLastSeenLabel, - display: ( - - ), - defaultSortDirection: 'desc', - isSortable: true, - schema: 'datetime', - }, -]; - interface Props { loading: boolean; entities: LatestEntities; @@ -136,7 +66,6 @@ export function EntitiesGrid({ onChangeSort, onFilterByType, }: Props) { - const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id)); const { services } = useKibana<{ share?: SharePluginStart }>(); const assetDetailsLocator = @@ -180,6 +109,24 @@ export function EntitiesGrid({ [assetDetailsLocator, serviceOverviewLocator] ); + const showAlertsColumn = useMemo( + () => entities.some((entity: Entity) => entity?.alertsCount && entity.alertsCount > 0), + [entities] + ); + + const visibleColumns = useMemo( + () => getColumns({ showAlertsColumn }).map(({ id }) => id), + [showAlertsColumn] + ); + + const columnVisibility = useMemo( + () => ({ + visibleColumns, + setVisibleColumns: () => {}, + }), + [visibleColumns] + ); + const renderCellValue = useCallback( ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { const entity = entities[rowIndex]; @@ -189,6 +136,23 @@ export function EntitiesGrid({ const columnEntityTableId = columnId as EntityColumnIds; switch (columnEntityTableId) { + case 'alertsCount': + return entity?.alertsCount ? ( + + + {entity.alertsCount} + + + ) : null; + case ENTITY_TYPE: const entityType = entity[columnEntityTableId] as EntityType; return ( @@ -254,8 +218,8 @@ export function EntitiesGrid({ 'xpack.inventory.entitiesGrid.euiDataGrid.inventoryEntitiesGridLabel', { defaultMessage: 'Inventory entities grid' } )} - columns={columns} - columnVisibility={{ visibleColumns, setVisibleColumns }} + columns={getColumns({ showAlertsColumn })} + columnVisibility={columnVisibility} rowCount={entities.length} renderCellValue={renderCellValue} gridStyle={{ border: 'horizontal', header: 'shade' }} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index b09ff9114d69c..ff95272a432a6 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -6,7 +6,6 @@ */ import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; -import { merge } from 'lodash'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; import { entityTypeRt } from '../../../common/entities'; @@ -16,6 +15,7 @@ import { getLatestEntities } from './get_latest_entities'; import { createAlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; import { getLatestEntitiesAlerts } from './get_latest_entities_alerts'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; +import { joinByKey } from '../../../common/utils/join_by_key'; export const getEntityTypesRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/entities/types', @@ -80,7 +80,17 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ kuery, }); - return { entities: merge(latestEntities, alerts) }; + console.log('identityFieldsPerEntityType', identityFieldsPerEntityType.values()); + console.log('alerts', alerts); + const joined = joinByKey( + [...latestEntities, ...alerts], + ['service.name', 'service.environment'] + ); + + console.log('joined', joined); + return { + entities: joined, + }; }, }); From 6c45bda6694a17dedcceb56da35b15fd87627ee7 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Wed, 9 Oct 2024 12:30:56 +0300 Subject: [PATCH 03/23] Add some tests --- .../common/utils/join_by_key/index.test.ts | 47 ++++ .../common/utils/join_by_key.test.ts | 224 ++++++++++++++++++ .../inventory/common/utils/join_by_key.ts | 22 +- .../entities/get_group_by_terms_agg.test.ts | 98 ++++---- .../entities/get_identify_fields.test.ts | 4 +- 5 files changed, 336 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts diff --git a/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts b/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts index 5938d952cb42f..70314e5e182d3 100644 --- a/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts +++ b/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts @@ -104,6 +104,53 @@ describe('joinByKey', () => { ]); }); + it('joins by multiple keys', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + environment: 'production', + alertCount: 10, + }, + { + serviceName: 'opbeans-node', + environment: 'production', + }, + { + serviceName: 'opbeans-node', + environment: 'staging', + }, + { + hostName: 'my-host', + cloudProvider: 'aws', + }, + { + hostName: 'my-host', + alertCount: 10, + serviceName: 'opbeans-node', + }, + ], + ['serviceName', 'environment', 'hostName'] + ); + + expect(joined).toEqual([ + { + serviceName: 'opbeans-node', + environment: 'production', + alertCount: 10, + }, + { + serviceName: 'opbeans-node', + environment: 'staging', + }, + { + hostName: 'my-host', + cloudProvider: 'aws', + alertCount: 10, + }, + ]); + }); + it('uses the custom merge fn to replace items', () => { const joined = joinByKey( [ diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts new file mode 100644 index 0000000000000..14988f7ef27e8 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts @@ -0,0 +1,224 @@ +/* + * Copyright 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 { joinByKey } from './join_by_key'; + +describe('joinByKey', () => { + // it('joins by a string key', () => { + // const joined = joinByKey( + // [ + // { + // serviceName: 'opbeans-node', + // avg: 10, + // }, + // { + // serviceName: 'opbeans-node', + // count: 12, + // }, + // { + // serviceName: 'opbeans-java', + // avg: 11, + // }, + // { + // serviceName: 'opbeans-java', + // p95: 18, + // }, + // ], + // 'serviceName' + // ); + + // expect(joined.length).toBe(2); + + // expect(joined).toEqual([ + // { + // serviceName: 'opbeans-node', + // avg: 10, + // count: 12, + // }, + // { + // serviceName: 'opbeans-java', + // avg: 11, + // p95: 18, + // }, + // ]); + // }); + + // it('joins by a record key', () => { + // const joined = joinByKey( + // [ + // { + // key: { + // serviceName: 'opbeans-node', + // transactionName: '/api/opbeans-node', + // }, + // avg: 10, + // }, + // { + // key: { + // serviceName: 'opbeans-node', + // transactionName: '/api/opbeans-node', + // }, + // count: 12, + // }, + // { + // key: { + // serviceName: 'opbeans-java', + // transactionName: '/api/opbeans-java', + // }, + // avg: 11, + // }, + // { + // key: { + // serviceName: 'opbeans-java', + // transactionName: '/api/opbeans-java', + // }, + // p95: 18, + // }, + // ], + // 'key' + // ); + + // expect(joined.length).toBe(2); + + // expect(joined).toEqual([ + // { + // key: { + // serviceName: 'opbeans-node', + // transactionName: '/api/opbeans-node', + // }, + // avg: 10, + // count: 12, + // }, + // { + // key: { + // serviceName: 'opbeans-java', + // transactionName: '/api/opbeans-java', + // }, + // avg: 11, + // p95: 18, + // }, + // ]); + // }); + + it('joins by multiple keys', () => { + const data = [ + { + serviceName: 'opbeans-node', + environment: 'production', + type: 'service', + }, + { + serviceName: 'opbeans-node', + environment: 'stage', + type: 'service', + }, + { + serviceName: 'opbeans-node', + hostName: 'host-1', + }, + { + containerId: 'containerId', + }, + ]; + + const alerts = [ + { + serviceName: 'opbeans-node', + environment: 'production', + type: 'service', + alertCount: 10, + }, + { + containerId: 'containerId', + alertCount: 1, + }, + { + hostName: 'host-1', + environment: 'production', + alertCount: 5, + }, + ]; + + const joined = joinByKey( + [...data, ...alerts], + ['serviceName', 'environment', 'hostName', 'containerId'] + ); + + expect(joined.length).toBe(5); + + expect(joined).toEqual([ + { environment: 'stage', serviceName: 'opbeans-node', type: 'service' }, + { hostName: 'host-1', serviceName: 'opbeans-node' }, + { alertCount: 10, environment: 'production', serviceName: 'opbeans-node', type: 'service' }, + { alertCount: 1, containerId: 'containerId' }, + { alertCount: 5, environment: 'production', hostName: 'host-1' }, + ]); + }); + + // it('uses the custom merge fn to replace items', () => { + // const joined = joinByKey( + // [ + // { + // serviceName: 'opbeans-java', + // values: ['a'], + // }, + // { + // serviceName: 'opbeans-node', + // values: ['a'], + // }, + // { + // serviceName: 'opbeans-node', + // values: ['b'], + // }, + // { + // serviceName: 'opbeans-node', + // values: ['c'], + // }, + // ], + // 'serviceName', + // (a, b) => ({ + // ...a, + // ...b, + // values: a.values.concat(b.values), + // }) + // ); + + // expect(joined.find((item) => item.serviceName === 'opbeans-node')?.values).toEqual([ + // 'a', + // 'b', + // 'c', + // ]); + // }); + + // it('deeply merges objects', () => { + // const joined = joinByKey( + // [ + // { + // serviceName: 'opbeans-node', + // properties: { + // foo: '', + // }, + // }, + // { + // serviceName: 'opbeans-node', + // properties: { + // bar: '', + // }, + // }, + // ], + // 'serviceName' + // ); + + // expect(joined[0]).toEqual({ + // serviceName: 'opbeans-node', + // properties: { + // foo: '', + // bar: '', + // }, + // }); + // }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts index 9bc90ee5e91dd..49072b9a1ca02 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts @@ -5,9 +5,26 @@ * 2.0. */ -import { castArray, merge } from 'lodash'; -import stableStringify from 'json-stable-stringify'; import { UnionToIntersection, ValuesType } from 'utility-types'; +import { merge, castArray } from 'lodash'; +import stableStringify from 'json-stable-stringify'; + +export type JoinedReturnType< + T extends Record, + U extends UnionToIntersection +> = Array< + Partial & { + [k in keyof T]: T[k]; + } +>; + +type ArrayOrSingle = T | T[]; + +export function joinByKey< + T extends Record, + U extends UnionToIntersection, + V extends ArrayOrSingle +>(items: T[], key: V): JoinedReturnType; export function joinByKey< T extends Record, @@ -17,6 +34,7 @@ export function joinByKey< X extends (a: T, b: T) => ValuesType >(items: T[], key: V, mergeFn: X): W; +// TODO util already exists in apm and other plugins. Move it to oblt shared export function joinByKey( items: Array>, key: string | string[], diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts index 09dbee6dfd26a..03027430116e6 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_group_by_terms_agg.test.ts @@ -5,73 +5,61 @@ * 2.0. */ -import { EntityType } from '../../../common/entities'; import { getGroupByTermsAgg } from './get_group_by_terms_agg'; +import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; describe('getGroupByTermsAgg', () => { - it('should return an empty object when no fields are provided', () => { - const result = getGroupByTermsAgg([]); + it('should return an empty object when fields is empty', () => { + const fields: IdentityFieldsPerEntityType = new Map(); + const result = getGroupByTermsAgg(fields); expect(result).toEqual({}); }); - it('should return a valid aggregation structure for a single field', () => { - const fields = new Map(['host' as EntityType, 'host.name']); + 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 result = getGroupByTermsAgg(fields); + expect(result).toEqual({ + service: { + composite: { + size: 500, + sources: [ + { 'service.name': { terms: { field: 'service.name' } } }, + { 'service.environment': { terms: { field: 'service.environment' } } }, + ], + }, + }, host: { - terms: { - field: 'host.name', + composite: { size: 500, + sources: [{ 'host.name': { terms: { field: 'host.name' } } }], + }, + }, + container: { + composite: { + size: 500, + sources: [ + { + 'container.id': { + terms: { field: 'container.id' }, + }, + }, + { + 'foo.bar': { terms: { field: 'foo.bar' } }, + }, + ], }, }, }); }); - - // it('should return a valid aggregation structure for multiple fields', () => { - // const result = getGroupByTermsAgg(['host.name', 'service.name']); - // expect(result).toEqual({ - // 'host.name': { - // terms: { - // field: 'host.name', - // size: 500, - // }, - // }, - // 'service.name': { - // terms: { - // field: 'service.name', - // size: 500, - // }, - // }, - // }); - // }); - - // it('should allow overriding the default maxSize value', () => { - // const result = getGroupByTermsAgg(['host.name'], 100); - // expect(result).toEqual({ - // 'host.name': { - // terms: { - // field: 'host.name', - // size: 100, - // }, - // }, - // }); - // }); - - // it('should apply maxSize to all fields', () => { - // const result = getGroupByTermsAgg(['host.name', 'service.name'], 200); - // expect(result).toEqual({ - // 'host.name': { - // terms: { - // field: 'host.name', - // size: 200, - // }, - // }, - // 'service.name': { - // terms: { - // field: 'service.name', - // size: 200, - // }, - // }, - // }); - // }); + it('should override maxSize when provided', () => { + const fields: IdentityFieldsPerEntityType = new Map([['host', ['host.name']]]); + const result = getGroupByTermsAgg(fields, 10); + expect(result.host.composite.size).toBe(10); + }); }); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts index 2fe2988839c63..ad23312c43998 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -34,7 +34,7 @@ describe('getIdentityFields', () => { 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('contaier.id'); + expect(result.get('host')).toEqual(['host.name']); + expect(result.get('container')).toEqual(['contaier.id']); }); }); From 0f5e13dbf8d3b4b8d4342ee47474851aea8a4ba2 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Wed, 9 Oct 2024 12:31:35 +0300 Subject: [PATCH 04/23] Sort alertCount --- .../inventory/common/entities.ts | 13 ++++++++ .../components/entities_grid/grid_columns.tsx | 9 +---- .../public/components/entities_grid/index.tsx | 6 ++-- .../entities_grid/mock/entities_mock.ts | 6 ++++ .../components/search_bar/discover_button.tsx | 3 +- .../inventory/public/routes/config.tsx | 11 ++++--- .../routes/entities/get_latest_entities.ts | 15 ++++++--- .../entities/get_latest_entities_alerts.ts | 5 +-- .../inventory/server/routes/entities/route.ts | 33 +++++++++---------- 9 files changed, 61 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 9d5e934982bed..7e59844a2fdd6 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -28,8 +28,19 @@ export const entityTypeRt = t.union([ t.literal('container'), ]); +export const entityColumnIdsRt = t.union([ + t.literal(ENTITY_DISPLAY_NAME), + t.literal(ENTITY_LAST_SEEN), + t.literal(ENTITY_TYPE), + t.literal('alertsCount'), +]); + +export type EntityColumnIds = t.TypeOf; + export type EntityType = t.TypeOf; +export const DEFAULT_ENTITIES_SORT_FIELD: EntityColumnIds = 'alertsCount'; + export const MAX_NUMBER_OF_ENTITIES = 500; export const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ @@ -49,6 +60,8 @@ export const defaultEntityDefinitions = [ export const defaultEntityTypes: EntityType[] = ['service', 'host', 'container']; +export const defaultEntitySortField = ENTITY_LAST_SEEN; + const entityArrayRt = t.array(entityTypeRt); export const entityTypesRt = new t.Type( 'entityTypesRt', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx index c4564d6a417d4..bf710f8f31bef 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx @@ -7,9 +7,7 @@ import { EuiButtonIcon, EuiDataGridColumn, EuiToolTip } from '@elastic/eui'; import React from 'react'; - import { i18n } from '@kbn/i18n'; - import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, @@ -54,12 +52,6 @@ const entityLastSeenToolip = i18n.translate( } ); -export type EntityColumnIds = - | typeof ENTITY_DISPLAY_NAME - | typeof ENTITY_LAST_SEEN - | typeof ENTITY_TYPE - | 'alertsCount'; - const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipContent: string }) => ( <> {title} @@ -88,6 +80,7 @@ export const getColumns = ({ isSortable: true, display: , initialWidth: 100, + schema: 'numeric', }, ] : []), diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index 3ac2454b6cd2e..d361933a6f4a9 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -26,7 +26,7 @@ import { import { last } from 'lodash'; import React, { useCallback, useMemo } from 'react'; -import { Entity, EntityType } from '../../../common/entities'; +import { Entity, EntityColumnIds, EntityType } from '../../../common/entities'; import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, @@ -36,7 +36,7 @@ import { APIReturnType } from '../../api'; import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; import { parseServiceParams } from '../../utils/parse_service_params'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; -import { EntityColumnIds, getColumns } from './grid_columns'; +import { getColumns } from './grid_columns'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; @@ -110,7 +110,7 @@ export function EntitiesGrid({ ); const showAlertsColumn = useMemo( - () => entities.some((entity: Entity) => entity?.alertsCount && entity.alertsCount > 0), + () => entities?.some((entity: Entity) => entity?.alertsCount && entity?.alertsCount > 0), [entities] ); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts index 10ba7fbe4119e..bf72d5d7832cf 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/mock/entities_mock.ts @@ -15,24 +15,29 @@ export const entitiesMock = [ 'entity.type': 'host', 'entity.displayName': 'Spider-Man', 'entity.id': '0', + alertsCount: 3, }, { 'entity.lastSeenTimestamp': '2024-06-16T21:48:16.259Z', 'entity.type': 'service', 'entity.displayName': 'Iron Man', 'entity.id': '1', + alertsCount: 3, }, + { 'entity.lastSeenTimestamp': '2024-04-28T03:31:57.528Z', 'entity.type': 'host', 'entity.displayName': 'Captain America', 'entity.id': '2', + alertsCount: 10, }, { 'entity.lastSeenTimestamp': '2024-05-14T11:32:04.275Z', 'entity.type': 'host', 'entity.displayName': 'Hulk', 'entity.id': '3', + alertsCount: 1, }, { 'entity.lastSeenTimestamp': '2023-12-05T13:33:54.028Z', @@ -1630,6 +1635,7 @@ export const entitiesMock = [ 'entity.displayName': 'Sed dignissim libero a diam sagittis, in convallis leo pellentesque. Cras ut sapien sed lacus scelerisque vehicula. Pellentesque at purus pulvinar, mollis justo hendrerit, pharetra purus. Morbi dapibus, augue et volutpat ultricies, neque quam sollicitudin mauris, vitae luctus ex libero id erat. Suspendisse risus lectus, scelerisque vel odio sed.', 'entity.id': '269', + alertsCount: 4, }, { 'entity.lastSeenTimestamp': '2023-10-22T13:49:53.092Z', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx index d91ca5bf7d2d9..68abfafd55b2e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx @@ -18,8 +18,7 @@ import { ENTITY_LAST_SEEN, ENTITY_TYPE, } from '../../../common/es_fields/entities'; -import { EntityColumnIds } from '../entities_grid'; -import { defaultEntityDefinitions } from '../../../common/entities'; +import { defaultEntityDefinitions, EntityColumnIds } from '../../../common/entities'; import { useInventoryParams } from '../../hooks/use_inventory_params'; const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; diff --git a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx index 21fe05fb373cd..ad10a762ab3c8 100644 --- a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx @@ -8,10 +8,13 @@ import { toNumberRt } from '@kbn/io-ts-utils'; import { Outlet, createRouter } from '@kbn/typed-react-router-config'; import * as t from 'io-ts'; import React from 'react'; -import { ENTITY_LAST_SEEN } from '../../common/es_fields/entities'; import { InventoryPageTemplate } from '../components/inventory_page_template'; import { InventoryPage } from '../pages/inventory_page'; -import { entityTypesRt } from '../../common/entities'; +import { + DEFAULT_ENTITIES_SORT_FIELD, + entityTypesRt, + entityColumnIdsRt, +} from '../../common/entities'; /** * The array of route definitions to be used when the application @@ -27,7 +30,7 @@ const inventoryRoutes = { params: t.type({ query: t.intersection([ t.type({ - sortField: t.string, + sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), pageIndex: toNumberRt, }), @@ -39,7 +42,7 @@ const inventoryRoutes = { }), defaults: { query: { - sortField: ENTITY_LAST_SEEN, + sortField: DEFAULT_ENTITIES_SORT_FIELD, sortDirection: 'desc', pageIndex: '0', }, diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index 853d52d8401a9..b78d0bd277c4e 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -13,8 +13,10 @@ import { MAX_NUMBER_OF_ENTITIES, type EntityType, Entity, + type EntityColumnIds, } from '../../../common/entities'; import { getEntityDefinitionIdWhereClause, getEntityTypesWhereClause } from './query_helper'; +import { ENTITY_LAST_SEEN } from '../../../common/es_fields/entities'; export async function getLatestEntities({ inventoryEsClient, @@ -25,15 +27,18 @@ export async function getLatestEntities({ }: { inventoryEsClient: ObservabilityElasticsearchClient; sortDirection: 'asc' | 'desc'; - sortField: string; + sortField: EntityColumnIds; entityTypes?: EntityType[]; kuery?: string; }) { - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { + // alertCount doesn't exist in entties. Ignore it and sort by entity.lastSeenTimestamp by default + const entitiesSortField = sortField === 'alertsCount' ? ENTITY_LAST_SEEN : sortField; + + const request = { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getEntityTypesWhereClause(entityTypes)} | ${getEntityDefinitionIdWhereClause()} - | SORT ${sortField} ${sortDirection} + | SORT ${entitiesSortField} ${sortDirection} | LIMIT ${MAX_NUMBER_OF_ENTITIES} `, filter: { @@ -41,7 +46,9 @@ export async function getLatestEntities({ filter: [...kqlQuery(kuery)], }, }, - }); + }; + + const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', request); return esqlResultToPlainObjects(latestEntitiesEsqlResponse); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts index 36afda2465f25..797d4ac93a132 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts @@ -39,10 +39,11 @@ export async function getLatestEntitiesAlerts({ aggs: getGroupByTermsAgg(identityFieldsPerEntityType), }); - const alerts = Array.from(identityFieldsPerEntityType).flatMap(([field, value]) => { - const buckets = response.aggregations?.[field]?.buckets ?? []; + const alerts = Array.from(identityFieldsPerEntityType).flatMap(([entityType]) => { + const buckets = response.aggregations?.[entityType]?.buckets ?? []; return buckets.map((bucket: { key: Record; doc_count: number }) => ({ alertsCount: bucket.doc_count, + type: entityType, ...bucket.key, })); }); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index ff95272a432a6..9a2c038e7b27e 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -8,7 +8,8 @@ import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; -import { entityTypeRt } from '../../../common/entities'; +import { sortBy } from 'lodash'; +import { entityTypeRt, entityColumnIdsRt } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; @@ -40,7 +41,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ params: t.type({ query: t.intersection([ t.type({ - sortField: t.string, + sortField: entityColumnIdsRt, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), }), t.partial({ @@ -62,15 +63,16 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ const { sortDirection, sortField, entityTypes, kuery } = params.query; - const alertsClient = await createAlertsClient({ plugins, request }); - - const latestEntities = await getLatestEntities({ - inventoryEsClient, - sortDirection, - sortField, - entityTypes, - kuery, - }); + const [alertsClient, latestEntities] = await Promise.all([ + createAlertsClient({ plugins, request }), + getLatestEntities({ + inventoryEsClient, + sortDirection, + sortField, + entityTypes, + kuery, + }), + ]); const identityFieldsPerEntityType = getIdentityFieldsPerEntityType(latestEntities); @@ -80,16 +82,13 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ kuery, }); - console.log('identityFieldsPerEntityType', identityFieldsPerEntityType.values()); - console.log('alerts', alerts); const joined = joinByKey( [...latestEntities, ...alerts], - ['service.name', 'service.environment'] - ); + [...identityFieldsPerEntityType.values()].flat() + ).filter((entity) => entity['entity.id']); - console.log('joined', joined); return { - entities: joined, + entities: sortField === 'alertsCount' ? sortBy(joined, sortField, sortDirection) : joined, }; }, }); From 900133b7d39498fd1001bdfc6a7f3a4a63dae73c Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Thu, 10 Oct 2024 12:09:19 +0300 Subject: [PATCH 05/23] use locator --- .../utils/get_identity_fields_values.test.ts | 85 +++++++++++++++++++ .../utils/get_identity_fields_values.ts | 27 ++++++ .../inventory/kibana.jsonc | 1 + .../components/alerts_badge/alerts_badge.tsx | 43 ++++++++++ .../components/entities_grid/grid_columns.tsx | 2 +- .../public/components/entities_grid/index.tsx | 21 +---- 6 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts new file mode 100644 index 0000000000000..d1b2c1c9cfa4d --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Entity } from '../entities'; +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '../es_fields/entities'; +import { getIdentityFieldValues } from './get_identity_fields_values'; + +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 3, +}; + +describe('getIdentityFieldValues', () => { + it('should return the value when identityFields is a single string', () => { + const entity: Entity = { + 'entity.identityFields': 'service.name', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = getIdentityFieldValues({ entity }); + expect(result).toEqual(['service.name: "my-service"']); + }); + + it('should return values when identityFields is an array of strings', () => { + const entity: Entity = { + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + 'service.environment': 'staging', + ...commonEntityFields, + }; + + const result = getIdentityFieldValues({ entity }); + expect(result).toEqual(['service.name: "my-service"', 'service.environment: "staging"']); + }); + + it('should return an empty array if identityFields is empty string', () => { + const entity: Entity = { + 'entity.identityFields': '', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = getIdentityFieldValues({ entity }); + expect(result).toEqual([]); + }); + it('should return an empty array if identityFields is empty array', () => { + const entity: Entity = { + 'entity.identityFields': [], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = getIdentityFieldValues({ entity }); + expect(result).toEqual([]); + }); + + it('should ignore fields that are not present in the entity', () => { + const entity: Entity = { + 'entity.identityFields': ['host.name', 'foo.bar'], + 'host.name': 'my-host', + 'entity.type': 'host', + ...commonEntityFields, + }; + + const result = getIdentityFieldValues({ entity }); + expect(result).toEqual(['host.name: "my-host"']); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts new file mode 100644 index 0000000000000..74429f703e3d9 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts @@ -0,0 +1,27 @@ +/* + * Copyright 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 { Entity } from '../entities'; +import { ENTITY_IDENTITY_FIELDS } from '../es_fields/entities'; + +export function getIdentityFieldValues({ entity }: { entity: Entity }) { + const mapping: string[] = []; + + const identityFields = entity[ENTITY_IDENTITY_FIELDS]; + + if (identityFields) { + const fields = Array.isArray(identityFields) ? identityFields : [identityFields]; + + fields.forEach((field) => { + if (field in entity) { + mapping.push(`${[field]}: "${entity[field as keyof Entity]}"`); + } + }); + } + + return mapping; +} diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc index 789ab4fa670dc..6a7eaa97a6e6d 100644 --- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -9,6 +9,7 @@ "configPath": ["xpack", "inventory"], "requiredPlugins": [ "observabilityShared", + "observability", "entityManager", "inference", "dataViews", diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx new file mode 100644 index 0000000000000..4d44e0c2faa07 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { alertsLocatorID } from '@kbn/observability-plugin/common'; +import { Entity } from '../../../common/entities'; +import { getIdentityFieldValues } from '../../../common/utils/get_identity_fields_values'; +import { useKibana } from '../../hooks/use_kibana'; + +export function AlertsBadge({ entity }: { entity: Entity }) { + const identityFieldValues = getIdentityFieldValues({ entity }); + const { + services: { share }, + } = useKibana(); + + const alertsLocator = share.url.locators.get(alertsLocatorID); + + const alertsLink = alertsLocator?.getRedirectUrl({ + kuery: identityFieldValues.join(' AND '), + status: 'active', + }); + + return ( + + + {entity.alertsCount} + + + ); +} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx index bf710f8f31bef..798fd29adb450 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx @@ -19,7 +19,7 @@ const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.ale }); const alertsTooltip = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip', { - defaultMessage: 'Active alerts', + defaultMessage: 'The count of the active alerts', }); const entityNameLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel', { diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index d361933a6f4a9..89bb2e11cb206 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -5,14 +5,12 @@ * 2.0. */ import { - EuiBadge, EuiDataGrid, EuiDataGridCellValueElementProps, EuiDataGridSorting, EuiLink, EuiLoadingSpinner, EuiText, - EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; @@ -37,6 +35,7 @@ import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; import { parseServiceParams } from '../../utils/parse_service_params'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; import { getColumns } from './grid_columns'; +import { AlertsBadge } from '../alerts_badge/alerts_badge'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; @@ -129,7 +128,7 @@ export function EntitiesGrid({ const renderCellValue = useCallback( ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { - const entity = entities[rowIndex]; + const entity = entities[rowIndex] as Entity; if (entity === undefined) { return null; } @@ -137,21 +136,7 @@ export function EntitiesGrid({ const columnEntityTableId = columnId as EntityColumnIds; switch (columnEntityTableId) { case 'alertsCount': - return entity?.alertsCount ? ( - - - {entity.alertsCount} - - - ) : null; + return entity?.alertsCount ? : null; case ENTITY_TYPE: const entityType = entity[columnEntityTableId] as EntityType; From c4550f9517ce19bf775e2892540c397ef3a944cb Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Thu, 10 Oct 2024 12:29:01 +0300 Subject: [PATCH 06/23] fix broken path --- .../observability_solution/inventory/common/entities.ts | 8 +++----- .../inventory/common/utils/get_identity_fields_values.ts | 2 +- .../public/components/entities_grid/grid_columns.tsx | 2 +- .../server/routes/entities/get_identify_fields.test.ts | 2 +- .../entities/get_identity_fields_per_entity_type.ts | 2 +- .../server/routes/entities/get_latest_entities.ts | 2 +- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index e3e17e9e01001..0e5afbfdf5890 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -12,17 +12,15 @@ import { AGENT_NAME, CLOUD_PROVIDER, CONTAINER_ID, -} from '@kbn/observability-shared-plugin/common'; -import { isRight } from 'fp-ts/lib/Either'; -import * as t from 'io-ts'; -import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, -} from './es_fields/entities'; +} from '@kbn/observability-shared-plugin/common'; +import { isRight } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; export const entityTypeRt = t.union([ t.literal('service'), diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts index 74429f703e3d9..eacf5d1893771 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; import { Entity } from '../entities'; -import { ENTITY_IDENTITY_FIELDS } from '../es_fields/entities'; export function getIdentityFieldValues({ entity }: { entity: Entity }) { const mapping: string[] = []; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx index 798fd29adb450..96fb8b3736ead 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/grid_columns.tsx @@ -12,7 +12,7 @@ import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, ENTITY_TYPE, -} from '../../../common/es_fields/entities'; +} from '@kbn/observability-shared-plugin/common'; const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel', { defaultMessage: 'Alerts', diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts index ad23312c43998..bdb2b5d907288 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -6,7 +6,7 @@ */ import { EntityType } from '../../../common/entities'; // Adjust the import path accordingly -import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '../../../common/es_fields/entities'; +import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; describe('getIdentityFields', () => { diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts index 891f149168c95..3e649b05af034 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { Entity, EntityType } from '../../../common/entities'; -import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '../../../common/es_fields/entities'; export type IdentityFieldsPerEntityType = Map; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index b78d0bd277c4e..d14a3cfcdfd16 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -8,6 +8,7 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, @@ -16,7 +17,6 @@ import { type EntityColumnIds, } from '../../../common/entities'; import { getEntityDefinitionIdWhereClause, getEntityTypesWhereClause } from './query_helper'; -import { ENTITY_LAST_SEEN } from '../../../common/es_fields/entities'; export async function getLatestEntities({ inventoryEsClient, From 57598d05fbc27b5ef1c2654508719e4bd8069879 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Thu, 10 Oct 2024 16:46:45 +0300 Subject: [PATCH 07/23] Change app route from `/app/observability/inventory` to `/app/inventory` --- .../plugins/observability_solution/inventory/public/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/plugin.ts b/x-pack/plugins/observability_solution/inventory/public/plugin.ts index c02a57b45f691..8f81b3ebdad8e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/plugin.ts +++ b/x-pack/plugins/observability_solution/inventory/public/plugin.ts @@ -91,7 +91,7 @@ export class InventoryPlugin defaultMessage: 'Inventory', }), euiIconType: 'logoObservability', - appRoute: '/app/observability/inventory', + appRoute: '/app/inventory', category: DEFAULT_APP_CATEGORIES.observability, visibleIn: ['sideNav', 'globalSearch'], order: 8200, From a2f4a723ef41c22ee0998bb2d2cb4c387d3631c9 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Thu, 10 Oct 2024 18:33:59 +0300 Subject: [PATCH 08/23] Remove alerts locator and add test --- .../inventory/kibana.jsonc | 1 - .../alerts_badge/alerts_badge.test.tsx | 86 +++++++++++++++++++ .../components/alerts_badge/alerts_badge.tsx | 27 +++--- 3 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx diff --git a/x-pack/plugins/observability_solution/inventory/kibana.jsonc b/x-pack/plugins/observability_solution/inventory/kibana.jsonc index 6a7eaa97a6e6d..789ab4fa670dc 100644 --- a/x-pack/plugins/observability_solution/inventory/kibana.jsonc +++ b/x-pack/plugins/observability_solution/inventory/kibana.jsonc @@ -9,7 +9,6 @@ "configPath": ["xpack", "inventory"], "requiredPlugins": [ "observabilityShared", - "observability", "entityManager", "inference", "dataViews", diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx new file mode 100644 index 0000000000000..c60490c8a12b1 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { type KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; +import { render, screen } from '@testing-library/react'; +import { AlertsBadge } from './alerts_badge'; +import * as useKibana from '../../hooks/use_kibana'; +import { HostEntity, ServiceEntity } from '../../../common/entities'; + +describe('AlertsBadge', () => { + jest.spyOn(useKibana, 'useKibana').mockReturnValue({ + services: { + http: { + basePath: { + prepend: (path: string) => path, + }, + }, + }, + } as unknown as KibanaReactContextValue); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('render alerts badge for a host entity', () => { + const entity: HostEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'entity.id': '1', + 'entity.type': 'host', + 'entity.displayName': 'foo', + 'entity.identityFields': 'host.name', + 'host.name': 'foo', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 1, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'host.name: "foo"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); + }); + it('render alerts badge for a service entity', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'agent.name': 'node', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': 'service.name', + 'service.name': 'bar', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 5, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'service.name: "bar"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); + }); + it('render alerts badge for a service entity with multiple identity fields', () => { + const entity: ServiceEntity = { + 'entity.lastSeenTimestamp': 'foo', + 'agent.name': 'node', + 'entity.id': '1', + 'entity.type': 'service', + 'entity.displayName': 'foo', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'bar', + 'service.environment': 'prod', + 'entity.definitionId': 'host', + 'cloud.provider': null, + alertsCount: 2, + }; + render(); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.getAttribute('href')).toEqual( + '/app/observability/alerts?_a=(kuery:\'service.name: "bar" AND service.environment: "prod"\',status:active)' + ); + expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('2'); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx index 4d44e0c2faa07..294bd39c0c33b 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -5,9 +5,9 @@ * 2.0. */ import React from 'react'; +import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { alertsLocatorID } from '@kbn/observability-plugin/common'; import { Entity } from '../../../common/entities'; import { getIdentityFieldValues } from '../../../common/utils/get_identity_fields_values'; import { useKibana } from '../../hooks/use_kibana'; @@ -15,16 +15,17 @@ import { useKibana } from '../../hooks/use_kibana'; export function AlertsBadge({ entity }: { entity: Entity }) { const identityFieldValues = getIdentityFieldValues({ entity }); const { - services: { share }, + services: { + http: { basePath }, + }, } = useKibana(); - const alertsLocator = share.url.locators.get(alertsLocatorID); - - const alertsLink = alertsLocator?.getRedirectUrl({ - kuery: identityFieldValues.join(' AND '), - status: 'active', - }); - + const activeAlertsHref = basePath.prepend( + `/app/observability/alerts?_a=${rison.encode({ + kuery: identityFieldValues.join(' AND '), + status: 'active', + })}` + ); return ( - + {entity.alertsCount} From 01831a97d28f9dd6a40455ae33e01706d68139f0 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Thu, 10 Oct 2024 18:58:07 +0300 Subject: [PATCH 09/23] Fix sorting with undefined aletsCount entities --- .../inventory/server/routes/entities/route.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index 9a2c038e7b27e..2109039891055 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -8,7 +8,7 @@ import { INVENTORY_APP_ID } from '@kbn/deeplinks-observability/constants'; import { jsonRt } from '@kbn/io-ts-utils'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; -import { sortBy } from 'lodash'; +import { orderBy } from 'lodash'; import { entityTypeRt, entityColumnIdsRt } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; @@ -88,7 +88,14 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ ).filter((entity) => entity['entity.id']); return { - entities: sortField === 'alertsCount' ? sortBy(joined, sortField, sortDirection) : joined, + entities: + sortField === 'alertsCount' + ? orderBy( + joined, + [(item) => item?.alertsCount === undefined, sortField], + ['asc', sortDirection] // push entities without alertsCount to the end + ) + : joined, }; }, }); From 0e5b0b649993834f6a7442ff3f51054388f7e407 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Thu, 10 Oct 2024 19:08:15 +0300 Subject: [PATCH 10/23] clean up --- .../common/utils/join_by_key.test.ts | 314 +++++++++--------- 1 file changed, 157 insertions(+), 157 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts index 14988f7ef27e8..8e0fc6ad09479 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts @@ -8,101 +8,101 @@ import { joinByKey } from './join_by_key'; describe('joinByKey', () => { - // it('joins by a string key', () => { - // const joined = joinByKey( - // [ - // { - // serviceName: 'opbeans-node', - // avg: 10, - // }, - // { - // serviceName: 'opbeans-node', - // count: 12, - // }, - // { - // serviceName: 'opbeans-java', - // avg: 11, - // }, - // { - // serviceName: 'opbeans-java', - // p95: 18, - // }, - // ], - // 'serviceName' - // ); - - // expect(joined.length).toBe(2); - - // expect(joined).toEqual([ - // { - // serviceName: 'opbeans-node', - // avg: 10, - // count: 12, - // }, - // { - // serviceName: 'opbeans-java', - // avg: 11, - // p95: 18, - // }, - // ]); - // }); - - // it('joins by a record key', () => { - // const joined = joinByKey( - // [ - // { - // key: { - // serviceName: 'opbeans-node', - // transactionName: '/api/opbeans-node', - // }, - // avg: 10, - // }, - // { - // key: { - // serviceName: 'opbeans-node', - // transactionName: '/api/opbeans-node', - // }, - // count: 12, - // }, - // { - // key: { - // serviceName: 'opbeans-java', - // transactionName: '/api/opbeans-java', - // }, - // avg: 11, - // }, - // { - // key: { - // serviceName: 'opbeans-java', - // transactionName: '/api/opbeans-java', - // }, - // p95: 18, - // }, - // ], - // 'key' - // ); - - // expect(joined.length).toBe(2); - - // expect(joined).toEqual([ - // { - // key: { - // serviceName: 'opbeans-node', - // transactionName: '/api/opbeans-node', - // }, - // avg: 10, - // count: 12, - // }, - // { - // key: { - // serviceName: 'opbeans-java', - // transactionName: '/api/opbeans-java', - // }, - // avg: 11, - // p95: 18, - // }, - // ]); - // }); + it('joins by a string key', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + avg: 10, + }, + { + serviceName: 'opbeans-node', + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + }, + { + serviceName: 'opbeans-java', + p95: 18, + }, + ], + 'serviceName' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + serviceName: 'opbeans-node', + avg: 10, + count: 12, + }, + { + serviceName: 'opbeans-java', + avg: 11, + p95: 18, + }, + ]); + }); + + it('joins by a record key', () => { + const joined = joinByKey( + [ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + }, + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + p95: 18, + }, + ], + 'key' + ); + + expect(joined.length).toBe(2); + + expect(joined).toEqual([ + { + key: { + serviceName: 'opbeans-node', + transactionName: '/api/opbeans-node', + }, + avg: 10, + count: 12, + }, + { + key: { + serviceName: 'opbeans-java', + transactionName: '/api/opbeans-java', + }, + avg: 11, + p95: 18, + }, + ]); + }); it('joins by multiple keys', () => { const data = [ @@ -159,66 +159,66 @@ describe('joinByKey', () => { ]); }); - // it('uses the custom merge fn to replace items', () => { - // const joined = joinByKey( - // [ - // { - // serviceName: 'opbeans-java', - // values: ['a'], - // }, - // { - // serviceName: 'opbeans-node', - // values: ['a'], - // }, - // { - // serviceName: 'opbeans-node', - // values: ['b'], - // }, - // { - // serviceName: 'opbeans-node', - // values: ['c'], - // }, - // ], - // 'serviceName', - // (a, b) => ({ - // ...a, - // ...b, - // values: a.values.concat(b.values), - // }) - // ); - - // expect(joined.find((item) => item.serviceName === 'opbeans-node')?.values).toEqual([ - // 'a', - // 'b', - // 'c', - // ]); - // }); - - // it('deeply merges objects', () => { - // const joined = joinByKey( - // [ - // { - // serviceName: 'opbeans-node', - // properties: { - // foo: '', - // }, - // }, - // { - // serviceName: 'opbeans-node', - // properties: { - // bar: '', - // }, - // }, - // ], - // 'serviceName' - // ); - - // expect(joined[0]).toEqual({ - // serviceName: 'opbeans-node', - // properties: { - // foo: '', - // bar: '', - // }, - // }); - // }); + it('uses the custom merge fn to replace items', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-java', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['a'], + }, + { + serviceName: 'opbeans-node', + values: ['b'], + }, + { + serviceName: 'opbeans-node', + values: ['c'], + }, + ], + 'serviceName', + (a, b) => ({ + ...a, + ...b, + values: a.values.concat(b.values), + }) + ); + + expect(joined.find((item) => item.serviceName === 'opbeans-node')?.values).toEqual([ + 'a', + 'b', + 'c', + ]); + }); + + it('deeply merges objects', () => { + const joined = joinByKey( + [ + { + serviceName: 'opbeans-node', + properties: { + foo: '', + }, + }, + { + serviceName: 'opbeans-node', + properties: { + bar: '', + }, + }, + ], + 'serviceName' + ); + + expect(joined[0]).toEqual({ + serviceName: 'opbeans-node', + properties: { + foo: '', + bar: '', + }, + }); + }); }); From 72a02ce05bdbdab3b171cd1d2f168e5b01c789c4 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:23:07 +0000 Subject: [PATCH 11/23] [CI] Auto-commit changed files from 'node scripts/yarn_deduplicate' --- .../plugins/observability_solution/inventory/tsconfig.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index 54fcfe7e3a11f..ea5875bb8025d 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -45,6 +45,10 @@ "@kbn/config-schema", "@kbn/elastic-agent-utils", "@kbn/custom-icons", - "@kbn/ui-theme" + "@kbn/ui-theme", + "@kbn/rison", + "@kbn/rule-registry-plugin", + "@kbn/observability-plugin", + "@kbn/rule-data-utils" ] } From c284f667084d8c19ec3de3d4b64001d0170778d8 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:24:33 +0000 Subject: [PATCH 12/23] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../routes/entities/get_inventory_entities_list.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts index e2a5b357de4a2..0241902787d98 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts @@ -6,14 +6,7 @@ */ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; -import { - ENTITIES_LATEST_ALIAS, - MAX_NUMBER_OF_ENTITIES, - type EntityType, - Entity, -} from '../../../common/entities'; +import { type EntityType } from '../../../common/entities'; export async function getInventoryEntitiesList({ inventoryEsClient, From b9781e940d3cb6d151b949188b49c4fb045e900d Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 11:11:48 +0300 Subject: [PATCH 13/23] clean up --- .../server/routes/entities/get_identify_fields.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts index bdb2b5d907288..ecd28958132b5 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { EntityType } from '../../../common/entities'; // Adjust the import path accordingly +import { EntityType } from '../../../common/entities'; import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; From 6555f9ed304dca549547a5ab4b85d021e9b3344d Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 11:16:14 +0300 Subject: [PATCH 14/23] Fix another broken path --- .../inventory/common/utils/get_identity_fields_values.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts index d1b2c1c9cfa4d..7831ff339e203 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { Entity } from '../entities'; import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, ENTITY_LAST_SEEN, -} from '../es_fields/entities'; +} from '@kbn/observability-shared-plugin/common'; +import { Entity } from '../entities'; import { getIdentityFieldValues } from './get_identity_fields_values'; const commonEntityFields = { From b396dae4d2532758825d438f6617002d60b31a68 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 11:16:14 +0300 Subject: [PATCH 15/23] Fix another broken path --- .../utils/get_identity_fields_values.test.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts index d1b2c1c9cfa4d..89093a689bd48 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { Entity } from '../entities'; import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, ENTITY_LAST_SEEN, -} from '../es_fields/entities'; +} from '@kbn/observability-shared-plugin/common'; +import { HostEntity, ServiceEntity } from '../entities'; import { getIdentityFieldValues } from './get_identity_fields_values'; const commonEntityFields = { @@ -24,7 +24,8 @@ const commonEntityFields = { describe('getIdentityFieldValues', () => { it('should return the value when identityFields is a single string', () => { - const entity: Entity = { + const entity: ServiceEntity = { + 'agent.name': 'node', 'entity.identityFields': 'service.name', 'service.name': 'my-service', 'entity.type': 'service', @@ -36,7 +37,8 @@ describe('getIdentityFieldValues', () => { }); it('should return values when identityFields is an array of strings', () => { - const entity: Entity = { + const entity: ServiceEntity = { + 'agent.name': 'node', 'entity.identityFields': ['service.name', 'service.environment'], 'service.name': 'my-service', 'entity.type': 'service', @@ -49,7 +51,8 @@ describe('getIdentityFieldValues', () => { }); it('should return an empty array if identityFields is empty string', () => { - const entity: Entity = { + const entity: ServiceEntity = { + 'agent.name': 'node', 'entity.identityFields': '', 'service.name': 'my-service', 'entity.type': 'service', @@ -60,7 +63,8 @@ describe('getIdentityFieldValues', () => { expect(result).toEqual([]); }); it('should return an empty array if identityFields is empty array', () => { - const entity: Entity = { + const entity: ServiceEntity = { + 'agent.name': 'node', 'entity.identityFields': [], 'service.name': 'my-service', 'entity.type': 'service', @@ -72,10 +76,11 @@ describe('getIdentityFieldValues', () => { }); it('should ignore fields that are not present in the entity', () => { - const entity: Entity = { + const entity: HostEntity = { 'entity.identityFields': ['host.name', 'foo.bar'], 'host.name': 'my-host', 'entity.type': 'host', + 'cloud.provider': null, ...commonEntityFields, }; From 082682b575dc23647100724b87eb497731aecdf1 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 12:53:20 +0300 Subject: [PATCH 16/23] Fix types --- .../public/components/entities_grid/index.tsx | 2 +- .../public/pages/inventory_page/index.tsx | 4 +-- .../entities/get_inventory_entities_list.ts | 25 ------------------- .../entities/get_latest_entities_alerts.ts | 19 +++++++++++--- .../inventory/server/routes/entities/route.ts | 4 +-- 5 files changed, 21 insertions(+), 33 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index bb81f0b770c52..a6e40d372f522 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -65,7 +65,7 @@ export function EntitiesGrid({ ); const showAlertsColumn = useMemo( - () => entities?.some((entity: Entity) => entity?.alertsCount && entity?.alertsCount > 0), + () => entities?.some((entity) => entity?.alertsCount && entity?.alertsCount > 0), [entities] ); diff --git a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx index 7af9a9fc21acc..965434eeac6d1 100644 --- a/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/pages/inventory_page/index.tsx @@ -7,7 +7,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; import React from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { EntityType } from '../../../common/entities'; +import { EntityColumnIds, EntityType } from '../../../common/entities'; import { EntitiesGrid } from '../../components/entities_grid'; import { useInventorySearchBarContext } from '../../context/inventory_search_bar_context_provider'; import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; @@ -76,7 +76,7 @@ export function InventoryPage() { path: {}, query: { ...query, - sortField: sorting.id, + sortField: sorting.id as EntityColumnIds, sortDirection: sorting.direction, }, }); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.ts deleted file mode 100644 index 0241902787d98..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_inventory_entities_list.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 ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { type EntityType } from '../../../common/entities'; - -export async function getInventoryEntitiesList({ - inventoryEsClient, - sortDirection, - sortField, - entityTypes, - kuery, - alertsClient, -}: { - inventoryEsClient: ObservabilityElasticsearchClient; - sortDirection: 'asc' | 'desc'; - sortField: string; - entityTypes?: EntityType[]; - kuery?: string; - alertsClient: AlertsClient; -}) {} diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts index 797d4ac93a132..4e6ce545a079e 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities_alerts.ts @@ -10,6 +10,14 @@ import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import { AlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; import { getGroupByTermsAgg } from './get_group_by_terms_agg'; import { IdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; +import { EntityType } from '../../../common/entities'; + +interface Bucket { + key: Record; + doc_count: number; +} + +type EntityTypeBucketsAggregation = Record; export async function getLatestEntitiesAlerts({ alertsClient, @@ -19,7 +27,7 @@ export async function getLatestEntitiesAlerts({ alertsClient: AlertsClient; kuery?: string; identityFieldsPerEntityType: IdentityFieldsPerEntityType; -}) { +}): Promise> { if (identityFieldsPerEntityType.size === 0) { return []; } @@ -39,9 +47,14 @@ export async function getLatestEntitiesAlerts({ aggs: getGroupByTermsAgg(identityFieldsPerEntityType), }); + const aggregations = response.aggregations as EntityTypeBucketsAggregation; + const alerts = Array.from(identityFieldsPerEntityType).flatMap(([entityType]) => { - const buckets = response.aggregations?.[entityType]?.buckets ?? []; - return buckets.map((bucket: { key: Record; doc_count: number }) => ({ + const entityAggregation = aggregations?.[entityType]; + + const buckets = entityAggregation.buckets ?? []; + + return buckets.map((bucket: Bucket) => ({ alertsCount: bucket.doc_count, type: entityType, ...bucket.key, diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index 2109039891055..311338c12cebd 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -9,7 +9,7 @@ import { jsonRt } from '@kbn/io-ts-utils'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; import { orderBy } from 'lodash'; -import { entityTypeRt, entityColumnIdsRt } from '../../../common/entities'; +import { entityTypeRt, entityColumnIdsRt, Entity } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; @@ -92,7 +92,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ sortField === 'alertsCount' ? orderBy( joined, - [(item) => item?.alertsCount === undefined, sortField], + [(item: Entity) => item?.alertsCount === undefined, sortField], ['asc', sortDirection] // push entities without alertsCount to the end ) : joined, From 6800888ce55f5c873bd703d274a8efb6d1d3c21b Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 16:40:54 +0300 Subject: [PATCH 17/23] Move joinByKey to observability-utils --- .../observability_utils/array}/join_by_key.test.ts | 0 .../observability/observability_utils/array}/join_by_key.ts | 1 - .../inventory/server/routes/entities/route.ts | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) rename x-pack/{plugins/observability_solution/inventory/common/utils => packages/observability/observability_utils/array}/join_by_key.test.ts (100%) rename x-pack/{plugins/observability_solution/inventory/common/utils => packages/observability/observability_utils/array}/join_by_key.ts (96%) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts similarity index 100% rename from x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.test.ts rename to x-pack/packages/observability/observability_utils/array/join_by_key.test.ts diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts similarity index 96% rename from x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts rename to x-pack/packages/observability/observability_utils/array/join_by_key.ts index 49072b9a1ca02..54e8ecdaf409b 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/join_by_key.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -34,7 +34,6 @@ export function joinByKey< X extends (a: T, b: T) => ValuesType >(items: T[], key: V, mergeFn: X): W; -// TODO util already exists in apm and other plugins. Move it to oblt shared export function joinByKey( items: Array>, key: string | string[], diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts index 311338c12cebd..eb80f80d02730 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/route.ts @@ -9,6 +9,7 @@ import { jsonRt } from '@kbn/io-ts-utils'; import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import * as t from 'io-ts'; import { orderBy } from 'lodash'; +import { joinByKey } from '@kbn/observability-utils/array/join_by_key'; import { entityTypeRt, entityColumnIdsRt, Entity } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; @@ -16,7 +17,6 @@ import { getLatestEntities } from './get_latest_entities'; import { createAlertsClient } from '../../lib/create_alerts_client.ts/create_alerts_client'; import { getLatestEntitiesAlerts } from './get_latest_entities_alerts'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; -import { joinByKey } from '../../../common/utils/join_by_key'; export const getEntityTypesRoute = createInventoryServerRoute({ endpoint: 'GET /internal/inventory/entities/types', From 9197530b2ea7b3ce3aea72d572cb07e98dab22fe Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 16:57:23 +0300 Subject: [PATCH 18/23] Address PR comments --- .../common/utils/join_by_key/index.test.ts | 47 ---------- .../inventory/common/entities.ts | 4 +- ...t_identity_field_value_pair_to_kql.test.ts | 90 +++++++++++++++++++ ... get_identity_field_value_pairs_to_kql.ts} | 4 +- .../utils/get_identity_fields_values.test.ts | 14 +-- .../components/alerts_badge/alerts_badge.tsx | 4 +- .../public/components/entities_grid/index.tsx | 15 ++-- .../inventory/public/routes/config.tsx | 8 +- 8 files changed, 109 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts rename x-pack/plugins/observability_solution/inventory/common/utils/{get_identity_fields_values.ts => get_identity_field_value_pairs_to_kql.ts} (80%) diff --git a/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts b/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts index 70314e5e182d3..5938d952cb42f 100644 --- a/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts +++ b/x-pack/plugins/observability_solution/apm/common/utils/join_by_key/index.test.ts @@ -104,53 +104,6 @@ describe('joinByKey', () => { ]); }); - it('joins by multiple keys', () => { - const joined = joinByKey( - [ - { - serviceName: 'opbeans-node', - environment: 'production', - alertCount: 10, - }, - { - serviceName: 'opbeans-node', - environment: 'production', - }, - { - serviceName: 'opbeans-node', - environment: 'staging', - }, - { - hostName: 'my-host', - cloudProvider: 'aws', - }, - { - hostName: 'my-host', - alertCount: 10, - serviceName: 'opbeans-node', - }, - ], - ['serviceName', 'environment', 'hostName'] - ); - - expect(joined).toEqual([ - { - serviceName: 'opbeans-node', - environment: 'production', - alertCount: 10, - }, - { - serviceName: 'opbeans-node', - environment: 'staging', - }, - { - hostName: 'my-host', - cloudProvider: 'aws', - alertCount: 10, - }, - ]); - }); - it('uses the custom merge fn to replace items', () => { const joined = joinByKey( [ diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 0e5afbfdf5890..7df71559aa97a 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -39,7 +39,7 @@ export type EntityColumnIds = t.TypeOf; export type EntityType = t.TypeOf; -export const DEFAULT_ENTITIES_SORT_FIELD: EntityColumnIds = 'alertsCount'; +export const defaultEntitySortField: EntityColumnIds = 'alertsCount'; export const MAX_NUMBER_OF_ENTITIES = 500; @@ -60,8 +60,6 @@ export const defaultEntityDefinitions = [ export const defaultEntityTypes: EntityType[] = ['service', 'host', 'container']; -export const defaultEntitySortField = ENTITY_LAST_SEEN; - const entityArrayRt = t.array(entityTypeRt); export const entityTypesRt = new t.Type( 'entityTypesRt', diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts new file mode 100644 index 0000000000000..c6611a1f2d02a --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; +import { HostEntity, ServiceEntity } from '../entities'; +import { getIdentityFieldValuePairsToKql } from './get_identity_field_value_pairs_to_kql'; + +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 3, +}; + +describe('getIdentityFieldValuePairsToKql', () => { + it('should return the value when identityFields is a single string', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': 'service.name', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = getIdentityFieldValuePairsToKql({ entity }); + expect(result).toEqual(['service.name: "my-service"']); + }); + + it('should return values when identityFields is an array of strings', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + 'service.environment': 'staging', + ...commonEntityFields, + }; + + const result = getIdentityFieldValuePairsToKql({ entity }); + expect(result).toEqual(['service.name: "my-service"', 'service.environment: "staging"']); + }); + + it('should return an empty array if identityFields is empty string', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': '', + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = getIdentityFieldValuePairsToKql({ entity }); + expect(result).toEqual([]); + }); + it('should return an empty array if identityFields is empty array', () => { + const entity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': [], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const result = getIdentityFieldValuePairsToKql({ entity }); + expect(result).toEqual([]); + }); + + it('should ignore fields that are not present in the entity', () => { + const entity: HostEntity = { + 'entity.identityFields': ['host.name', 'foo.bar'], + 'host.name': 'my-host', + 'entity.type': 'host', + 'cloud.provider': null, + ...commonEntityFields, + }; + + const result = getIdentityFieldValuePairsToKql({ entity }); + expect(result).toEqual(['host.name: "my-host"']); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pairs_to_kql.ts similarity index 80% rename from x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts rename to x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pairs_to_kql.ts index eacf5d1893771..aa3a1c6ac5187 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pairs_to_kql.ts @@ -8,13 +8,13 @@ import { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; import { Entity } from '../entities'; -export function getIdentityFieldValues({ entity }: { entity: Entity }) { +export function getIdentityFieldValuePairsToKql({ entity }: { entity: Entity }) { const mapping: string[] = []; const identityFields = entity[ENTITY_IDENTITY_FIELDS]; if (identityFields) { - const fields = Array.isArray(identityFields) ? identityFields : [identityFields]; + const fields = [identityFields].flat(); fields.forEach((field) => { if (field in entity) { diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts index 89093a689bd48..c6611a1f2d02a 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts @@ -12,7 +12,7 @@ import { ENTITY_LAST_SEEN, } from '@kbn/observability-shared-plugin/common'; import { HostEntity, ServiceEntity } from '../entities'; -import { getIdentityFieldValues } from './get_identity_fields_values'; +import { getIdentityFieldValuePairsToKql } from './get_identity_field_value_pairs_to_kql'; const commonEntityFields = { [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', @@ -22,7 +22,7 @@ const commonEntityFields = { alertCount: 3, }; -describe('getIdentityFieldValues', () => { +describe('getIdentityFieldValuePairsToKql', () => { it('should return the value when identityFields is a single string', () => { const entity: ServiceEntity = { 'agent.name': 'node', @@ -32,7 +32,7 @@ describe('getIdentityFieldValues', () => { ...commonEntityFields, }; - const result = getIdentityFieldValues({ entity }); + const result = getIdentityFieldValuePairsToKql({ entity }); expect(result).toEqual(['service.name: "my-service"']); }); @@ -46,7 +46,7 @@ describe('getIdentityFieldValues', () => { ...commonEntityFields, }; - const result = getIdentityFieldValues({ entity }); + const result = getIdentityFieldValuePairsToKql({ entity }); expect(result).toEqual(['service.name: "my-service"', 'service.environment: "staging"']); }); @@ -59,7 +59,7 @@ describe('getIdentityFieldValues', () => { ...commonEntityFields, }; - const result = getIdentityFieldValues({ entity }); + const result = getIdentityFieldValuePairsToKql({ entity }); expect(result).toEqual([]); }); it('should return an empty array if identityFields is empty array', () => { @@ -71,7 +71,7 @@ describe('getIdentityFieldValues', () => { ...commonEntityFields, }; - const result = getIdentityFieldValues({ entity }); + const result = getIdentityFieldValuePairsToKql({ entity }); expect(result).toEqual([]); }); @@ -84,7 +84,7 @@ describe('getIdentityFieldValues', () => { ...commonEntityFields, }; - const result = getIdentityFieldValues({ entity }); + const result = getIdentityFieldValuePairsToKql({ entity }); expect(result).toEqual(['host.name: "my-host"']); }); }); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx index 294bd39c0c33b..2dc9269716d6e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -9,11 +9,11 @@ import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Entity } from '../../../common/entities'; -import { getIdentityFieldValues } from '../../../common/utils/get_identity_fields_values'; import { useKibana } from '../../hooks/use_kibana'; +import { getIdentityFieldValuePairsToKql } from '../../../common/utils/get_identity_field_value_pairs_to_kql'; export function AlertsBadge({ entity }: { entity: Entity }) { - const identityFieldValues = getIdentityFieldValues({ entity }); + const identityFieldValues = getIdentityFieldValuePairsToKql({ entity }); const { services: { http: { basePath }, diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index a6e40d372f522..74fa54a430fc7 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -20,7 +20,7 @@ import { ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; -import { Entity, EntityColumnIds, EntityType } from '../../../common/entities'; +import { EntityColumnIds, EntityType } from '../../../common/entities'; import { APIReturnType } from '../../api'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; import { getColumns } from './grid_columns'; @@ -35,7 +35,7 @@ interface Props { loading: boolean; entities: LatestEntities; sortDirection: 'asc' | 'desc'; - sortField: string; + sortField: EntityColumnIds; pageIndex: number; onChangeSort: (sorting: EuiDataGridSorting['columns'][0]) => void; onChangePage: (nextPage: number) => void; @@ -69,22 +69,17 @@ export function EntitiesGrid({ [entities] ); - const visibleColumns = useMemo( - () => getColumns({ showAlertsColumn }).map(({ id }) => id), - [showAlertsColumn] - ); - const columnVisibility = useMemo( () => ({ - visibleColumns, + visibleColumns: getColumns({ showAlertsColumn }).map(({ id }) => id), setVisibleColumns: () => {}, }), - [visibleColumns] + [showAlertsColumn] ); const renderCellValue = useCallback( ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { - const entity = entities[rowIndex] as Entity; + const entity = entities[rowIndex]; if (entity === undefined) { return null; } diff --git a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx index ad10a762ab3c8..dc7ba13451e02 100644 --- a/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/routes/config.tsx @@ -10,11 +10,7 @@ import * as t from 'io-ts'; import React from 'react'; import { InventoryPageTemplate } from '../components/inventory_page_template'; import { InventoryPage } from '../pages/inventory_page'; -import { - DEFAULT_ENTITIES_SORT_FIELD, - entityTypesRt, - entityColumnIdsRt, -} from '../../common/entities'; +import { defaultEntitySortField, entityTypesRt, entityColumnIdsRt } from '../../common/entities'; /** * The array of route definitions to be used when the application @@ -42,7 +38,7 @@ const inventoryRoutes = { }), defaults: { query: { - sortField: DEFAULT_ENTITIES_SORT_FIELD, + sortField: defaultEntitySortField, sortDirection: 'desc', pageIndex: '0', }, From b594c5a304e6b14098ecc4c9a2335895e8b86c89 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 18:27:54 +0300 Subject: [PATCH 19/23] Use better function name --- .../utils/get_identity_fields_values.test.ts | 90 ------------------- ...arse_identity_field_values_to_kql.test.ts} | 14 +-- ... => parse_identity_field_values_to_kql.ts} | 2 +- .../components/alerts_badge/alerts_badge.tsx | 4 +- .../public/components/entities_grid/index.tsx | 2 +- 5 files changed, 11 insertions(+), 101 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts rename x-pack/plugins/observability_solution/inventory/common/utils/{get_identity_field_value_pair_to_kql.test.ts => parse_identity_field_values_to_kql.test.ts} (84%) rename x-pack/plugins/observability_solution/inventory/common/utils/{get_identity_field_value_pairs_to_kql.ts => parse_identity_field_values_to_kql.ts} (89%) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts deleted file mode 100644 index c6611a1f2d02a..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_fields_values.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_LAST_SEEN, -} from '@kbn/observability-shared-plugin/common'; -import { HostEntity, ServiceEntity } from '../entities'; -import { getIdentityFieldValuePairsToKql } from './get_identity_field_value_pairs_to_kql'; - -const commonEntityFields = { - [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', - [ENTITY_ID]: '1', - [ENTITY_DISPLAY_NAME]: 'entity_name', - [ENTITY_DEFINITION_ID]: 'entity_definition_id', - alertCount: 3, -}; - -describe('getIdentityFieldValuePairsToKql', () => { - it('should return the value when identityFields is a single string', () => { - const entity: ServiceEntity = { - 'agent.name': 'node', - 'entity.identityFields': 'service.name', - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, - }; - - const result = getIdentityFieldValuePairsToKql({ entity }); - expect(result).toEqual(['service.name: "my-service"']); - }); - - it('should return values when identityFields is an array of strings', () => { - const entity: ServiceEntity = { - 'agent.name': 'node', - 'entity.identityFields': ['service.name', 'service.environment'], - 'service.name': 'my-service', - 'entity.type': 'service', - 'service.environment': 'staging', - ...commonEntityFields, - }; - - const result = getIdentityFieldValuePairsToKql({ entity }); - expect(result).toEqual(['service.name: "my-service"', 'service.environment: "staging"']); - }); - - it('should return an empty array if identityFields is empty string', () => { - const entity: ServiceEntity = { - 'agent.name': 'node', - 'entity.identityFields': '', - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, - }; - - const result = getIdentityFieldValuePairsToKql({ entity }); - expect(result).toEqual([]); - }); - it('should return an empty array if identityFields is empty array', () => { - const entity: ServiceEntity = { - 'agent.name': 'node', - 'entity.identityFields': [], - 'service.name': 'my-service', - 'entity.type': 'service', - ...commonEntityFields, - }; - - const result = getIdentityFieldValuePairsToKql({ entity }); - expect(result).toEqual([]); - }); - - it('should ignore fields that are not present in the entity', () => { - const entity: HostEntity = { - 'entity.identityFields': ['host.name', 'foo.bar'], - 'host.name': 'my-host', - 'entity.type': 'host', - 'cloud.provider': null, - ...commonEntityFields, - }; - - const result = getIdentityFieldValuePairsToKql({ entity }); - expect(result).toEqual(['host.name: "my-host"']); - }); -}); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts similarity index 84% rename from x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts rename to x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts index c6611a1f2d02a..09ccd87096f96 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pair_to_kql.test.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts @@ -12,7 +12,7 @@ import { ENTITY_LAST_SEEN, } from '@kbn/observability-shared-plugin/common'; import { HostEntity, ServiceEntity } from '../entities'; -import { getIdentityFieldValuePairsToKql } from './get_identity_field_value_pairs_to_kql'; +import { parseIdentityFieldValuesToKql } from './parse_identity_field_values_to_kql'; const commonEntityFields = { [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', @@ -22,7 +22,7 @@ const commonEntityFields = { alertCount: 3, }; -describe('getIdentityFieldValuePairsToKql', () => { +describe('parseIdentityFieldValuesToKql', () => { it('should return the value when identityFields is a single string', () => { const entity: ServiceEntity = { 'agent.name': 'node', @@ -32,7 +32,7 @@ describe('getIdentityFieldValuePairsToKql', () => { ...commonEntityFields, }; - const result = getIdentityFieldValuePairsToKql({ entity }); + const result = parseIdentityFieldValuesToKql({ entity }); expect(result).toEqual(['service.name: "my-service"']); }); @@ -46,7 +46,7 @@ describe('getIdentityFieldValuePairsToKql', () => { ...commonEntityFields, }; - const result = getIdentityFieldValuePairsToKql({ entity }); + const result = parseIdentityFieldValuesToKql({ entity }); expect(result).toEqual(['service.name: "my-service"', 'service.environment: "staging"']); }); @@ -59,7 +59,7 @@ describe('getIdentityFieldValuePairsToKql', () => { ...commonEntityFields, }; - const result = getIdentityFieldValuePairsToKql({ entity }); + const result = parseIdentityFieldValuesToKql({ entity }); expect(result).toEqual([]); }); it('should return an empty array if identityFields is empty array', () => { @@ -71,7 +71,7 @@ describe('getIdentityFieldValuePairsToKql', () => { ...commonEntityFields, }; - const result = getIdentityFieldValuePairsToKql({ entity }); + const result = parseIdentityFieldValuesToKql({ entity }); expect(result).toEqual([]); }); @@ -84,7 +84,7 @@ describe('getIdentityFieldValuePairsToKql', () => { ...commonEntityFields, }; - const result = getIdentityFieldValuePairsToKql({ entity }); + const result = parseIdentityFieldValuesToKql({ entity }); expect(result).toEqual(['host.name: "my-host"']); }); }); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pairs_to_kql.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts similarity index 89% rename from x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pairs_to_kql.ts rename to x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts index aa3a1c6ac5187..cba5765b23ad8 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/get_identity_field_value_pairs_to_kql.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts @@ -8,7 +8,7 @@ import { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; import { Entity } from '../entities'; -export function getIdentityFieldValuePairsToKql({ entity }: { entity: Entity }) { +export function parseIdentityFieldValuesToKql({ entity }: { entity: Entity }) { const mapping: string[] = []; const identityFields = entity[ENTITY_IDENTITY_FIELDS]; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx index 2dc9269716d6e..7ae7683cfe9b9 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -10,10 +10,10 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Entity } from '../../../common/entities'; import { useKibana } from '../../hooks/use_kibana'; -import { getIdentityFieldValuePairsToKql } from '../../../common/utils/get_identity_field_value_pairs_to_kql'; +import { parseIdentityFieldValuesToKql } from '../../../common/utils/parse_identity_field_values_to_kql'; export function AlertsBadge({ entity }: { entity: Entity }) { - const identityFieldValues = getIdentityFieldValuePairsToKql({ entity }); + const identityFieldValues = parseIdentityFieldValuesToKql({ entity }); const { services: { http: { basePath }, diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index 74fa54a430fc7..697bc3304753e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -35,7 +35,7 @@ interface Props { loading: boolean; entities: LatestEntities; sortDirection: 'asc' | 'desc'; - sortField: EntityColumnIds; + sortField: string; pageIndex: number; onChangeSort: (sorting: EuiDataGridSorting['columns'][0]) => void; onChangePage: (nextPage: number) => void; From 00493ec56b7b5cc9a9a04278ffb3928ecacb1d4d Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Fri, 11 Oct 2024 18:36:40 +0300 Subject: [PATCH 20/23] fix typo --- .../inventory/server/routes/entities/get_latest_entities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index d14a3cfcdfd16..e5b686e19f9d8 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -31,7 +31,7 @@ export async function getLatestEntities({ entityTypes?: EntityType[]; kuery?: string; }) { - // alertCount doesn't exist in entties. Ignore it and sort by entity.lastSeenTimestamp by default + // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. const entitiesSortField = sortField === 'alertsCount' ? ENTITY_LAST_SEEN : sortField; const request = { From 638c8515f6562f8dca2d78e3d71215233cb5406e Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Mon, 14 Oct 2024 16:55:33 +0300 Subject: [PATCH 21/23] Return kuery expression instead of an array --- .../utils/parse_identity_field_values_to_kql.test.ts | 12 ++++++------ .../utils/parse_identity_field_values_to_kql.ts | 11 +++++++++-- .../public/components/alerts_badge/alerts_badge.tsx | 3 +-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts index 09ccd87096f96..c4b48410456f8 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.test.ts @@ -33,7 +33,7 @@ describe('parseIdentityFieldValuesToKql', () => { }; const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual(['service.name: "my-service"']); + expect(result).toEqual('service.name: "my-service"'); }); it('should return values when identityFields is an array of strings', () => { @@ -47,10 +47,10 @@ describe('parseIdentityFieldValuesToKql', () => { }; const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual(['service.name: "my-service"', 'service.environment: "staging"']); + expect(result).toEqual('service.name: "my-service" AND service.environment: "staging"'); }); - it('should return an empty array if identityFields is empty string', () => { + it('should return an empty string if identityFields is empty string', () => { const entity: ServiceEntity = { 'agent.name': 'node', 'entity.identityFields': '', @@ -60,7 +60,7 @@ describe('parseIdentityFieldValuesToKql', () => { }; const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual([]); + expect(result).toEqual(''); }); it('should return an empty array if identityFields is empty array', () => { const entity: ServiceEntity = { @@ -72,7 +72,7 @@ describe('parseIdentityFieldValuesToKql', () => { }; const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual([]); + expect(result).toEqual(''); }); it('should ignore fields that are not present in the entity', () => { @@ -85,6 +85,6 @@ describe('parseIdentityFieldValuesToKql', () => { }; const result = parseIdentityFieldValuesToKql({ entity }); - expect(result).toEqual(['host.name: "my-host"']); + expect(result).toEqual('host.name: "my-host"'); }); }); diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts index cba5765b23ad8..2e3f3dadd4109 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/parse_identity_field_values_to_kql.ts @@ -8,7 +8,14 @@ import { ENTITY_IDENTITY_FIELDS } from '@kbn/observability-shared-plugin/common'; import { Entity } from '../entities'; -export function parseIdentityFieldValuesToKql({ entity }: { entity: Entity }) { +type Operator = 'AND'; +export function parseIdentityFieldValuesToKql({ + entity, + operator = 'AND', +}: { + entity: Entity; + operator?: Operator; +}) { const mapping: string[] = []; const identityFields = entity[ENTITY_IDENTITY_FIELDS]; @@ -23,5 +30,5 @@ export function parseIdentityFieldValuesToKql({ entity }: { entity: Entity }) { }); } - return mapping; + return mapping.join(` ${operator} `); } diff --git a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx index 7ae7683cfe9b9..ba1b992ff62c1 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/alerts_badge/alerts_badge.tsx @@ -13,7 +13,6 @@ import { useKibana } from '../../hooks/use_kibana'; import { parseIdentityFieldValuesToKql } from '../../../common/utils/parse_identity_field_values_to_kql'; export function AlertsBadge({ entity }: { entity: Entity }) { - const identityFieldValues = parseIdentityFieldValuesToKql({ entity }); const { services: { http: { basePath }, @@ -22,7 +21,7 @@ export function AlertsBadge({ entity }: { entity: Entity }) { const activeAlertsHref = basePath.prepend( `/app/observability/alerts?_a=${rison.encode({ - kuery: identityFieldValues.join(' AND '), + kuery: parseIdentityFieldValuesToKql({ entity }), status: 'active', })}` ); From ca6be0dbd95f3b8bf2835d8464310961c1aa76c5 Mon Sep 17 00:00:00 2001 From: Kate Patticha Date: Tue, 15 Oct 2024 12:12:42 +0300 Subject: [PATCH 22/23] Address PR comments --- .../entities/get_identify_fields.test.ts | 58 +++++++++++++------ .../get_identity_fields_per_entity_type.ts | 11 +--- .../routes/entities/get_latest_entities.ts | 2 +- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts index ecd28958132b5..90bf2967b894d 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identify_fields.test.ts @@ -5,36 +5,60 @@ * 2.0. */ -import { EntityType } from '../../../common/entities'; -import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { ContainerEntity, HostEntity, ServiceEntity } from '../../../common/entities'; +import { + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_ID, + ENTITY_LAST_SEEN, +} from '@kbn/observability-shared-plugin/common'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; +const commonEntityFields = { + [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', + [ENTITY_ID]: '1', + [ENTITY_DISPLAY_NAME]: 'entity_name', + [ENTITY_DEFINITION_ID]: 'entity_definition_id', + alertCount: 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 mockEntities = [ - { - [ENTITY_TYPE]: 'service' as EntityType, - [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], - }, - { - [ENTITY_TYPE]: 'host' as EntityType, - [ENTITY_IDENTITY_FIELDS]: 'host.name', - }, - { - [ENTITY_TYPE]: 'container' as EntityType, - [ENTITY_IDENTITY_FIELDS]: 'contaier.id', - }, - ]; + const serviceEntity: ServiceEntity = { + 'agent.name': 'node', + 'entity.identityFields': ['service.name', 'service.environment'], + 'service.name': 'my-service', + 'entity.type': 'service', + ...commonEntityFields, + }; + + const hostEntity: HostEntity = { + 'entity.identityFields': ['host.name'], + 'host.name': 'my-host', + 'entity.type': 'host', + 'cloud.provider': null, + ...commonEntityFields, + }; + + const containerEntity: ContainerEntity = { + 'entity.identityFields': 'container.id', + 'host.name': 'my-host', + 'entity.type': 'container', + 'cloud.provider': null, + 'container.id': '123', + ...commonEntityFields, + }; + + 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(['contaier.id']); + expect(result.get('container')).toEqual(['container.id']); }); }); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts index 3e649b05af034..a9696593212e1 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts @@ -10,18 +10,11 @@ import { Entity, EntityType } from '../../../common/entities'; export type IdentityFieldsPerEntityType = Map; -export const getIdentityFieldsPerEntityType = ( - entities: Array> -) => { +export const getIdentityFieldsPerEntityType = (entities: Entity[]) => { const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); entities.map((entity) => - identityFieldsPerEntityType.set( - entity[ENTITY_TYPE], - Array.isArray(entity[ENTITY_IDENTITY_FIELDS]) - ? entity[ENTITY_IDENTITY_FIELDS] - : [entity[ENTITY_IDENTITY_FIELDS]] - ) + identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat()) ); return identityFieldsPerEntityType; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts index e5b686e19f9d8..e500ce32c3cef 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_latest_entities.ts @@ -13,7 +13,7 @@ import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, type EntityType, - Entity, + type Entity, type EntityColumnIds, } from '../../../common/entities'; import { getEntityDefinitionIdWhereClause, getEntityTypesWhereClause } from './query_helper'; From af85ce08a78e79823e3cf0a83961462a13cbd3ec Mon Sep 17 00:00:00 2001 From: Katerina Date: Tue, 15 Oct 2024 12:45:34 +0300 Subject: [PATCH 23/23] Update x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: CauĂȘ Marcondes <55978943+cauemarcondes@users.noreply.github.com> --- .../routes/entities/get_identity_fields_per_entity_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts index a9696593212e1..0ca4eb9d21239 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_identity_fields_per_entity_type.ts @@ -13,7 +13,7 @@ export type IdentityFieldsPerEntityType = Map; export const getIdentityFieldsPerEntityType = (entities: Entity[]) => { const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); - entities.map((entity) => + entities.forEach((entity) => identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat()) );