From ad95291fbf48b48d1a6593fdf272f0b26c0ab0d5 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 23 Oct 2024 17:53:36 +0200 Subject: [PATCH 01/27] Improve typing --- .../src/lib/entities/kubernetes/index.ts | 8 +- .../kbn-entities-schema/src/schema/entity.ts | 12 +-- .../observability_utils/array/join_by_key.ts | 6 +- .../es/utils/esql_result_to_plain_objects.ts | 32 ++++---- .../components/alerts_badge/alerts_badge.tsx | 7 +- .../entities_grid/entity_name/index.tsx | 10 +-- .../public/components/entities_grid/index.tsx | 8 +- .../public/components/entity_icon/index.tsx | 13 +--- .../public/hooks/use_detail_view_redirect.ts | 35 ++++----- .../entities/get_identify_fields.test.ts | 76 ++++++++++++------- .../get_identity_fields_per_entity_type.ts | 9 +-- .../routes/entities/get_latest_entities.ts | 18 +++-- .../entities/get_latest_entities_alerts.ts | 2 +- .../inventory/server/routes/entities/route.ts | 8 +- 14 files changed, 135 insertions(+), 109 deletions(-) diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts index db95dcf4155b..6033533cf359 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts @@ -55,9 +55,11 @@ export class K8sEntity extends Serializable { super({ ...fields, 'entity.type': entityTypeWithSchema, - 'entity.definition_id': `builtin_${entityTypeWithSchema}`, - 'entity.identity_fields': identityFields, - 'entity.display_name': getDisplayName({ identityFields, fields }), + 'entity.definitionId': `builtin_${entityTypeWithSchema}`, + 'entity.identityFields': identityFields, + 'entity.displayName': getDisplayName({ identityFields, fields }), + 'entity.definitionVersion': '1.0.0', + 'entity.schemaVersion': '1.0', }); } } diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index 9ab02e0931d9..c758077f2fc6 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -11,12 +11,12 @@ import { arrayOfStringsSchema } from './common'; export const entityBaseSchema = z.object({ id: z.string(), type: z.string(), - identity_fields: arrayOfStringsSchema, - display_name: z.string(), - metrics: z.record(z.string(), z.number()), - definition_version: z.string(), - schema_version: z.string(), - definition_id: z.string(), + identityFields: arrayOfStringsSchema, + displayName: z.string(), + metrics: z.optional(z.record(z.string(), z.number())), + definitionVersion: z.string(), + schemaVersion: z.string(), + definitionId: z.string(), }); export interface MetadataRecord { diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts index 54e8ecdaf409..a5baf6e6b7bc 100644 --- a/x-pack/packages/observability/observability_utils/array/join_by_key.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -18,6 +18,10 @@ export type JoinedReturnType< } >; +function getValueByPath(obj: any, path: string): any { + return path.split('.').reduce((acc, part) => acc && acc[part], obj); +} + type ArrayOrSingle = T | T[]; export function joinByKey< @@ -45,7 +49,7 @@ export function joinByKey( 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])); + const stableKey = stableStringify(keys.map((k) => getValueByPath(current, k))); if (map.has(stableKey)) { const item = map.get(stableKey); diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts index 96049f75ef15..6f3e5ea726ca 100644 --- a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts @@ -6,25 +6,31 @@ */ import type { ESQLSearchResponse } from '@kbn/es-types'; +import { castArray } from 'lodash'; +import { unflattenObject } from '../../object/unflatten_object'; export function esqlResultToPlainObjects>( - result: ESQLSearchResponse + result: ESQLSearchResponse, + knownArrayFields?: string[] ): T[] { + const knownArrayFieldsSet = new Set(knownArrayFields); return result.values.map((row) => { - return row.reduce>((acc, value, index) => { - const column = result.columns[index]; + return unflattenObject( + row.reduce>((acc, value, index) => { + const column = result.columns[index]; - if (!column) { - return acc; - } + if (!column) { + return acc; + } - // Removes the type suffix from the column name - const name = column.name.replace(/\.(text|keyword)$/, ''); - if (!acc[name]) { - acc[name] = value; - } + // Removes the type suffix from the column name + const name = column.name.replace(/\.(text|keyword)$/, ''); + if (!acc[name]) { + acc[column.name] = knownArrayFieldsSet.has(column.name) ? castArray(value) : value; + } - return acc; - }, {}); + return acc; + }, {}) + ); }) as T[]; } 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 a5845a7b42dc..0fb4a403bf40 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 @@ -8,11 +8,10 @@ import React from 'react'; import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { Entity } from '../../../common/entities'; -import { unflattenEntity } from '../../../common/utils/unflatten_entity'; +import type { InventoryEntityLatest } from '../../../common/entities'; import { useKibana } from '../../hooks/use_kibana'; -export function AlertsBadge({ entity }: { entity: Entity }) { +export function AlertsBadge({ entity }: { entity: InventoryEntityLatest }) { 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: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)), + kuery: entityManager.entityClient.asKqlFilter(entity), status: 'active', })}` ); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index e8db7013f8cb..11c63b62634a 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -6,15 +6,13 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { ENTITY_DISPLAY_NAME } from '@kbn/observability-shared-plugin/common'; -import { useKibana } from '../../../hooks/use_kibana'; -import type { Entity } from '../../../../common/entities'; +import React from 'react'; +import { InventoryEntityLatest } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; interface EntityNameProps { - entity: Entity; + entity: InventoryEntityLatest; } export function EntityName({ entity }: EntityNameProps) { @@ -40,7 +38,7 @@ export function EntityName({ entity }: EntityNameProps) { - {entity[ENTITY_DISPLAY_NAME]} + {entity.entity.displayName} 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 e3c0d24837f9..331e19e54fec 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 @@ -84,7 +84,7 @@ export function EntitiesGrid({ } const columnEntityTableId = columnId as EntityColumnIds; - const entityType = entity[ENTITY_TYPE]; + const entityType = entity.entity.type; switch (columnEntityTableId) { case 'alertsCount': @@ -107,7 +107,7 @@ export function EntitiesGrid({ values={{ date: ( ; default: - return entity[columnId as EntityColumnIds] || ''; + return ''; } }, [entities, onFilterByType] diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 48b21779d2e3..fa7720ae1c40 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -6,20 +6,15 @@ */ import React from 'react'; -import { - AGENT_NAME, - CLOUD_PROVIDER, - ENTITY_TYPE, - ENTITY_TYPES, -} from '@kbn/observability-shared-plugin/common'; +import { AGENT_NAME, CLOUD_PROVIDER, ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import type { AgentName } from '@kbn/elastic-agent-utils'; import { euiThemeVars } from '@kbn/ui-theme'; -import type { Entity } from '../../../common/entities'; +import type { InventoryEntityLatest } from '../../../common/entities'; interface EntityIconProps { - entity: Entity; + entity: InventoryEntityLatest; } type NotNullableCloudProvider = Exclude; @@ -29,7 +24,7 @@ const getSingleValue = (value?: T | T[] | null): T | undefined => { }; export function EntityIcon({ entity }: EntityIconProps) { - const entityType = entity[ENTITY_TYPE]; + const entityType = entity.entity.type; const defaultIconSize = euiThemeVars.euiSizeL; if (entityType === ENTITY_TYPES.HOST || entityType === ENTITY_TYPES.CONTAINER) { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 23380dc3704d..a476baf18a0f 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -7,10 +7,6 @@ import { ASSET_DETAILS_LOCATOR_ID, AssetDetailsLocatorParams, - ENTITY_IDENTITY_FIELDS, - ENTITY_TYPE, - ENTITY_TYPES, - SERVICE_ENVIRONMENT, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; @@ -18,8 +14,7 @@ import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; -import type { Entity } from '../../common/entities'; -import { unflattenEntity } from '../../common/utils/unflatten_entity'; +import type { InventoryEntityLatest } from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { @@ -45,24 +40,22 @@ export const useDetailViewRedirect = () => { const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); const getSingleIdentityFieldValue = useCallback( - (entity: Entity) => { - const identityFields = castArray(entity[ENTITY_IDENTITY_FIELDS]); + (latestEntity: InventoryEntityLatest) => { + const identityFields = castArray(latestEntity.entity.type); if (identityFields.length > 1) { throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`); } const identityField = identityFields[0]; - return entityManager.entityClient.getIdentityFieldsValue(unflattenEntity(entity))[ - identityField - ]; + return entityManager.entityClient.getIdentityFieldsValue(latestEntity)[identityField]; }, [entityManager.entityClient] ); const getDetailViewRedirectUrl = useCallback( - (entity: Entity) => { - const type = entity[ENTITY_TYPE]; - const identityValue = getSingleIdentityFieldValue(entity); + (latestEntity: InventoryEntityLatest) => { + const type = latestEntity.entity.type; + const identityValue = getSingleIdentityFieldValue(latestEntity); switch (type) { case ENTITY_TYPES.HOST: @@ -77,7 +70,10 @@ export const useDetailViewRedirect = () => { serviceName: identityValue, // service.environemnt is not part of entity.identityFields // we need to manually get its value - environment: [entity[SERVICE_ENVIRONMENT] || undefined].flat()[0], + environment: + 'service' in latestEntity + ? (latestEntity.service as { environment?: string }).environment + : undefined, }); default: @@ -88,8 +84,8 @@ export const useDetailViewRedirect = () => { ); const getDashboardRedirectUrl = useCallback( - (entity: Entity) => { - const type = entity[ENTITY_TYPE]; + (latestEntity: InventoryEntityLatest) => { + const type = latestEntity.entity.type; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; return dashboardId @@ -97,7 +93,7 @@ export const useDetailViewRedirect = () => { dashboardId, query: { language: 'kuery', - query: entityManager.entityClient.asKqlFilter(unflattenEntity(entity)), + query: entityManager.entityClient.asKqlFilter(latestEntity), }, }) : undefined; @@ -106,7 +102,8 @@ export const useDetailViewRedirect = () => { ); const getEntityRedirectUrl = useCallback( - (entity: Entity) => getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity), + (entity: InventoryEntityLatest) => + getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity), [getDashboardRedirectUrl, getDetailViewRedirectUrl] ); 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 62d77c08fd27..40fd25d9ad74 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,21 +5,16 @@ * 2.0. */ -import type { Entity } from '../../../common/entities'; -import { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, -} from '@kbn/observability-shared-plugin/common'; +import { InventoryEntityLatest } from '../../../common/entities'; 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', +const commonEntityFields: InventoryEntityLatest = { + entity: { + lastSeenTimestamp: '2023-10-09T00:00:00Z', + id: '1', + displayName: 'entity_name', + definitionId: 'entity_definition_id', + } as InventoryEntityLatest['entity'], alertCount: 3, }; describe('getIdentityFields', () => { @@ -28,29 +23,52 @@ describe('getIdentityFields', () => { expect(result.size).toBe(0); }); it('should return a Map with unique entity types and their respective identity fields', () => { - const serviceEntity: Entity = { - 'agent.name': 'node', - [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], - 'service.name': 'my-service', - 'entity.type': 'service', + const serviceEntity: InventoryEntityLatest = { ...commonEntityFields, + agent: { + name: 'node', + }, + entity: { + ...commonEntityFields.entity, + identityFields: ['service.name', 'service.environment'], + type: 'service', + }, + service: { + name: 'my-service', + }, }; - const hostEntity: Entity = { - [ENTITY_IDENTITY_FIELDS]: ['host.name'], - 'host.name': 'my-host', - 'entity.type': 'host', - 'cloud.provider': null, + const hostEntity: InventoryEntityLatest = { ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['host.name'], + type: 'host', + }, + host: { + name: 'my-host', + }, + cloud: { + provider: null, + }, }; - const containerEntity: Entity = { - [ENTITY_IDENTITY_FIELDS]: 'container.id', - 'host.name': 'my-host', - 'entity.type': 'container', - 'cloud.provider': null, - 'container.id': '123', + const containerEntity: InventoryEntityLatest = { ...commonEntityFields, + entity: { + ...commonEntityFields.entity, + identityFields: ['container.id'], + type: 'container', + }, + host: { + name: 'my-host', + }, + cloud: { + provider: null, + }, + container: { + id: '123', + }, }; const mockEntities = [serviceEntity, hostEntity, containerEntity]; 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 f54dc8a7f121..269f98595331 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,16 +5,15 @@ * 2.0. */ -import { ENTITY_IDENTITY_FIELDS, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { Entity } from '../../../common/entities'; +import { InventoryEntityLatest } from '../../../common/entities'; export type IdentityFieldsPerEntityType = Map; -export const getIdentityFieldsPerEntityType = (entities: Entity[]) => { - const identityFieldsPerEntityType: IdentityFieldsPerEntityType = new Map(); +export const getIdentityFieldsPerEntityType = (entities: InventoryEntityLatest[]) => { + const identityFieldsPerEntityType = new Map(); entities.forEach((entity) => - identityFieldsPerEntityType.set(entity[ENTITY_TYPE], [entity[ENTITY_IDENTITY_FIELDS]].flat()) + identityFieldsPerEntityType.set(entity.entity.type, entity.entity.identityFields) ); 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 c95a488ad49d..7656bac33f8d 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 @@ -5,16 +5,22 @@ * 2.0. */ +import { z } from '@kbn/zod'; 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, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; +import { + ENTITY_IDENTITY_FIELDS, + ENTITY_LAST_SEEN, + ENTITY_TYPE, +} from '@kbn/observability-shared-plugin/common'; +import { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; +import { entityLatestSchema } from '@kbn/entities-schema'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, - type Entity, type EntityColumnIds, + InventoryEntityLatest, } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; @@ -30,7 +36,7 @@ export async function getLatestEntities({ sortField: EntityColumnIds; entityTypes?: string[]; kuery?: string; -}) { +}): Promise { // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. const entitiesSortField = sortField === 'alertsCount' ? ENTITY_LAST_SEEN : sortField; @@ -58,5 +64,7 @@ export async function getLatestEntities({ params, }); - return esqlResultToPlainObjects(latestEntitiesEsqlResponse); + return z + .array(entityLatestSchema) + .parse(esqlResultToPlainObjects(latestEntitiesEsqlResponse, [ENTITY_IDENTITY_FIELDS])); } 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 e969f1d537e9..48100c2f3143 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 @@ -27,7 +27,7 @@ export async function getLatestEntitiesAlerts({ alertsClient: AlertsClient; kuery?: string; identityFieldsPerEntityType: IdentityFieldsPerEntityType; -}): Promise> { +}): Promise> { if (identityFieldsPerEntityType.size === 0) { return []; } 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 88d6cb68ee21..631a544a1ac2 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 @@ -11,7 +11,7 @@ import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import * as t from 'io-ts'; import { orderBy } from 'lodash'; import { joinByKey } from '@kbn/observability-utils/array/join_by_key'; -import { entityColumnIdsRt, Entity } from '../../../common/entities'; +import { entityColumnIdsRt, InventoryEntityLatest } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; @@ -92,15 +92,15 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ const joined = joinByKey( [...latestEntities, ...alerts], - [...identityFieldsPerEntityType.values()].flat() - ).filter((entity) => entity['entity.id']) as Entity[]; + [...identityFieldsPerEntityType.values()].flat() as Array + ).filter((latestEntity: InventoryEntityLatest) => latestEntity.entity.id); return { entities: sortField === 'alertsCount' ? orderBy( joined, - [(item: Entity) => item?.alertsCount === undefined, sortField], + [(item: InventoryEntityLatest) => item?.alertsCount === undefined, sortField], ['asc', sortDirection] // push entities without alertsCount to the end ) : joined, From 99cd0d44feac1a78a1be95c66dd2a2b01ed69762 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 25 Oct 2024 10:22:33 +0200 Subject: [PATCH 02/27] Improve types --- packages/kbn-es-types/src/search.ts | 9 +- .../kbn-entities-schema/src/schema/entity.ts | 2 +- .../client/create_observability_es_client.ts | 10 +- .../es/utils/esql_result_to_plain_objects.ts | 17 ++-- .../inventory/common/entities.ts | 22 +++++ .../public/components/entity_icon/index.tsx | 30 +++--- .../hooks/use_detail_view_redirect.test.ts | 99 ++++++++++--------- .../public/hooks/use_detail_view_redirect.ts | 45 +++++---- .../routes/entities/get_entity_types.ts | 13 ++- .../get_identity_fields_per_entity_type.ts | 3 +- .../routes/entities/get_latest_entities.ts | 32 +++--- .../server/routes/has_data/get_has_data.ts | 6 +- 12 files changed, 158 insertions(+), 130 deletions(-) diff --git a/packages/kbn-es-types/src/search.ts b/packages/kbn-es-types/src/search.ts index 87f9dd15517c..42d870cae108 100644 --- a/packages/kbn-es-types/src/search.ts +++ b/packages/kbn-es-types/src/search.ts @@ -674,12 +674,15 @@ export interface ESQLColumn { export type ESQLRow = unknown[]; -export interface ESQLSearchResponse { - columns: ESQLColumn[]; +export interface ESQLSearchResponse< + TColumn extends ESQLColumn = ESQLColumn, + TRow extends ESQLRow = ESQLRow +> { + columns: TColumn[]; // In case of ?drop_null_columns in the query, then // all_columns will have available and empty fields // while columns only the available ones (non nulls) - all_columns?: ESQLColumn[]; + all_columns?: TRow[]; values: ESQLRow[]; took?: number; _clusters?: estypes.ClusterStatistics; diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index c758077f2fc6..f996f088df7e 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -11,7 +11,7 @@ import { arrayOfStringsSchema } from './common'; export const entityBaseSchema = z.object({ id: z.string(), type: z.string(), - identityFields: arrayOfStringsSchema, + identityFields: z.union([arrayOfStringsSchema, z.string()]), displayName: z.string(), metrics: z.optional(z.record(z.string(), z.number())), definitionVersion: z.string(), diff --git a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts index 0011e0f17c1c..efee7766ac1d 100644 --- a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts +++ b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts @@ -9,6 +9,7 @@ import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import type { ESQLSearchResponse, ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; import { withSpan } from '@kbn/apm-utils'; import type { EsqlQueryRequest } from '@elastic/elasticsearch/lib/api/types'; +import { esqlResultToPlainObjects } from '../utils/esql_result_to_plain_objects'; type SearchRequest = ESSearchRequest & { index: string | string[]; @@ -25,7 +26,10 @@ export interface ObservabilityElasticsearchClient { operationName: string, parameters: TSearchRequest ): Promise>; - esql(operationName: string, parameters: EsqlQueryRequest): Promise; + esql( + operationName: string, + parameters: EsqlQueryRequest + ): Promise; client: ElasticsearchClient; } @@ -40,7 +44,7 @@ export function createObservabilityEsClient({ }): ObservabilityElasticsearchClient { return { client, - esql(operationName: string, parameters: EsqlQueryRequest) { + esql(operationName: string, parameters: EsqlQueryRequest) { logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`); return withSpan({ name: operationName, labels: { plugin } }, () => { return client.esql.query( @@ -54,7 +58,7 @@ export function createObservabilityEsClient({ }) .then((response) => { logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`); - return response as unknown as ESQLSearchResponse; + return esqlResultToPlainObjects(response as unknown as ESQLSearchResponse); }) .catch((error) => { throw error; diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts index 6f3e5ea726ca..233385442101 100644 --- a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts @@ -6,17 +6,14 @@ */ import type { ESQLSearchResponse } from '@kbn/es-types'; -import { castArray } from 'lodash'; import { unflattenObject } from '../../object/unflatten_object'; -export function esqlResultToPlainObjects>( - result: ESQLSearchResponse, - knownArrayFields?: string[] -): T[] { - const knownArrayFieldsSet = new Set(knownArrayFields); +export function esqlResultToPlainObjects( + result: ESQLSearchResponse +): TDocument[] { return result.values.map((row) => { return unflattenObject( - row.reduce>((acc, value, index) => { + row.reduce>((acc, value, index) => { const column = result.columns[index]; if (!column) { @@ -26,11 +23,11 @@ export function esqlResultToPlainObjects>( // Removes the type suffix from the column name const name = column.name.replace(/\.(text|keyword)$/, ''); if (!acc[name]) { - acc[column.name] = knownArrayFieldsSet.has(column.name) ? castArray(value) : value; + acc[column.name] = value; } return acc; }, {}) - ); - }) as T[]; + ) as TDocument; + }); } diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index f686490b90bf..3e1c2c838360 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -17,6 +17,7 @@ import { import { decode, encode } from '@kbn/rison'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; +import { AgentName } from '@kbn/elastic-agent-utils'; export const entityColumnIdsRt = t.union([ t.literal(ENTITY_DISPLAY_NAME), @@ -122,3 +123,24 @@ export type EntityGroup = { export type InventoryEntityLatest = z.infer & { alertsCount?: number; }; + +export const isHostEntity = ( + entity: InventoryEntityLatest +): entity is InventoryEntityLatest & { cloud?: { provider: string } } => { + return entity.entity.type === 'host'; +}; + +export const isContainerEntity = ( + entity: InventoryEntityLatest +): entity is InventoryEntityLatest & { cloud?: { provider: string } } => { + return entity.entity.type === 'container'; +}; + +export const isServiceEntity = ( + entity: InventoryEntityLatest +): entity is InventoryEntityLatest & { + agent?: { name: AgentName }; + service: { name: string; environment: string }; +} => { + return entity.entity.type === 'service'; +}; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index fa7720ae1c40..860c6c5ca2e8 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -6,31 +6,26 @@ */ import React from 'react'; -import { AGENT_NAME, CLOUD_PROVIDER, ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; -import type { AgentName } from '@kbn/elastic-agent-utils'; import { euiThemeVars } from '@kbn/ui-theme'; -import type { InventoryEntityLatest } from '../../../common/entities'; +import { + isHostEntity, + type InventoryEntityLatest, + isContainerEntity, + isServiceEntity, +} from '../../../common/entities'; interface EntityIconProps { entity: InventoryEntityLatest; } -type NotNullableCloudProvider = Exclude; - -const getSingleValue = (value?: T | T[] | null): T | undefined => { - return value == null ? undefined : Array.isArray(value) ? value[0] : value; -}; - export function EntityIcon({ entity }: EntityIconProps) { - const entityType = entity.entity.type; const defaultIconSize = euiThemeVars.euiSizeL; - if (entityType === ENTITY_TYPES.HOST || entityType === ENTITY_TYPES.CONTAINER) { - const cloudProvider = getSingleValue( - entity[CLOUD_PROVIDER] as NotNullableCloudProvider | NotNullableCloudProvider[] - ); + if (isHostEntity(entity) || isContainerEntity(entity)) { + const cloudProvider = entity.cloud?.provider; + return ( ; + if (isServiceEntity(entity)) { + return ; } if (entityType.startsWith('kubernetes')) { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index cf4993f87188..0cc68bcea5ee 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -8,23 +8,9 @@ import { renderHook } from '@testing-library/react-hooks'; import { useDetailViewRedirect } from './use_detail_view_redirect'; import { useKibana } from './use_kibana'; -import { - AGENT_NAME, - CLOUD_PROVIDER, - CONTAINER_ID, - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, - ENTITY_TYPE, - HOST_NAME, - ENTITY_TYPES, - SERVICE_ENVIRONMENT, - SERVICE_NAME, -} from '@kbn/observability-shared-plugin/common'; +import { CONTAINER_ID, HOST_NAME, SERVICE_NAME } from '@kbn/observability-shared-plugin/common'; import { unflattenEntity } from '../../common/utils/unflatten_entity'; -import type { Entity } from '../../common/entities'; +import type { InventoryEntityLatest } from '../../common/entities'; jest.mock('./use_kibana'); jest.mock('../../common/utils/unflatten_entity'); @@ -32,11 +18,13 @@ jest.mock('../../common/utils/unflatten_entity'); const useKibanaMock = useKibana as jest.Mock; const unflattenEntityMock = unflattenEntity as jest.Mock; -const commonEntityFields: Partial = { - [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', - [ENTITY_ID]: '1', - [ENTITY_DISPLAY_NAME]: 'entity_name', - [ENTITY_DEFINITION_ID]: 'entity_definition_id', +const commonEntityFields: Partial = { + lastSeenTimestamp: '2023-10-09T00:00:00Z', + id: '1', + displayName: 'entity_name', + definitionId: 'entity_definition_id', + definitionVersion: '1', + schemaVersion: '1', }; describe('useDetailViewRedirect', () => { @@ -71,12 +59,18 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for host entity', () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: [HOST_NAME], - [ENTITY_TYPE]: 'host', - [HOST_NAME]: 'host-1', - [CLOUD_PROVIDER]: null, + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: 'host', + identityFields: ['host.name'], + }, + host: { + name: 'host-1', + }, + cloud: { + provider: null, + }, }; mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' }); @@ -90,12 +84,18 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for container entity', () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: [CONTAINER_ID], - [ENTITY_TYPE]: 'container', - [CONTAINER_ID]: 'container-1', - [CLOUD_PROVIDER]: null, + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: 'container', + identityFields: ['container.id'], + }, + container: { + id: 'container-1', + }, + cloud: { + provider: null, + }, }; mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' }); @@ -112,13 +112,19 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for service entity', () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: [SERVICE_NAME], - [ENTITY_TYPE]: 'service', - [SERVICE_NAME]: 'service-1', - [SERVICE_ENVIRONMENT]: 'prod', - [AGENT_NAME]: 'node', + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: 'service', + identityFields: ['service.name'], + }, + agent: { + name: 'node', + }, + service: { + name: 'service-1', + environment: 'prod', + }, }; mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' }); mockGetRedirectUrl.mockReturnValue('service-overview-url'); @@ -145,10 +151,15 @@ describe('useDetailViewRedirect', () => { [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { - const entity: Entity = { - ...(commonEntityFields as Entity), - [ENTITY_IDENTITY_FIELDS]: ['some.field'], - [ENTITY_TYPE]: entityType, + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: entityType, + identityFields: ['some.field'], + }, + some: { + field: 'some-value', + }, }; mockAsKqlFilter.mockReturnValue('kql-query'); diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index a476baf18a0f..518f1f8dfb1a 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -14,7 +14,12 @@ import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; -import type { InventoryEntityLatest } from '../../common/entities'; +import { + isHostEntity, + type InventoryEntityLatest, + isContainerEntity, + isServiceEntity, +} from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { @@ -41,7 +46,7 @@ export const useDetailViewRedirect = () => { const getSingleIdentityFieldValue = useCallback( (latestEntity: InventoryEntityLatest) => { - const identityFields = castArray(latestEntity.entity.type); + const identityFields = castArray(latestEntity.entity.identityFields); if (identityFields.length > 1) { throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`); } @@ -54,31 +59,25 @@ export const useDetailViewRedirect = () => { const getDetailViewRedirectUrl = useCallback( (latestEntity: InventoryEntityLatest) => { - const type = latestEntity.entity.type; const identityValue = getSingleIdentityFieldValue(latestEntity); - switch (type) { - case ENTITY_TYPES.HOST: - case ENTITY_TYPES.CONTAINER: - return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue, - assetType: type, - }); - - case 'service': - return serviceOverviewLocator?.getRedirectUrl({ - serviceName: identityValue, - // service.environemnt is not part of entity.identityFields - // we need to manually get its value - environment: - 'service' in latestEntity - ? (latestEntity.service as { environment?: string }).environment - : undefined, - }); + if (isHostEntity(latestEntity) || isContainerEntity(latestEntity)) { + return assetDetailsLocator?.getRedirectUrl({ + assetId: identityValue, + assetType: latestEntity.entity.type, + }); + } - default: - return undefined; + if (isServiceEntity(latestEntity)) { + return serviceOverviewLocator?.getRedirectUrl({ + serviceName: identityValue, + // service.environemnt is not part of entity.identityFields + // we need to manually get its value + environment: latestEntity.service.environment, + }); } + + return undefined; }, [assetDetailsLocator, getSingleIdentityFieldValue, serviceOverviewLocator] ); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index 2dfc9b8ccfdf..32631e062d9b 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -15,12 +15,15 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql('get_entity_types', { - query: `FROM ${ENTITIES_LATEST_ALIAS} + const entityTypesEsqlResponse = await inventoryEsClient.esql<{ [ENTITY_TYPE]: string }>( + 'get_entity_types', + { + query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} - | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE} + | STATS BY ${ENTITY_TYPE} `, - }); + } + ); - return entityTypesEsqlResponse.values.map(([_, val]) => val as string); + return entityTypesEsqlResponse.flatMap((types) => Object.values(types)); } 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 269f98595331..b755bc87e530 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,6 +5,7 @@ * 2.0. */ +import { castArray } from 'lodash'; import { InventoryEntityLatest } from '../../../common/entities'; export type IdentityFieldsPerEntityType = Map; @@ -13,7 +14,7 @@ export const getIdentityFieldsPerEntityType = (entities: InventoryEntityLatest[] const identityFieldsPerEntityType = new Map(); entities.forEach((entity) => - identityFieldsPerEntityType.set(entity.entity.type, entity.entity.identityFields) + identityFieldsPerEntityType.set(entity.entity.type, castArray(entity.entity.identityFields)) ); 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 7656bac33f8d..1e4d3b341a0c 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 @@ -5,17 +5,10 @@ * 2.0. */ -import { z } from '@kbn/zod'; 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_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; -import { entityLatestSchema } from '@kbn/entities-schema'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, @@ -54,17 +47,18 @@ export async function getLatestEntities({ const query = [from, ...where, sort, limit].join(' | '); - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { - query, - filter: { - bool: { - filter: [...kqlQuery(kuery)], + const latestEntitiesEsqlResponse = await inventoryEsClient.esql( + 'get_latest_entities', + { + query, + filter: { + bool: { + filter: [...kqlQuery(kuery)], + }, }, - }, - params, - }); + params, + } + ); - return z - .array(entityLatestSchema) - .parse(esqlResultToPlainObjects(latestEntitiesEsqlResponse, [ENTITY_IDENTITY_FIELDS])); + return latestEntitiesEsqlResponse; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts index c1e4a82c343b..d328a4f3b8d6 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -5,7 +5,6 @@ * 2.0. */ import type { Logger } from '@kbn/core/server'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from '../entities/query_helper'; import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; @@ -18,14 +17,15 @@ export async function getHasData({ logger: Logger; }) { try { - const esqlResults = await inventoryEsClient.esql('get_has_data', { + const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS _count = COUNT(*) | LIMIT 1`, }); - const totalCount = esqlResultToPlainObjects(esqlResults)?.[0]._count ?? 0; + const totalCount = esqlResults[0]._count; + return { hasData: totalCount > 0 }; } catch (e) { logger.error(e); From b23fae202f8f6cb4c2a69ceeb617c414815cf2e9 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 4 Nov 2024 11:09:06 +0100 Subject: [PATCH 03/27] update type considering the latest changes --- .../kbn-entities-schema/src/schema/entity.ts | 10 +-- .../public/lib/entity_client.test.ts | 2 +- .../.storybook/get_mock_inventory_context.tsx | 9 +- .../inventory/common/entities.ts | 36 -------- .../common/utils/entity_type_guards.ts | 25 ++++++ .../common/utils/unflatten_entity.ts | 13 --- .../alerts_badge/alerts_badge.test.tsx | 89 ++++++++++++------- .../entities_grid/entities_grid.stories.tsx | 4 +- .../entity_name/entity_name.test.tsx | 28 +++--- .../entities_grid/entity_name/index.tsx | 7 +- .../public/components/entities_grid/index.tsx | 4 +- .../entities_grid/mock/entities_mock.ts | 73 ++++++++------- .../public/components/entity_icon/index.tsx | 14 ++- .../hooks/use_detail_view_redirect.test.ts | 31 +++---- .../public/hooks/use_detail_view_redirect.ts | 17 ++-- .../entities/get_identify_fields.test.ts | 12 +-- .../get_identity_fields_per_entity_type.ts | 2 +- .../inventory/server/routes/entities/route.ts | 2 +- 18 files changed, 189 insertions(+), 189 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts delete mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index f996f088df7e..a2ebf9bf9853 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -11,12 +11,12 @@ import { arrayOfStringsSchema } from './common'; export const entityBaseSchema = z.object({ id: z.string(), type: z.string(), - identityFields: z.union([arrayOfStringsSchema, z.string()]), - displayName: z.string(), + identity_fields: z.union([arrayOfStringsSchema, z.string()]), + display_name: z.string(), metrics: z.optional(z.record(z.string(), z.number())), - definitionVersion: z.string(), - schemaVersion: z.string(), - definitionId: z.string(), + definition_version: z.string(), + schema_version: z.string(), + definition_id: z.string(), }); export interface MetadataRecord { diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index dbaf1205cdf9..0a13952a6d69 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -14,7 +14,7 @@ const commonEntityFields: EnitityInstance = { id: '1', display_name: 'entity_name', definition_id: 'entity_definition_id', - } as EnitityInstance['entity'], + }, }; describe('EntityClient', () => { diff --git a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx index d3d28fe04019..0188ed314303 100644 --- a/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx +++ b/x-pack/plugins/observability_solution/inventory/.storybook/get_mock_inventory_context.tsx @@ -24,7 +24,14 @@ export function getMockInventoryContext(): InventoryKibanaContext { return { ...coreStart, - entityManager: {} as unknown as EntityManagerPublicPluginStart, + entityManager: { + entityClient: { + asKqlFilter: jest.fn(), + getIdentityFieldsValue() { + return 'entity_id'; + }, + }, + } as unknown as EntityManagerPublicPluginStart, observabilityShared: {} as unknown as ObservabilitySharedPluginStart, inference: {} as unknown as InferencePublicStart, share: { diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 3e1c2c838360..705a0d5a5a21 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -7,17 +7,13 @@ import { z } from '@kbn/zod'; import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; import { - ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; import { decode, encode } from '@kbn/rison'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { AgentName } from '@kbn/elastic-agent-utils'; export const entityColumnIdsRt = t.union([ t.literal(ENTITY_DISPLAY_NAME), @@ -103,17 +99,6 @@ export const entityTypesRt = new t.Type( (arr) => arr.join() ); -export interface Entity { - [ENTITY_LAST_SEEN]: string; - [ENTITY_ID]: string; - [ENTITY_TYPE]: string; - [ENTITY_DISPLAY_NAME]: string; - [ENTITY_DEFINITION_ID]: string; - [ENTITY_IDENTITY_FIELDS]: string | string[]; - alertsCount?: number; - [key: string]: any; -} - export type EntityGroup = { count: number; } & { @@ -123,24 +108,3 @@ export type EntityGroup = { export type InventoryEntityLatest = z.infer & { alertsCount?: number; }; - -export const isHostEntity = ( - entity: InventoryEntityLatest -): entity is InventoryEntityLatest & { cloud?: { provider: string } } => { - return entity.entity.type === 'host'; -}; - -export const isContainerEntity = ( - entity: InventoryEntityLatest -): entity is InventoryEntityLatest & { cloud?: { provider: string } } => { - return entity.entity.type === 'container'; -}; - -export const isServiceEntity = ( - entity: InventoryEntityLatest -): entity is InventoryEntityLatest & { - agent?: { name: AgentName }; - service: { name: string; environment: string }; -} => { - return entity.entity.type === 'service'; -}; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts new file mode 100644 index 000000000000..8665f22996aa --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentName } from '@kbn/elastic-agent-utils'; +import type { InventoryEntityLatest } from '../entities'; + +interface EntityMap { + host: InventoryEntityLatest & { cloud?: { provider?: string } }; + container: InventoryEntityLatest & { cloud?: { provider?: string } }; + service: InventoryEntityLatest & { + agent?: { name: AgentName }; + service?: { name: string; environment?: string }; + }; +} + +export const isEntityOfType = ( + type: T, + entity: InventoryEntityLatest +): entity is EntityMap[T] => { + return entity.entity.type === type; +}; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts b/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts deleted file mode 100644 index 758d185a5753..000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/utils/unflatten_entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { unflattenObject } from '@kbn/observability-utils/object/unflatten_object'; -import type { Entity, InventoryEntityLatest } from '../entities'; - -export function unflattenEntity(entity: Entity) { - return unflattenObject(entity) as InventoryEntityLatest; -} 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 index b5244cb29f7f..8e549315e17d 100644 --- 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 @@ -8,11 +8,18 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { AlertsBadge } from './alerts_badge'; import { useKibana } from '../../hooks/use_kibana'; -import type { Entity } from '../../../common/entities'; +import type { InventoryEntityLatest } from '../../../common/entities'; jest.mock('../../hooks/use_kibana'); const useKibanaMock = useKibana as jest.Mock; +const commonEntityFields: Partial = { + last_seen_timestamp: 'foo', + id: '1', + definition_version: '1', + schema_version: '1', +}; + describe('AlertsBadge', () => { const mockAsKqlFilter = jest.fn(); @@ -40,16 +47,21 @@ describe('AlertsBadge', () => { }); it('render alerts badge for a host entity', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'entity.id': '1', - 'entity.type': 'host', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'host.name', - 'host.name': 'foo', - 'entity.definition_id': 'host', - 'cloud.provider': null, + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: 'host', + display_name: 'foo', + identity_fields: 'host.name', + definition_id: 'host', + }, alertsCount: 1, + host: { + name: 'foo', + }, + cloud: { + provider: null, + }, }; mockAsKqlFilter.mockReturnValue('host.name: foo'); @@ -60,16 +72,24 @@ describe('AlertsBadge', () => { expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); }); it('render alerts badge for a service entity', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'agent.name': 'node', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': 'service.name', - 'service.name': 'bar', - 'entity.definition_id': 'host', - 'cloud.provider': null, + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: 'service', + display_name: 'foo', + identity_fields: 'service.name', + definition_id: 'service', + }, + service: { + name: 'bar', + }, + agent: { + name: 'node', + }, + cloud: { + provider: null, + }, + alertsCount: 5, }; mockAsKqlFilter.mockReturnValue('service.name: bar'); @@ -81,17 +101,24 @@ describe('AlertsBadge', () => { expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); }); it('render alerts badge for a service entity with multiple identity fields', () => { - const entity: Entity = { - 'entity.last_seen_timestamp': 'foo', - 'agent.name': 'node', - 'entity.id': '1', - 'entity.type': 'service', - 'entity.display_name': 'foo', - 'entity.identity_fields': ['service.name', 'service.environment'], - 'service.name': 'bar', - 'service.environment': 'prod', - 'entity.definition_id': 'host', - 'cloud.provider': null, + const entity: InventoryEntityLatest = { + entity: { + ...(commonEntityFields as InventoryEntityLatest['entity']), + type: 'service', + display_name: 'foo', + identity_fields: ['service.name', 'service.environment'], + definition_id: 'service', + }, + service: { + name: 'bar', + environment: 'prod', + }, + agent: { + name: 'node', + }, + cloud: { + provider: null, + }, alertsCount: 2, }; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx index 047c2e73d0d3..255b2fd263c5 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiDataGridSorting, EuiFlexGroup, EuiFlexItem } from '@elast import { Meta, Story } from '@storybook/react'; import { orderBy } from 'lodash'; import React, { useMemo, useState } from 'react'; -import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { ENTITY_LAST_SEEN } from '@kbn/observability-shared-plugin/common'; import { useArgs } from '@storybook/addons'; import { EntitiesGrid } from '.'; import { entitiesMock } from './mock/entities_mock'; @@ -45,7 +45,7 @@ export const Grid: Story = (args) => { const filteredAndSortedItems = useMemo( () => orderBy( - entityType ? entitiesMock.filter((mock) => mock[ENTITY_TYPE] === entityType) : entitiesMock, + entityType ? entitiesMock.filter((mock) => mock.entity.type === entityType) : entitiesMock, sort.id, sort.direction ), diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx index d5d08ed415a4..5b2c86225367 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -9,28 +9,24 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { EntityName } from '.'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; -import { Entity } from '../../../../common/entities'; -import { - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_ID, - ENTITY_IDENTITY_FIELDS, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import { InventoryEntityLatest } from '../../../../common/entities'; jest.mock('../../../hooks/use_detail_view_redirect'); const useDetailViewRedirectMock = useDetailViewRedirect as jest.Mock; describe('EntityName', () => { - const mockEntity: Entity = { - [ENTITY_LAST_SEEN]: '2023-10-09T00:00:00Z', - [ENTITY_ID]: '1', - [ENTITY_DISPLAY_NAME]: 'entity_name', - [ENTITY_DEFINITION_ID]: 'entity_definition_id', - [ENTITY_IDENTITY_FIELDS]: ['service.name', 'service.environment'], - [ENTITY_TYPE]: 'service', + const mockEntity: InventoryEntityLatest = { + entity: { + last_seen_timestamp: '2023-10-09T00:00:00Z', + id: '1', + type: 'service', + display_name: 'entity_name', + identity_fields: ['service.name', 'service.environment'], + definition_id: 'entity_definition_id', + definition_version: '1.0.0', + schema_version: '1.0.0', + }, }; beforeEach(() => { diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 11c63b62634a..82ba1337bf82 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -6,7 +6,8 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { useKibana } from '../../../hooks/use_kibana'; import { InventoryEntityLatest } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; @@ -27,7 +28,7 @@ export function EntityName({ entity }: EntityNameProps) { const handleLinkClick = useCallback(() => { telemetry.reportEntityViewClicked({ view_type: 'detail', - entity_type: entity['entity.type'], + entity_type: entity.entity.type, }); }, [entity, telemetry]); @@ -38,7 +39,7 @@ export function EntityName({ entity }: EntityNameProps) { - {entity.entity.displayName} + {entity.entity.display_name} 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 331e19e54fec..73a918d18155 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 @@ -107,7 +107,7 @@ export function EntitiesGrid({ values={{ date: ( { let id = 0; @@ -33,38 +25,51 @@ function generateRandomTimestamp() { return randomDate.toISOString(); } -const getEntity = (entityType: string, customFields: Record = {}) => ({ - [ENTITY_LAST_SEEN]: generateRandomTimestamp(), - [ENTITY_TYPE]: entityType, - [ENTITY_DISPLAY_NAME]: faker.person.fullName(), - [ENTITY_ID]: generateId(), - ...customFields, -}); +const indentityFieldsPerType: Record = { + host: ['host.name'], + container: ['container.id'], + service: ['service.name'], +}; -const alertsMock = [ - { - ...getEntity('host'), - alertsCount: 3, - }, - { - ...getEntity('service'), - alertsCount: 3, +const getEntityLatest = ( + entityType: string, + overrides?: Partial +): InventoryEntityLatest => ({ + entity: { + last_seen_timestamp: generateRandomTimestamp(), + type: entityType, + display_name: faker.person.fullName(), + id: generateId(), + definition_id: faker.string.uuid(), + definition_version: '1.0.0', + identity_fields: indentityFieldsPerType[entityType], + schema_version: '1.0.0', + ...(overrides?.entity ? overrides.entity : undefined), }, + ...((overrides ? overrides : {}) as Record), +}); - { - ...getEntity('host'), +const alertsMock: InventoryEntityLatest[] = [ + getEntityLatest('host', { + alertsCount: 1, + }), + getEntityLatest('service', { + alertsCount: 3, + }), + getEntityLatest('host', { alertsCount: 10, - }, - { - ...getEntity('host'), + }), + getEntityLatest('host', { alertsCount: 1, - }, + }), ]; -const hostsMock = Array.from({ length: 20 }, () => getEntity('host', { [CLOUD_PROVIDER]: 'gcp' })); -const containersMock = Array.from({ length: 20 }, () => getEntity('container')); +const hostsMock = Array.from({ length: 20 }, () => + getEntityLatest('host', { cloud: { provider: 'gcp' } }) +); +const containersMock = Array.from({ length: 20 }, () => getEntityLatest('container')); const servicesMock = Array.from({ length: 20 }, () => - getEntity('service', { [AGENT_NAME]: 'java' }) + getEntityLatest('service', { agent: { name: 'java' } }) ); export const entitiesMock = [ @@ -72,4 +77,4 @@ export const entitiesMock = [ ...hostsMock, ...containersMock, ...servicesMock, -] as Entity[]; +] as InventoryEntityLatest[]; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 860c6c5ca2e8..8ecb372b2cea 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -9,12 +9,8 @@ import React from 'react'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; -import { - isHostEntity, - type InventoryEntityLatest, - isContainerEntity, - isServiceEntity, -} from '../../../common/entities'; +import type { InventoryEntityLatest } from '../../../common/entities'; +import { isEntityOfType } from '../../../common/utils/entity_type_guards'; interface EntityIconProps { entity: InventoryEntityLatest; @@ -23,7 +19,7 @@ interface EntityIconProps { export function EntityIcon({ entity }: EntityIconProps) { const defaultIconSize = euiThemeVars.euiSizeL; - if (isHostEntity(entity) || isContainerEntity(entity)) { + if (isEntityOfType('host', entity) || isEntityOfType('container', entity)) { const cloudProvider = entity.cloud?.provider; return ( @@ -44,11 +40,11 @@ export function EntityIcon({ entity }: EntityIconProps) { ); } - if (isServiceEntity(entity)) { + if (isEntityOfType('service', entity)) { return ; } - if (entityType.startsWith('kubernetes')) { + if (entity.entity.type.startsWith('kubernetes')) { return ; } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index 0cc68bcea5ee..f74303fc02ca 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -8,23 +8,20 @@ import { renderHook } from '@testing-library/react-hooks'; import { useDetailViewRedirect } from './use_detail_view_redirect'; import { useKibana } from './use_kibana'; -import { CONTAINER_ID, HOST_NAME, SERVICE_NAME } from '@kbn/observability-shared-plugin/common'; -import { unflattenEntity } from '../../common/utils/unflatten_entity'; +import { ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; import type { InventoryEntityLatest } from '../../common/entities'; jest.mock('./use_kibana'); -jest.mock('../../common/utils/unflatten_entity'); const useKibanaMock = useKibana as jest.Mock; -const unflattenEntityMock = unflattenEntity as jest.Mock; const commonEntityFields: Partial = { - lastSeenTimestamp: '2023-10-09T00:00:00Z', + last_seen_timestamp: '2023-10-09T00:00:00Z', id: '1', - displayName: 'entity_name', - definitionId: 'entity_definition_id', - definitionVersion: '1', - schemaVersion: '1', + display_name: 'entity_name', + definition_id: 'entity_definition_id', + definition_version: '1', + schema_version: '1', }; describe('useDetailViewRedirect', () => { @@ -54,8 +51,6 @@ describe('useDetailViewRedirect', () => { }, }, }); - - unflattenEntityMock.mockImplementation((entity) => entity); }); it('getEntityRedirectUrl should return the correct URL for host entity', () => { @@ -63,7 +58,7 @@ describe('useDetailViewRedirect', () => { entity: { ...(commonEntityFields as InventoryEntityLatest['entity']), type: 'host', - identityFields: ['host.name'], + identity_fields: ['host.name'], }, host: { name: 'host-1', @@ -73,7 +68,7 @@ describe('useDetailViewRedirect', () => { }, }; - mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' }); + mockGetIdentityFieldsValue.mockReturnValue({ host: { name: 'host-1' } }); mockGetRedirectUrl.mockReturnValue('asset-details-url'); const { result } = renderHook(() => useDetailViewRedirect()); @@ -88,7 +83,7 @@ describe('useDetailViewRedirect', () => { entity: { ...(commonEntityFields as InventoryEntityLatest['entity']), type: 'container', - identityFields: ['container.id'], + identity_fields: ['container.id'], }, container: { id: 'container-1', @@ -98,7 +93,7 @@ describe('useDetailViewRedirect', () => { }, }; - mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' }); + mockGetIdentityFieldsValue.mockReturnValue({ container: { id: 'container-1' } }); mockGetRedirectUrl.mockReturnValue('asset-details-url'); const { result } = renderHook(() => useDetailViewRedirect()); @@ -116,7 +111,7 @@ describe('useDetailViewRedirect', () => { entity: { ...(commonEntityFields as InventoryEntityLatest['entity']), type: 'service', - identityFields: ['service.name'], + identity_fields: ['service.name'], }, agent: { name: 'node', @@ -126,7 +121,7 @@ describe('useDetailViewRedirect', () => { environment: 'prod', }, }; - mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' }); + mockGetIdentityFieldsValue.mockReturnValue({ service: { name: 'service-1' } }); mockGetRedirectUrl.mockReturnValue('service-overview-url'); const { result } = renderHook(() => useDetailViewRedirect()); @@ -155,7 +150,7 @@ describe('useDetailViewRedirect', () => { entity: { ...(commonEntityFields as InventoryEntityLatest['entity']), type: entityType, - identityFields: ['some.field'], + identity_fields: ['some.field'], }, some: { field: 'some-value', diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 518f1f8dfb1a..58d637145281 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -7,6 +7,7 @@ import { ASSET_DETAILS_LOCATOR_ID, AssetDetailsLocatorParams, + ENTITY_TYPES, SERVICE_OVERVIEW_LOCATOR_ID, ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; @@ -14,12 +15,8 @@ import { useCallback } from 'react'; import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; -import { - isHostEntity, - type InventoryEntityLatest, - isContainerEntity, - isServiceEntity, -} from '../../common/entities'; +import { isEntityOfType } from '../../common/utils/entity_type_guards'; +import { type InventoryEntityLatest } from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { @@ -46,7 +43,7 @@ export const useDetailViewRedirect = () => { const getSingleIdentityFieldValue = useCallback( (latestEntity: InventoryEntityLatest) => { - const identityFields = castArray(latestEntity.entity.identityFields); + const identityFields = castArray(latestEntity.entity.identity_fields); if (identityFields.length > 1) { throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`); } @@ -61,19 +58,19 @@ export const useDetailViewRedirect = () => { (latestEntity: InventoryEntityLatest) => { const identityValue = getSingleIdentityFieldValue(latestEntity); - if (isHostEntity(latestEntity) || isContainerEntity(latestEntity)) { + if (isEntityOfType('host', latestEntity) || isEntityOfType('container', latestEntity)) { return assetDetailsLocator?.getRedirectUrl({ assetId: identityValue, assetType: latestEntity.entity.type, }); } - if (isServiceEntity(latestEntity)) { + if (isEntityOfType('service', latestEntity)) { return serviceOverviewLocator?.getRedirectUrl({ serviceName: identityValue, // service.environemnt is not part of entity.identityFields // we need to manually get its value - environment: latestEntity.service.environment, + environment: latestEntity.service?.environment, }); } 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 40fd25d9ad74..7b59c7064b89 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 @@ -10,10 +10,10 @@ import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity const commonEntityFields: InventoryEntityLatest = { entity: { - lastSeenTimestamp: '2023-10-09T00:00:00Z', + last_seen_timestamp: '2023-10-09T00:00:00Z', id: '1', - displayName: 'entity_name', - definitionId: 'entity_definition_id', + display_name: 'entity_name', + definition_id: 'entity_definition_id', } as InventoryEntityLatest['entity'], alertCount: 3, }; @@ -30,7 +30,7 @@ describe('getIdentityFields', () => { }, entity: { ...commonEntityFields.entity, - identityFields: ['service.name', 'service.environment'], + identity_fields: ['service.name', 'service.environment'], type: 'service', }, service: { @@ -42,7 +42,7 @@ describe('getIdentityFields', () => { ...commonEntityFields, entity: { ...commonEntityFields.entity, - identityFields: ['host.name'], + identity_fields: ['host.name'], type: 'host', }, host: { @@ -57,7 +57,7 @@ describe('getIdentityFields', () => { ...commonEntityFields, entity: { ...commonEntityFields.entity, - identityFields: ['container.id'], + identity_fields: ['container.id'], type: 'container', }, host: { 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 b755bc87e530..c6e5aedf987a 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 @@ -14,7 +14,7 @@ export const getIdentityFieldsPerEntityType = (entities: InventoryEntityLatest[] const identityFieldsPerEntityType = new Map(); entities.forEach((entity) => - identityFieldsPerEntityType.set(entity.entity.type, castArray(entity.entity.identityFields)) + identityFieldsPerEntityType.set(entity.entity.type, castArray(entity.entity.identity_fields)) ); return identityFieldsPerEntityType; 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 631a544a1ac2..6b22b66ba4c5 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 @@ -61,7 +61,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ logger, plugins, request, - }): Promise<{ entities: Entity[] }> => { + }): Promise<{ entities: InventoryEntityLatest[] }> => { const coreContext = await context.core; const inventoryEsClient = createObservabilityEsClient({ client: coreContext.elasticsearch.client.asCurrentUser, From 8ba7131fcb6ea3c89a01e6c5269de777f1985155 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 4 Nov 2024 12:08:12 +0100 Subject: [PATCH 04/27] Adjust group by feature --- .../src/lib/entities/kubernetes/index.ts | 10 +++++----- .../public/lib/entity_client.test.ts | 2 +- .../inventory_group_accordion.tsx | 3 ++- .../server/routes/entities/get_entity_groups.ts | 5 +---- .../server/routes/entities/get_entity_types.ts | 4 ++-- .../server/routes/has_data/get_has_data.ts | 15 +++++---------- 6 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts index 6033533cf359..6da1decaab9a 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/entities/kubernetes/index.ts @@ -55,11 +55,11 @@ export class K8sEntity extends Serializable { super({ ...fields, 'entity.type': entityTypeWithSchema, - 'entity.definitionId': `builtin_${entityTypeWithSchema}`, - 'entity.identityFields': identityFields, - 'entity.displayName': getDisplayName({ identityFields, fields }), - 'entity.definitionVersion': '1.0.0', - 'entity.schemaVersion': '1.0', + 'entity.definition_id': `builtin_${entityTypeWithSchema}`, + 'entity.identity_fields': identityFields, + 'entity.display_name': getDisplayName({ identityFields, fields }), + 'entity.definition_version': '1.0.0', + 'entity.schema_version': '1.0', }); } } diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index 0a13952a6d69..dbaf1205cdf9 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -14,7 +14,7 @@ const commonEntityFields: EnitityInstance = { id: '1', display_name: 'entity_name', definition_id: 'entity_definition_id', - }, + } as EnitityInstance['entity'], }; describe('EntityClient', () => { diff --git a/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/inventory_group_accordion.tsx b/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/inventory_group_accordion.tsx index 4c5d34e5a028..f5b52603fb64 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/inventory_group_accordion.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/inventory_group_accordion.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle, useEuiTheme } from '@elastic/eui'; +import { flattenObject } from '@kbn/observability-utils/object/flatten_object'; import { GroupedEntitiesGrid } from './grouped_entities_grid'; import type { EntityGroup } from '../../../common/entities'; import { InventoryPanelBadge } from './inventory_panel_badge'; @@ -29,7 +30,7 @@ export function InventoryGroupAccordion({ isLoading, }: InventoryGroupAccordionProps) { const { euiTheme } = useEuiTheme(); - const field = group[groupBy]; + const field = flattenObject(group)[groupBy]; const [open, setOpen] = useState(false); const onToggle = useCallback(() => { diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts index b61f245f1aaf..c978766fe46b 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_groups.ts @@ -7,7 +7,6 @@ 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_TYPE } from '@kbn/observability-shared-plugin/common'; import { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; import { @@ -44,7 +43,7 @@ export async function getEntityGroupsBy({ const limit = `LIMIT ${MAX_NUMBER_OF_ENTITIES}`; const query = [from, ...where, group, sort, limit].join(' | '); - const groups = await inventoryEsClient.esql('get_entities_groups', { + return inventoryEsClient.esql('get_entities_groups', { query, filter: { bool: { @@ -53,6 +52,4 @@ export async function getEntityGroupsBy({ }, params, }); - - return esqlResultToPlainObjects(groups); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index 32631e062d9b..081545270297 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -15,7 +15,7 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql<{ [ENTITY_TYPE]: string }>( + const entityTypesEsqlResponse = await inventoryEsClient.esql<{ entity: { type: string } }>( 'get_entity_types', { query: `FROM ${ENTITIES_LATEST_ALIAS} @@ -25,5 +25,5 @@ export async function getEntityTypes({ } ); - return entityTypesEsqlResponse.flatMap((types) => Object.values(types)); + return entityTypesEsqlResponse.map((response) => response.entity.type); } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts index d328a4f3b8d6..ce9ca5611f0f 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -16,19 +16,14 @@ export async function getHasData({ inventoryEsClient: ObservabilityElasticsearchClient; logger: Logger; }) { - try { - const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { - query: `FROM ${ENTITIES_LATEST_ALIAS} + const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { + query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS _count = COUNT(*) | LIMIT 1`, - }); + }); - const totalCount = esqlResults[0]._count; + const totalCount = esqlResults[0]._count; - return { hasData: totalCount > 0 }; - } catch (e) { - logger.error(e); - return { hasData: false }; - } + return { hasData: totalCount > 0 }; } From 9321024bf891e2ed0e5b44cc134fc0b3f65bac64 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 4 Nov 2024 17:56:38 +0100 Subject: [PATCH 05/27] Fix imports --- packages/kbn-es-types/src/search.ts | 9 +++------ .../components/entities_grid/entity_name/index.tsx | 2 +- .../inventory/public/hooks/use_detail_view_redirect.ts | 8 ++++---- .../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 | 6 +++--- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/kbn-es-types/src/search.ts b/packages/kbn-es-types/src/search.ts index 42d870cae108..87f9dd15517c 100644 --- a/packages/kbn-es-types/src/search.ts +++ b/packages/kbn-es-types/src/search.ts @@ -674,15 +674,12 @@ export interface ESQLColumn { export type ESQLRow = unknown[]; -export interface ESQLSearchResponse< - TColumn extends ESQLColumn = ESQLColumn, - TRow extends ESQLRow = ESQLRow -> { - columns: TColumn[]; +export interface ESQLSearchResponse { + columns: ESQLColumn[]; // In case of ?drop_null_columns in the query, then // all_columns will have available and empty fields // while columns only the available ones (non nulls) - all_columns?: TRow[]; + all_columns?: ESQLColumn[]; values: ESQLRow[]; took?: number; _clusters?: estypes.ClusterStatistics; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 82ba1337bf82..4c71883c4343 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React, { useCallback } from 'react'; import { useKibana } from '../../../hooks/use_kibana'; -import { InventoryEntityLatest } from '../../../../common/entities'; +import type { InventoryEntityLatest } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 58d637145281..a43e7819e30c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -6,17 +6,17 @@ */ import { ASSET_DETAILS_LOCATOR_ID, - AssetDetailsLocatorParams, ENTITY_TYPES, SERVICE_OVERVIEW_LOCATOR_ID, - ServiceOverviewParams, + type AssetDetailsLocatorParams, + type ServiceOverviewParams, } from '@kbn/observability-shared-plugin/common'; import { useCallback } from 'react'; -import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; +import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; import { isEntityOfType } from '../../common/utils/entity_type_guards'; -import { type InventoryEntityLatest } from '../../common/entities'; +import type { InventoryEntityLatest } from '../../common/entities'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: 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 index 7b59c7064b89..ffe5aa715dcb 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 { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntityLatest } from '../../../common/entities'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; const commonEntityFields: InventoryEntityLatest = { 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 c6e5aedf987a..591c14741227 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 @@ -6,7 +6,7 @@ */ import { castArray } from 'lodash'; -import { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntityLatest } from '../../../common/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 1e4d3b341a0c..4f392650f62c 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 @@ -5,15 +5,15 @@ * 2.0. */ -import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; +import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, type EntityColumnIds, - InventoryEntityLatest, + type InventoryEntityLatest, } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; From 5155dcff5336f6d7826a7c4a00f7d0998b9b408e Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 5 Nov 2024 18:08:45 +0100 Subject: [PATCH 06/27] Create an inteface between API and UI independent from index mapping --- .../kbn-entities-schema/src/schema/entity.ts | 14 ++++-- .../array/join_by_key.test.ts | 46 +++++++++++++++++++ .../observability_utils/array/join_by_key.ts | 17 +++++-- .../es/utils/esql_result_to_plain_objects.ts | 2 +- .../routes/entities/get_data_stream_types.ts | 12 ++--- .../routes/entities/get_latest_entity.ts | 13 +++--- .../inventory/common/entities.ts | 30 ++++++++---- .../common/utils/entity_type_guards.ts | 8 ++-- .../inventory/common/utils/mappers.ts | 37 +++++++++++++++ .../alerts_badge/alerts_badge.test.tsx | 44 ++++++++---------- .../components/alerts_badge/alerts_badge.tsx | 3 +- .../entity_name/entity_name.test.tsx | 20 ++++---- .../entities_grid/entity_name/index.tsx | 4 +- .../components/entities_grid/grid_columns.tsx | 21 +++------ .../public/components/entities_grid/index.tsx | 24 ++++------ .../entities_grid/mock/entities_mock.ts | 11 ++--- .../public/components/entity_icon/index.tsx | 7 +-- .../hooks/use_detail_view_redirect.test.ts | 46 ++++++++----------- .../public/hooks/use_detail_view_redirect.ts | 17 ++++--- .../routes/entities/get_entity_types.ts | 4 +- .../entities/get_identify_fields.test.ts | 46 ++++++++----------- .../get_identity_fields_per_entity_type.ts | 6 +-- .../routes/entities/get_latest_entities.ts | 37 +++++++++++++-- .../entities/get_latest_entities_alerts.ts | 5 +- .../inventory/server/routes/entities/route.ts | 6 +-- 25 files changed, 287 insertions(+), 193 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index a2ebf9bf9853..2b1c732a7a01 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -24,10 +24,18 @@ export interface MetadataRecord { } const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); + type Literal = z.infer; -type Metadata = Literal | { [key: string]: Metadata } | Metadata[]; -export const entityMetadataSchema: z.ZodType = z.lazy(() => - z.union([literalSchema, z.array(entityMetadataSchema), z.record(entityMetadataSchema)]) +interface Metadata { + [key: string]: Literal | Literal[] | Metadata; +} + +// Define the recursive schema using z.lazy +export const entityMetadataSchema: z.ZodSchema = z.lazy(() => + z.union([ + z.record(literalSchema), // Object with string keys and values of type string, number, or boolean + entityMetadataSchema, // Recursive reference to SomeType + ]) ); export const entityLatestSchema = z diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts index 8e0fc6ad0947..bb1d5a2e2410 100644 --- a/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.test.ts @@ -221,4 +221,50 @@ describe('joinByKey', () => { }, }); }); + + it('deeply merges by unflatten keys', () => { + const joined = joinByKey( + [ + { + service: { + name: 'opbeans-node', + metrics: { + cpu: 0.1, + }, + }, + properties: { + foo: 'bar', + }, + }, + { + service: { + environment: 'prod', + metrics: { + memory: 0.5, + }, + }, + properties: { + foo: 'bar', + }, + }, + ], + 'properties.foo' + ); + + expect(joined).toEqual([ + { + service: { + name: 'opbeans-node', + environment: 'prod', + metrics: { + cpu: 0.1, + memory: 0.5, + }, + }, + properties: { + foo: 'bar', + }, + }, + ]); + }); }); diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts index a5baf6e6b7bc..ce679f6b1cc3 100644 --- a/x-pack/packages/observability/observability_utils/array/join_by_key.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -19,21 +19,28 @@ export type JoinedReturnType< >; function getValueByPath(obj: any, path: string): any { - return path.split('.').reduce((acc, part) => acc && acc[part], obj); + return path.split('.').reduce((acc, keyPart) => { + // Check if acc is a valid object and has the key + return acc && acc[keyPart] !== undefined ? acc[keyPart] : undefined; + }, obj); } -type ArrayOrSingle = T | T[]; +type NestedKeys = T extends object + ? { [K in keyof T]: K extends string ? `${K}` | `${K}.${NestedKeys}` : never }[keyof T] + : never; +type ArrayOrSingle = T | T[]; +type CombinedNestedKeys = (NestedKeys & NestedKeys) | (keyof T & keyof U); export function joinByKey< T extends Record, U extends UnionToIntersection, - V extends ArrayOrSingle + V extends ArrayOrSingle> >(items: T[], key: V): JoinedReturnType; export function joinByKey< T extends Record, U extends UnionToIntersection, - V extends ArrayOrSingle, + V extends ArrayOrSingle>, W extends JoinedReturnType, X extends (a: T, b: T) => ValuesType >(items: T[], key: V, mergeFn: X): W; @@ -49,7 +56,7 @@ export function joinByKey( 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) => getValueByPath(current, k))); + const stableKey = stableStringify(keys.map((k) => current[k] ?? getValueByPath(current, k))); if (map.has(stableKey)) { const item = map.get(stableKey); diff --git a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts index 233385442101..717983a2958c 100644 --- a/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts +++ b/x-pack/packages/observability/observability_utils/es/utils/esql_result_to_plain_objects.ts @@ -23,7 +23,7 @@ export function esqlResultToPlainObjects( // Removes the type suffix from the column name const name = column.name.replace(/\.(text|keyword)$/, ''); if (!acc[name]) { - acc[column.name] = value; + acc[name] = value; } return acc; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts index 3218ae257f1a..afd57a85c109 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts @@ -7,11 +7,9 @@ import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; import { findInventoryFields } from '@kbn/metrics-data-access-plugin/common'; -import { - EntityDataStreamType, - SOURCE_DATA_STREAM_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import { EntityDataStreamType } from '@kbn/observability-shared-plugin/common'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { castArray } from 'lodash'; import { type InfraMetricsClient } from '../../lib/helpers/get_infra_metrics_client'; import { getHasMetricsData } from './get_has_metrics_data'; import { getLatestEntity } from './get_latest_entity'; @@ -45,15 +43,15 @@ export async function getDataStreamTypes({ return Array.from(sourceDataStreams); } - const entity = await getLatestEntity({ + const latestEntity = await getLatestEntity({ inventoryEsClient: obsEsClient, entityId, entityType, entityManagerClient, }); - if (entity?.[SOURCE_DATA_STREAM_TYPE]) { - [entity[SOURCE_DATA_STREAM_TYPE]].flat().forEach((item) => { + if (latestEntity) { + castArray(latestEntity.source_data_stream.type).forEach((item) => { sourceDataStreams.add(item as EntityDataStreamType); }); } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts index 7bcce2964fd1..06d47feb83d2 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -12,15 +12,14 @@ import { SOURCE_DATA_STREAM_TYPE, } from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; -import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ type: '*', dataset: ENTITY_LATEST, }); -interface Entity { - [SOURCE_DATA_STREAM_TYPE]: string | string[]; +interface LatestEntityResponse { + source_data_stream: { type?: string | string[] }; } export async function getLatestEntity({ @@ -33,7 +32,7 @@ export async function getLatestEntity({ entityType: 'host' | 'container'; entityId: string; entityManagerClient: EntityClient; -}): Promise { +}): Promise { const { definitions } = await entityManagerClient.getEntityDefinitions({ builtIn: true, type: entityType, @@ -41,10 +40,10 @@ export async function getLatestEntity({ const hostOrContainerIdentityField = definitions[0]?.identityFields?.[0]?.field; if (hostOrContainerIdentityField === undefined) { - return { [SOURCE_DATA_STREAM_TYPE]: [] }; + return undefined; } - const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { + const response = await inventoryEsClient.esql('get_latest_entities', { query: `FROM ${ENTITIES_LATEST_ALIAS} | WHERE ${ENTITY_TYPE} == ? | WHERE ${hostOrContainerIdentityField} == ? @@ -53,5 +52,5 @@ export async function getLatestEntity({ params: [entityType, entityId], }); - return esqlResultToPlainObjects(latestEntitiesEsqlResponse)[0]; + return response[0]; } diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 705a0d5a5a21..41e7200d5a7e 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -5,20 +5,20 @@ * 2.0. */ import { z } from '@kbn/zod'; -import { ENTITY_LATEST, entitiesAliasPattern, entityLatestSchema } from '@kbn/entities-schema'; import { - ENTITY_DISPLAY_NAME, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; + ENTITY_LATEST, + entitiesAliasPattern, + entityLatestSchema, + entityMetadataSchema, +} from '@kbn/entities-schema'; import { decode, encode } from '@kbn/rison'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; export const entityColumnIdsRt = t.union([ - t.literal(ENTITY_DISPLAY_NAME), - t.literal(ENTITY_LAST_SEEN), - t.literal(ENTITY_TYPE), + t.literal('entityDisplayName'), + t.literal('entityLastSeenTimestamp'), + t.literal('entityType'), t.literal('alertsCount'), ]); @@ -105,6 +105,16 @@ export type EntityGroup = { [key: string]: any; }; -export type InventoryEntityLatest = z.infer & { +export type EntityLatest = z.infer; + +export type InventoryEntityLatest = { + entityId: string; + entityType: string; + entityIdentityFields: string | string[]; + entityDisplayName: string; + entityDefinitionId: string; + entityLastSeenTimestamp: string; + entityDefinitionVersion: string; + entitySchemaVersion: string; alertsCount?: number; -}; +} & z.infer; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts index 8665f22996aa..f15cbd99fa8c 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts @@ -9,10 +9,10 @@ import type { AgentName } from '@kbn/elastic-agent-utils'; import type { InventoryEntityLatest } from '../entities'; interface EntityMap { - host: InventoryEntityLatest & { cloud?: { provider?: string } }; - container: InventoryEntityLatest & { cloud?: { provider?: string } }; + host: InventoryEntityLatest & { cloud?: { provider?: string[] } }; + container: InventoryEntityLatest & { cloud?: { provider?: string[] } }; service: InventoryEntityLatest & { - agent?: { name: AgentName }; + agent?: { name: AgentName[] }; service?: { name: string; environment?: string }; }; } @@ -21,5 +21,5 @@ export const isEntityOfType = ( type: T, entity: InventoryEntityLatest ): entity is EntityMap[T] => { - return entity.entity.type === type; + return entity.entityType === type; }; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts b/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts new file mode 100644 index 000000000000..ae7607341b19 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts @@ -0,0 +1,37 @@ +/* + * 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 { EntityLatest, InventoryEntityLatest } from '../entities'; + +export function toEntityLatest(inventoryEntityLatest: InventoryEntityLatest): EntityLatest { + const { + entityDefinitionId, + entityDisplayName, + entityId, + entityIdentityFields, + entityLastSeenTimestamp, + entityType, + entityDefinitionVersion, + entitySchemaVersion, + alertsCount: _, + ...metadata + } = inventoryEntityLatest; + + return { + entity: { + id: entityId, + type: entityType, + definition_id: entityDefinitionId, + display_name: entityDisplayName, + identity_fields: entityIdentityFields, + last_seen_timestamp: entityLastSeenTimestamp, + definition_version: entityDefinitionVersion, + schema_version: entitySchemaVersion, + }, + ...metadata, + }; +} 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 index 8e549315e17d..282fc0d65274 100644 --- 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 @@ -13,11 +13,9 @@ import type { InventoryEntityLatest } from '../../../common/entities'; jest.mock('../../hooks/use_kibana'); const useKibanaMock = useKibana as jest.Mock; -const commonEntityFields: Partial = { - last_seen_timestamp: 'foo', - id: '1', - definition_version: '1', - schema_version: '1', +const commonEntityFields: Partial = { + entityLastSeenTimestamp: 'foo', + entityId: '1', }; describe('AlertsBadge', () => { @@ -48,13 +46,11 @@ describe('AlertsBadge', () => { it('render alerts badge for a host entity', () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: 'host', - display_name: 'foo', - identity_fields: 'host.name', - definition_id: 'host', - }, + ...(commonEntityFields as InventoryEntityLatest), + entityType: 'host', + entityDisplayName: 'foo', + entityIdentityFields: 'host.name', + entityDefinitionId: 'host', alertsCount: 1, host: { name: 'foo', @@ -73,13 +69,11 @@ describe('AlertsBadge', () => { }); it('render alerts badge for a service entity', () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: 'service', - display_name: 'foo', - identity_fields: 'service.name', - definition_id: 'service', - }, + ...(commonEntityFields as InventoryEntityLatest), + entityType: 'service', + entityDisplayName: 'foo', + entityIdentityFields: 'service.name', + entityDefinitionId: 'service', service: { name: 'bar', }, @@ -102,13 +96,11 @@ describe('AlertsBadge', () => { }); it('render alerts badge for a service entity with multiple identity fields', () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: 'service', - display_name: 'foo', - identity_fields: ['service.name', 'service.environment'], - definition_id: 'service', - }, + ...(commonEntityFields as InventoryEntityLatest), + entityType: 'service', + entityDisplayName: 'foo', + entityIdentityFields: ['service.name', 'service.environment'], + entityDefinitionId: 'service', service: { name: 'bar', environment: 'prod', 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 0fb4a403bf40..cb3062c3556b 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,6 +9,7 @@ import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { InventoryEntityLatest } from '../../../common/entities'; +import { toEntityLatest } from '../../../common/utils/mappers'; import { useKibana } from '../../hooks/use_kibana'; export function AlertsBadge({ entity }: { entity: InventoryEntityLatest }) { @@ -21,7 +22,7 @@ export function AlertsBadge({ entity }: { entity: InventoryEntityLatest }) { const activeAlertsHref = basePath.prepend( `/app/observability/alerts?_a=${rison.encode({ - kuery: entityManager.entityClient.asKqlFilter(entity), + kuery: entityManager.entityClient.asKqlFilter(toEntityLatest(entity)), status: 'active', })}` ); diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx index 5b2c86225367..7658f1239456 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { EntityName } from '.'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; -import { InventoryEntityLatest } from '../../../../common/entities'; +import type { InventoryEntityLatest } from '../../../../common/entities'; jest.mock('../../../hooks/use_detail_view_redirect'); @@ -17,16 +17,14 @@ const useDetailViewRedirectMock = useDetailViewRedirect as jest.Mock; describe('EntityName', () => { const mockEntity: InventoryEntityLatest = { - entity: { - last_seen_timestamp: '2023-10-09T00:00:00Z', - id: '1', - type: 'service', - display_name: 'entity_name', - identity_fields: ['service.name', 'service.environment'], - definition_id: 'entity_definition_id', - definition_version: '1.0.0', - schema_version: '1.0.0', - }, + entityLastSeenTimestamp: '2023-10-09T00:00:00Z', + entityId: '1', + entityType: 'service', + entityDisplayName: 'entity_name', + entityIdentityFields: ['service.name', 'service.environment'], + entityDefinitionId: 'entity_definition_id', + entitySchemaVersion: '1', + entityDefinitionVersion: '1', }; beforeEach(() => { diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 4c71883c4343..75a2976b067f 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -28,7 +28,7 @@ export function EntityName({ entity }: EntityNameProps) { const handleLinkClick = useCallback(() => { telemetry.reportEntityViewClicked({ view_type: 'detail', - entity_type: entity.entity.type, + entity_type: entity.entityType, }); }, [entity, telemetry]); @@ -39,7 +39,7 @@ export function EntityName({ entity }: EntityNameProps) { - {entity.entity.display_name} + {entity.entityDisplayName} 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 96fb8b3736ea..a47b9dbc6e96 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 @@ -8,11 +8,6 @@ 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 '@kbn/observability-shared-plugin/common'; const alertsLabel = i18n.translate('xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel', { defaultMessage: 'Alerts', @@ -66,16 +61,12 @@ const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipCon ); -export const getColumns = ({ - showAlertsColumn, -}: { - showAlertsColumn: boolean; -}): EuiDataGridColumn[] => { +export const getColumns = ({ showAlertsColumn }: { showAlertsColumn: boolean }) => { return [ ...(showAlertsColumn ? [ { - id: 'alertsCount', + id: 'alertsCount' as const, displayAsText: alertsLabel, isSortable: true, display: , @@ -85,21 +76,21 @@ export const getColumns = ({ ] : []), { - id: ENTITY_DISPLAY_NAME, + id: 'entityDisplayName' as const, // keep it for accessibility purposes displayAsText: entityNameLabel, display: , isSortable: true, }, { - id: ENTITY_TYPE, + id: 'entityType' as const, // keep it for accessibility purposes displayAsText: entityTypeLabel, display: , isSortable: true, }, { - id: ENTITY_LAST_SEEN, + id: 'entityLastSeenTimestamp' as const, // keep it for accessibility purposes displayAsText: entityLastSeenLabel, display: ( @@ -109,5 +100,5 @@ export const getColumns = ({ isSortable: true, schema: 'datetime', }, - ]; + ] satisfies EuiDataGridColumn[]; }; 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 73a918d18155..6dab7f340c47 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 @@ -15,11 +15,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; import { last } from 'lodash'; import React, { useCallback, useMemo } from 'react'; -import { - ENTITY_DISPLAY_NAME, - ENTITY_LAST_SEEN, - ENTITY_TYPE, -} from '@kbn/observability-shared-plugin/common'; +import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { EntityColumnIds } from '../../../common/entities'; import { APIReturnType } from '../../api'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; @@ -28,11 +24,11 @@ import { AlertsBadge } from '../alerts_badge/alerts_badge'; import { EntityName } from './entity_name'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; -type LatestEntities = InventoryEntitiesAPIReturnType['entities']; +type InventoryLatestEntities = InventoryEntitiesAPIReturnType['entities']; interface Props { loading: boolean; - entities: LatestEntities; + entities: InventoryLatestEntities; sortDirection: 'asc' | 'desc'; sortField: string; pageIndex: number; @@ -84,13 +80,13 @@ export function EntitiesGrid({ } const columnEntityTableId = columnId as EntityColumnIds; - const entityType = entity.entity.type; + const entityType = entity.entityType; switch (columnEntityTableId) { case 'alertsCount': return entity?.alertsCount ? : null; - case ENTITY_TYPE: + case 'entityType': return ( onFilterByType(entityType)} /> ); - case ENTITY_LAST_SEEN: + case 'entityLastSeenTimestamp': return ( ); - case ENTITY_DISPLAY_NAME: + case 'entityDisplayName': return ; default: - return ''; + return 'a'; } }, [entities, onFilterByType] 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 627933828ac8..6053592c70d1 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 @@ -6,7 +6,7 @@ */ import { faker } from '@faker-js/faker'; -import type { InventoryEntityLatest } from '../../../../common/entities'; +import type { EntityLatest } from '../../../../common/entities'; const idGenerator = () => { let id = 0; @@ -31,10 +31,7 @@ const indentityFieldsPerType: Record = { service: ['service.name'], }; -const getEntityLatest = ( - entityType: string, - overrides?: Partial -): InventoryEntityLatest => ({ +const getEntityLatest = (entityType: string, overrides?: Partial): EntityLatest => ({ entity: { last_seen_timestamp: generateRandomTimestamp(), type: entityType, @@ -49,7 +46,7 @@ const getEntityLatest = ( ...((overrides ? overrides : {}) as Record), }); -const alertsMock: InventoryEntityLatest[] = [ +const alertsMock: EntityLatest[] = [ getEntityLatest('host', { alertsCount: 1, }), @@ -77,4 +74,4 @@ export const entitiesMock = [ ...hostsMock, ...containersMock, ...servicesMock, -] as InventoryEntityLatest[]; +] as EntityLatest[]; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 8ecb372b2cea..6382c991ab9e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-icons'; import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; +import { castArray } from 'lodash'; import type { InventoryEntityLatest } from '../../../common/entities'; import { isEntityOfType } from '../../../common/utils/entity_type_guards'; @@ -20,7 +21,7 @@ export function EntityIcon({ entity }: EntityIconProps) { const defaultIconSize = euiThemeVars.euiSizeL; if (isEntityOfType('host', entity) || isEntityOfType('container', entity)) { - const cloudProvider = entity.cloud?.provider; + const cloudProvider = castArray(entity.cloud?.provider)[0]; return ( ; + return ; } - if (entity.entity.type.startsWith('kubernetes')) { + if (entity.entityType.startsWith('kubernetes')) { return ; } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index f74303fc02ca..4ff3246c504d 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -15,13 +15,13 @@ jest.mock('./use_kibana'); const useKibanaMock = useKibana as jest.Mock; -const commonEntityFields: Partial = { - last_seen_timestamp: '2023-10-09T00:00:00Z', - id: '1', - display_name: 'entity_name', - definition_id: 'entity_definition_id', - definition_version: '1', - schema_version: '1', +const commonEntityFields: Partial = { + entityLastSeenTimestamp: '2023-10-09T00:00:00Z', + entityId: '1', + entityDisplayName: 'entity_name', + entityDefinitionId: 'entity_definition_id', + entityDefinitionVersion: '1', + entitySchemaVersion: '1', }; describe('useDetailViewRedirect', () => { @@ -55,11 +55,9 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for host entity', () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: 'host', - identity_fields: ['host.name'], - }, + ...(commonEntityFields as InventoryEntityLatest), + entityYype: 'host', + entityIdentityFields: ['host.name'], host: { name: 'host-1', }, @@ -80,11 +78,9 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for container entity', () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: 'container', - identity_fields: ['container.id'], - }, + ...(commonEntityFields as InventoryEntityLatest), + entityType: 'container', + entityIdentityFields: ['container.id'], container: { id: 'container-1', }, @@ -108,11 +104,9 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for service entity', () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: 'service', - identity_fields: ['service.name'], - }, + ...(commonEntityFields as InventoryEntityLatest), + entityType: 'service', + entityIdentityFields: ['service.name'], agent: { name: 'node', }, @@ -147,11 +141,9 @@ describe('useDetailViewRedirect', () => { ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { const entity: InventoryEntityLatest = { - entity: { - ...(commonEntityFields as InventoryEntityLatest['entity']), - type: entityType, - identity_fields: ['some.field'], - }, + ...(commonEntityFields as InventoryEntityLatest), + entityType, + entityIdentityFields: ['some.field'], some: { field: 'some-value', }, diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index a43e7819e30c..b966a8a1fcaf 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -17,6 +17,7 @@ import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; import { isEntityOfType } from '../../common/utils/entity_type_guards'; import type { InventoryEntityLatest } from '../../common/entities'; +import { toEntityLatest } from '../../common/utils/mappers'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { @@ -43,13 +44,17 @@ export const useDetailViewRedirect = () => { const getSingleIdentityFieldValue = useCallback( (latestEntity: InventoryEntityLatest) => { - const identityFields = castArray(latestEntity.entity.identity_fields); + const identityFields = castArray(latestEntity.entityIdentityFields); if (identityFields.length > 1) { - throw new Error(`Multiple identity fields are not supported for ${entity[ENTITY_TYPE]}`); + throw new Error( + `Multiple identity fields are not supported for ${latestEntity.entityType}` + ); } const identityField = identityFields[0]; - return entityManager.entityClient.getIdentityFieldsValue(latestEntity)[identityField]; + return entityManager.entityClient.getIdentityFieldsValue(toEntityLatest(latestEntity))[ + identityField + ]; }, [entityManager.entityClient] ); @@ -61,7 +66,7 @@ export const useDetailViewRedirect = () => { if (isEntityOfType('host', latestEntity) || isEntityOfType('container', latestEntity)) { return assetDetailsLocator?.getRedirectUrl({ assetId: identityValue, - assetType: latestEntity.entity.type, + assetType: latestEntity.entityType, }); } @@ -81,7 +86,7 @@ export const useDetailViewRedirect = () => { const getDashboardRedirectUrl = useCallback( (latestEntity: InventoryEntityLatest) => { - const type = latestEntity.entity.type; + const type = latestEntity.entityType; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; return dashboardId @@ -89,7 +94,7 @@ export const useDetailViewRedirect = () => { dashboardId, query: { language: 'kuery', - query: entityManager.entityClient.asKqlFilter(latestEntity), + query: entityManager.entityClient.asKqlFilter(toEntityLatest(latestEntity)), }, }) : undefined; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index 081545270297..43d82c154304 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -7,7 +7,7 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; +import { ENTITIES_LATEST_ALIAS, EntityLatest } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; export async function getEntityTypes({ @@ -15,7 +15,7 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql<{ entity: { type: string } }>( + const entityTypesEsqlResponse = await inventoryEsClient.esql>( 'get_entity_types', { query: `FROM ${ENTITIES_LATEST_ALIAS} 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 ffe5aa715dcb..9b050736b110 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 @@ -8,15 +8,14 @@ import type { InventoryEntityLatest } from '../../../common/entities'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; -const commonEntityFields: InventoryEntityLatest = { - entity: { - last_seen_timestamp: '2023-10-09T00:00:00Z', - id: '1', - display_name: 'entity_name', - definition_id: 'entity_definition_id', - } as InventoryEntityLatest['entity'], - alertCount: 3, +const commonEntityFields: Partial = { + entityLastSeenTimestamp: '2023-10-09T00:00:00Z', + entityId: '1', + entityDisplayName: 'entity_name', + entityDefinitionId: 'entity_definition_id', + alertsCount: 3, }; + describe('getIdentityFields', () => { it('should return an empty Map when no entities are provided', () => { const result = getIdentityFieldsPerEntityType([]); @@ -24,42 +23,33 @@ describe('getIdentityFields', () => { }); it('should return a Map with unique entity types and their respective identity fields', () => { const serviceEntity: InventoryEntityLatest = { - ...commonEntityFields, + ...(commonEntityFields as InventoryEntityLatest), + entityIdentityFields: ['service.name', 'service.environment'], + entityType: 'service', agent: { name: 'node', }, - entity: { - ...commonEntityFields.entity, - identity_fields: ['service.name', 'service.environment'], - type: 'service', - }, service: { name: 'my-service', }, }; const hostEntity: InventoryEntityLatest = { - ...commonEntityFields, - entity: { - ...commonEntityFields.entity, - identity_fields: ['host.name'], - type: 'host', + ...(commonEntityFields as InventoryEntityLatest), + entityIdentityFields: ['host.name'], + entityType: 'host', + cloud: { + provider: null, }, host: { name: 'my-host', }, - cloud: { - provider: null, - }, }; const containerEntity: InventoryEntityLatest = { - ...commonEntityFields, - entity: { - ...commonEntityFields.entity, - identity_fields: ['container.id'], - type: 'container', - }, + ...(commonEntityFields as InventoryEntityLatest), + entityIdentityFields: ['container.id'], + entityType: 'container', host: { name: 'my-host', }, 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 591c14741227..16b00e5f96dd 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,11 +10,11 @@ import type { InventoryEntityLatest } from '../../../common/entities'; export type IdentityFieldsPerEntityType = Map; -export const getIdentityFieldsPerEntityType = (entities: InventoryEntityLatest[]) => { +export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntityLatest[]) => { const identityFieldsPerEntityType = new Map(); - entities.forEach((entity) => - identityFieldsPerEntityType.set(entity.entity.type, castArray(entity.entity.identity_fields)) + latestEntities.forEach((entity) => + identityFieldsPerEntityType.set(entity.entityType, castArray(entity.entityIdentityFields)) ); 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 4f392650f62c..b51114dcc6ca 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 @@ -7,16 +7,27 @@ import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { kqlQuery } from '@kbn/observability-utils/es/queries/kql_query'; -import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; +import { + ENTITY_LAST_SEEN, + ENTITY_TYPE, + ENTITY_DISPLAY_NAME, +} from '@kbn/observability-shared-plugin/common'; import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, type EntityColumnIds, - type InventoryEntityLatest, + type EntityLatest, + InventoryEntityLatest, } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; +const SORT_FIELDS_TO_ES_FIELDS: Record, string> = { + entityLastSeenTimestamp: ENTITY_LAST_SEEN, + entityDisplayName: ENTITY_DISPLAY_NAME, + entityType: ENTITY_TYPE, +} as const; + export async function getLatestEntities({ inventoryEsClient, sortDirection, @@ -31,7 +42,9 @@ export async function getLatestEntities({ kuery?: string; }): Promise { // 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 entitiesSortField = + SORT_FIELDS_TO_ES_FIELDS[sortField as Exclude] ?? + ENTITY_LAST_SEEN; const from = `FROM ${ENTITIES_LATEST_ALIAS}`; const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()]; @@ -47,7 +60,7 @@ export async function getLatestEntities({ const query = [from, ...where, sort, limit].join(' | '); - const latestEntitiesEsqlResponse = await inventoryEsClient.esql( + const latestEntitiesEsqlResponse = await inventoryEsClient.esql( 'get_latest_entities', { query, @@ -60,5 +73,19 @@ export async function getLatestEntities({ } ); - return latestEntitiesEsqlResponse; + return latestEntitiesEsqlResponse.map((lastestEntity) => { + const { entity, ...metadata } = lastestEntity; + + return { + entityId: entity.id, + entityType: entity.type, + entityDefinitionId: entity.definition_id, + entityDisplayName: entity.display_name, + entityIdentityFields: entity.identity_fields, + entityLastSeenTimestamp: entity.last_seen_timestamp, + entityDefinitionVersion: entity.definition_version, + entitySchemaVersion: entity.schema_version, + ...metadata, + }; + }); } diff --git a/x-pack/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 48100c2f3143..d4575ba2213b 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 @@ -7,7 +7,6 @@ import { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; -import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; 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'; @@ -27,7 +26,7 @@ export async function getLatestEntitiesAlerts({ alertsClient: AlertsClient; kuery?: string; identityFieldsPerEntityType: IdentityFieldsPerEntityType; -}): Promise> { +}): Promise> { if (identityFieldsPerEntityType.size === 0) { return []; } @@ -56,7 +55,7 @@ export async function getLatestEntitiesAlerts({ return buckets.map((bucket: Bucket) => ({ alertsCount: bucket.doc_count, - [ENTITY_TYPE]: entityType, + 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 6b22b66ba4c5..040868472881 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 @@ -91,9 +91,9 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ }); const joined = joinByKey( - [...latestEntities, ...alerts], - [...identityFieldsPerEntityType.values()].flat() as Array - ).filter((latestEntity: InventoryEntityLatest) => latestEntity.entity.id); + [...latestEntities, ...alerts] as InventoryEntityLatest[], + [...identityFieldsPerEntityType.values()].flat() + ).filter((latestEntity) => latestEntity.entityId); return { entities: From cb9654251ec32f4d24f89f5b5093d0076b7923be Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 5 Nov 2024 18:22:38 +0100 Subject: [PATCH 07/27] Clean up --- .../entities_grid/mock/entities_mock.ts | 32 +++++++++---------- .../server/routes/has_data/get_has_data.ts | 15 ++++++--- 2 files changed, 26 insertions(+), 21 deletions(-) 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 6053592c70d1..3ade818f1b0a 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 @@ -6,7 +6,7 @@ */ import { faker } from '@faker-js/faker'; -import type { EntityLatest } from '../../../../common/entities'; +import type { InventoryEntityLatest } from '../../../../common/entities'; const idGenerator = () => { let id = 0; @@ -31,22 +31,22 @@ const indentityFieldsPerType: Record = { service: ['service.name'], }; -const getEntityLatest = (entityType: string, overrides?: Partial): EntityLatest => ({ - entity: { - last_seen_timestamp: generateRandomTimestamp(), - type: entityType, - display_name: faker.person.fullName(), - id: generateId(), - definition_id: faker.string.uuid(), - definition_version: '1.0.0', - identity_fields: indentityFieldsPerType[entityType], - schema_version: '1.0.0', - ...(overrides?.entity ? overrides.entity : undefined), - }, - ...((overrides ? overrides : {}) as Record), +const getEntityLatest = ( + entityType: string, + overrides?: Partial +): InventoryEntityLatest => ({ + entityLastSeenTimestamp: generateRandomTimestamp(), + entityType, + entityDisplayName: faker.person.fullName(), + entityId: generateId(), + entityDefinitionId: faker.string.uuid(), + entityDefinitionVersion: '1.0.0', + entityIdentityFields: indentityFieldsPerType[entityType], + entitySchemaVersion: '1.0.0', + ...overrides, }); -const alertsMock: EntityLatest[] = [ +const alertsMock: InventoryEntityLatest[] = [ getEntityLatest('host', { alertsCount: 1, }), @@ -74,4 +74,4 @@ export const entitiesMock = [ ...hostsMock, ...containersMock, ...servicesMock, -] as EntityLatest[]; +] as InventoryEntityLatest[]; diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts index ce9ca5611f0f..d328a4f3b8d6 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/has_data/get_has_data.ts @@ -16,14 +16,19 @@ export async function getHasData({ inventoryEsClient: ObservabilityElasticsearchClient; logger: Logger; }) { - const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { - query: `FROM ${ENTITIES_LATEST_ALIAS} + try { + const esqlResults = await inventoryEsClient.esql<{ _count: number }>('get_has_data', { + query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS _count = COUNT(*) | LIMIT 1`, - }); + }); - const totalCount = esqlResults[0]._count; + const totalCount = esqlResults[0]._count; - return { hasData: totalCount > 0 }; + return { hasData: totalCount > 0 }; + } catch (e) { + logger.error(e); + return { hasData: false }; + } } From bd2814933de396c9abe5322d07a2f564214148ed Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 5 Nov 2024 20:01:14 +0100 Subject: [PATCH 08/27] Missing change --- .../public/components/entities_grid/entities_grid.stories.tsx | 2 +- .../public/components/search_bar/discover_button.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx index 255b2fd263c5..b5f6f5f56b0d 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx @@ -45,7 +45,7 @@ export const Grid: Story = (args) => { const filteredAndSortedItems = useMemo( () => orderBy( - entityType ? entitiesMock.filter((mock) => mock.entity.type === entityType) : entitiesMock, + entityType ? entitiesMock.filter((mock) => mock.entityType === entityType) : entitiesMock, sort.id, sort.direction ), 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 d5ed5b5af8cf..3f49bd3552d4 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 @@ -10,18 +10,16 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { buildPhrasesFilter, PhrasesFilter } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; - import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, ENTITY_TYPE, } from '@kbn/observability-shared-plugin/common'; -import { EntityColumnIds } from '../../../common/entities'; import { useInventoryParams } from '../../hooks/use_inventory_params'; import { useKibana } from '../../hooks/use_kibana'; -const ACTIVE_COLUMNS: EntityColumnIds[] = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; +const ACTIVE_COLUMNS = [ENTITY_DISPLAY_NAME, ENTITY_TYPE, ENTITY_LAST_SEEN]; export function DiscoverButton({ dataView }: { dataView: DataView }) { const { From 0334bfcb3a0eafe7c94586cd4c102d4c7644bcff Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 5 Nov 2024 22:29:16 +0100 Subject: [PATCH 09/27] Fix tests and zod schema --- .../kbn-entities-schema/src/schema/entity.ts | 11 +++-------- .../routes/entities/get_data_stream_types.test.ts | 4 ++-- .../routes/entities/get_data_stream_types.ts | 8 +++----- .../server/routes/entities/get_latest_entity.ts | 8 +++++--- .../public/hooks/use_detail_view_redirect.test.ts | 15 ++++++++++----- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index 2b1c732a7a01..7a205dcd2baa 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -27,15 +27,10 @@ const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; interface Metadata { - [key: string]: Literal | Literal[] | Metadata; + [key: string]: Metadata | Literal | Literal[]; } - -// Define the recursive schema using z.lazy -export const entityMetadataSchema: z.ZodSchema = z.lazy(() => - z.union([ - z.record(literalSchema), // Object with string keys and values of type string, number, or boolean - entityMetadataSchema, // Recursive reference to SomeType - ]) +export const entityMetadataSchema: z.ZodType = z.lazy(() => + z.record(z.string(), z.union([literalSchema, z.array(literalSchema), entityMetadataSchema])) ); export const entityLatestSchema = z diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts index c66416331e4d..19f7e47e84fc 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.test.ts @@ -74,7 +74,7 @@ describe('getDataStreamTypes', () => { it('should return metrics and entity source_data_stream types when entityCentriExperienceEnabled is true and has entity data', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(true); (getLatestEntity as jest.Mock).mockResolvedValue({ - 'source_data_stream.type': ['logs', 'metrics'], + sourceDataStreamType: ['logs', 'metrics'], }); const params = { @@ -118,7 +118,7 @@ describe('getDataStreamTypes', () => { it('should return entity source_data_stream types when has no metrics', async () => { (getHasMetricsData as jest.Mock).mockResolvedValue(false); (getLatestEntity as jest.Mock).mockResolvedValue({ - 'source_data_stream.type': ['logs', 'traces'], + sourceDataStreamType: ['logs', 'traces'], }); const params = { diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts index afd57a85c109..1bc9368658bc 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts @@ -50,11 +50,9 @@ export async function getDataStreamTypes({ entityManagerClient, }); - if (latestEntity) { - castArray(latestEntity.source_data_stream.type).forEach((item) => { - sourceDataStreams.add(item as EntityDataStreamType); - }); - } + castArray(latestEntity?.sourceDataStreamType).forEach((item) => { + sourceDataStreams.add(item as EntityDataStreamType); + }); return Array.from(sourceDataStreams); } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts index 06d47feb83d2..26a25faac202 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -19,7 +19,7 @@ const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ }); interface LatestEntityResponse { - source_data_stream: { type?: string | string[] }; + sourceDataStreamType?: string | string[]; } export async function getLatestEntity({ @@ -43,7 +43,9 @@ export async function getLatestEntity({ return undefined; } - const response = await inventoryEsClient.esql('get_latest_entities', { + const response = await inventoryEsClient.esql<{ + source_data_stream?: { type?: string | string[] }; + }>('get_latest_entities', { query: `FROM ${ENTITIES_LATEST_ALIAS} | WHERE ${ENTITY_TYPE} == ? | WHERE ${hostOrContainerIdentityField} == ? @@ -52,5 +54,5 @@ export async function getLatestEntity({ params: [entityType, entityId], }); - return response[0]; + return { sourceDataStreamType: response[0].source_data_stream?.type }; } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index 4ff3246c504d..b25d1087757a 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -8,7 +8,12 @@ import { renderHook } from '@testing-library/react-hooks'; import { useDetailViewRedirect } from './use_detail_view_redirect'; import { useKibana } from './use_kibana'; -import { ENTITY_TYPES } from '@kbn/observability-shared-plugin/common'; +import { + CONTAINER_ID, + ENTITY_TYPES, + HOST_NAME, + SERVICE_NAME, +} from '@kbn/observability-shared-plugin/common'; import type { InventoryEntityLatest } from '../../common/entities'; jest.mock('./use_kibana'); @@ -56,7 +61,7 @@ describe('useDetailViewRedirect', () => { it('getEntityRedirectUrl should return the correct URL for host entity', () => { const entity: InventoryEntityLatest = { ...(commonEntityFields as InventoryEntityLatest), - entityYype: 'host', + entityType: 'host', entityIdentityFields: ['host.name'], host: { name: 'host-1', @@ -66,7 +71,7 @@ describe('useDetailViewRedirect', () => { }, }; - mockGetIdentityFieldsValue.mockReturnValue({ host: { name: 'host-1' } }); + mockGetIdentityFieldsValue.mockReturnValue({ [HOST_NAME]: 'host-1' }); mockGetRedirectUrl.mockReturnValue('asset-details-url'); const { result } = renderHook(() => useDetailViewRedirect()); @@ -89,7 +94,7 @@ describe('useDetailViewRedirect', () => { }, }; - mockGetIdentityFieldsValue.mockReturnValue({ container: { id: 'container-1' } }); + mockGetIdentityFieldsValue.mockReturnValue({ [CONTAINER_ID]: 'container-1' }); mockGetRedirectUrl.mockReturnValue('asset-details-url'); const { result } = renderHook(() => useDetailViewRedirect()); @@ -115,7 +120,7 @@ describe('useDetailViewRedirect', () => { environment: 'prod', }, }; - mockGetIdentityFieldsValue.mockReturnValue({ service: { name: 'service-1' } }); + mockGetIdentityFieldsValue.mockReturnValue({ [SERVICE_NAME]: 'service-1' }); mockGetRedirectUrl.mockReturnValue('service-overview-url'); const { result } = renderHook(() => useDetailViewRedirect()); From 8a49c1c38c676329b4b9eadea59b4e5743c49b16 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 6 Nov 2024 10:01:06 +0100 Subject: [PATCH 10/27] Fix infra get_latest_entity --- .../infra/server/routes/entities/get_data_stream_types.ts | 8 +++++--- .../infra/server/routes/entities/get_latest_entity.ts | 5 +---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts index 1bc9368658bc..f9b2d41bbe05 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_data_stream_types.ts @@ -50,9 +50,11 @@ export async function getDataStreamTypes({ entityManagerClient, }); - castArray(latestEntity?.sourceDataStreamType).forEach((item) => { - sourceDataStreams.add(item as EntityDataStreamType); - }); + if (latestEntity) { + castArray(latestEntity.sourceDataStreamType).forEach((item) => { + sourceDataStreams.add(item as EntityDataStreamType); + }); + } return Array.from(sourceDataStreams); } diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts index 26a25faac202..143394d8fbe0 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -7,10 +7,7 @@ import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; import { type EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; -import { - ENTITY_TYPE, - SOURCE_DATA_STREAM_TYPE, -} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; +import { ENTITY_TYPE, SOURCE_DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common'; import type { ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ From be5cc415159d75ddf765a2d1fb6f287485327672 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 6 Nov 2024 11:39:50 +0100 Subject: [PATCH 11/27] Revert get_entity_types change --- .../inventory/server/routes/entities/get_entity_types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index 43d82c154304..7a7b3b019df3 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -20,7 +20,7 @@ export async function getEntityTypes({ { query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} - | STATS BY ${ENTITY_TYPE} + | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE} `, } ); From 8d8df1cefd3c39f9bdc07a7a2a1685f06ef32ae1 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Wed, 6 Nov 2024 17:19:57 +0100 Subject: [PATCH 12/27] Fix cypress tests --- .../tutorial/replace_template_strings.js | 1 - .../transaction_details.cy.ts | 5 +- .../inventory/e2e/cypress/e2e/home.cy.ts | 49 +++++++++++-------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/plugins/home/public/application/components/tutorial/replace_template_strings.js b/src/plugins/home/public/application/components/tutorial/replace_template_strings.js index 75da52e9af2b..09abb7300866 100644 --- a/src/plugins/home/public/application/components/tutorial/replace_template_strings.js +++ b/src/plugins/home/public/application/components/tutorial/replace_template_strings.js @@ -38,7 +38,6 @@ export function replaceTemplateStrings(text, params = {}) { filebeat: docLinks.links.filebeat.base, metricbeat: docLinks.links.metricbeat.base, heartbeat: docLinks.links.heartbeat.base, - functionbeat: docLinks.links.functionbeat.base, winlogbeat: docLinks.links.winlogbeat.base, auditbeat: docLinks.links.auditbeat.base, }, diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts index 0fc1b609b14b..1dd0330edbf4 100644 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts +++ b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/transaction_details/transaction_details.cy.ts @@ -115,7 +115,10 @@ describe('Transaction details', () => { ); cy.contains('Top 5 errors', { timeout: 30000 }); - cy.getByTestSubj('topErrorsForTransactionTable').contains('a', '[MockError] Foo').click(); + cy.getByTestSubj('topErrorsForTransactionTable') + .should('be.visible') + .contains('a', '[MockError] Foo', { timeout: 10000 }) + .click(); cy.url().should('include', 'opbeans-java/errors'); }); diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index 501b6b8078da..332a43e4c6ae 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -166,22 +166,25 @@ describe('Home page', () => { fixture: 'eem_enabled.json', }).as('getEEMStatus'); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); + cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); - cy.getByTestSubj('entityTypesFilterComboBox') - .click() - .getByTestSubj('entityTypesFilterserviceOption') - .click(); + cy.wait('@getEntitiesTypes'); + cy.getByTestSubj('entityTypesFilterComboBox').click(); + cy.getByTestSubj('entityTypesFilterserviceOption') + .should('be.visible') + .click({ force: true }); cy.wait('@getGroups'); cy.contains('service'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click(); + cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist'); + cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist'); + cy.getByTestSubj('inventoryGroup_entity.type_service').should('be.visible'); + cy.getByTestSubj('inventoryGroupTitle_entity.type_service').click({ force: true }); cy.wait('@getEntities'); cy.get('server1').should('not.exist'); cy.contains('synth-node-trace-logs'); cy.contains('foo').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist'); }); it('Filters entities by host type', () => { @@ -189,22 +192,23 @@ describe('Home page', () => { fixture: 'eem_enabled.json', }).as('getEEMStatus'); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); + cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); - cy.getByTestSubj('entityTypesFilterComboBox') - .click() - .getByTestSubj('entityTypesFilterhostOption') - .click(); + cy.wait('@getEntitiesTypes'); + cy.getByTestSubj('entityTypesFilterComboBox').click(); + cy.getByTestSubj('entityTypesFilterhostOption').should('be.visible').click({ force: true }); cy.wait('@getGroups'); cy.contains('host'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click(); + cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist'); + cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist'); + cy.getByTestSubj('inventoryGroup_entity.type_host').should('be.visible'); + cy.getByTestSubj('inventoryGroupTitle_entity.type_host').click({ force: true }); cy.wait('@getEntities'); cy.contains('server1'); cy.contains('synth-node-trace-logs').should('not.exist'); cy.contains('foo').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_container').should('not.exist'); }); it('Filters entities by container type', () => { @@ -212,22 +216,25 @@ describe('Home page', () => { fixture: 'eem_enabled.json', }).as('getEEMStatus'); cy.intercept('GET', '/internal/inventory/entities?**').as('getEntities'); + cy.intercept('GET', '/internal/inventory/entities/types').as('getEntitiesTypes'); cy.intercept('GET', '/internal/inventory/entities/group_by/**').as('getGroups'); cy.visitKibana('/app/inventory'); cy.wait('@getEEMStatus'); - cy.getByTestSubj('entityTypesFilterComboBox') - .click() - .getByTestSubj('entityTypesFiltercontainerOption') - .click(); + cy.wait('@getEntitiesTypes'); + cy.getByTestSubj('entityTypesFilterComboBox').click(); + cy.getByTestSubj('entityTypesFiltercontainerOption') + .should('be.visible') + .click({ force: true }); cy.wait('@getGroups'); cy.contains('container'); - cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click(); + cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist'); + cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist'); + cy.getByTestSubj('inventoryGroup_entity.type_container').should('be.visible'); + cy.getByTestSubj('inventoryGroupTitle_entity.type_container').click({ force: true }); cy.wait('@getEntities'); cy.contains('server1').should('not.exist'); cy.contains('synth-node-trace-logs').should('not.exist'); cy.contains('foo'); - cy.getByTestSubj('inventoryGroup_entity.type_host').should('not.exist'); - cy.getByTestSubj('inventoryGroup_entity.type_service').should('not.exist'); }); }); }); From 29551e26c95a26db6d5d6b225c5c72f21822302b Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 12:30:35 +0100 Subject: [PATCH 13/27] CR fixes --- .../observability_utils/array/join_by_key.ts | 2 +- .../client/create_observability_es_client.ts | 28 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts index ce679f6b1cc3..6a4b036277b4 100644 --- a/x-pack/packages/observability/observability_utils/array/join_by_key.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -21,7 +21,7 @@ export type JoinedReturnType< function getValueByPath(obj: any, path: string): any { return path.split('.').reduce((acc, keyPart) => { // Check if acc is a valid object and has the key - return acc && acc[keyPart] !== undefined ? acc[keyPart] : undefined; + return Object.prototype.hasOwnProperty.call(acc, keyPart) ? acc[keyPart] : undefined; }, obj); } diff --git a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts index efee7766ac1d..b42de8da9460 100644 --- a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts +++ b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts @@ -17,6 +17,13 @@ type SearchRequest = ESSearchRequest & { size: number | boolean; }; +type EsqlQueryColumnarRequest = EsqlQueryRequest & { columnar: true }; +type EsqlQueryRowBasedRequest = EsqlQueryRequest & { columnar?: false }; +export type InferESQLResponseOf< + TDocument = unknown, + TSearchRequest extends EsqlQueryRequest = EsqlQueryRequest +> = TSearchRequest['columnar'] extends true ? ESQLSearchResponse : TDocument[]; + /** * An Elasticsearch Client with a fully typed `search` method and built-in * APM instrumentation. @@ -26,10 +33,14 @@ export interface ObservabilityElasticsearchClient { operationName: string, parameters: TSearchRequest ): Promise>; - esql( + esql( + operationName: string, + parameters: TSearchRequest + ): Promise>; + esql( operationName: string, - parameters: EsqlQueryRequest - ): Promise; + parameters: TSearchRequest + ): Promise>; client: ElasticsearchClient; } @@ -44,7 +55,10 @@ export function createObservabilityEsClient({ }): ObservabilityElasticsearchClient { return { client, - esql(operationName: string, parameters: EsqlQueryRequest) { + esql( + operationName: string, + parameters: EsqlQueryRequest + ) { logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`); return withSpan({ name: operationName, labels: { plugin } }, () => { return client.esql.query( @@ -57,8 +71,12 @@ export function createObservabilityEsClient({ ); }) .then((response) => { + const esqlResponse = response as unknown as ESQLSearchResponse; logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`); - return esqlResultToPlainObjects(response as unknown as ESQLSearchResponse); + + return ( + parameters.columnar ? esqlResponse : esqlResultToPlainObjects(esqlResponse) + ) as InferESQLResponseOf; }) .catch((error) => { throw error; From 595ac73a1d46e2065ad4fc44d7ba398ad5c30b05 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 13:17:59 +0100 Subject: [PATCH 14/27] CR adjustments --- .../kbn-entities-schema/src/schema/entity.ts | 3 ++ .../public/lib/entity_client.ts | 9 ++--- .../inventory/common/entities.ts | 14 ++----- .../common/utils/entity_type_guards.ts | 10 ++--- .../inventory/common/utils/mappers.ts | 5 ++- .../alerts_badge/alerts_badge.test.tsx | 16 ++++---- .../components/alerts_badge/alerts_badge.tsx | 4 +- .../entity_name/entity_name.test.tsx | 4 +- .../entities_grid/entity_name/index.tsx | 4 +- .../public/components/entities_grid/index.tsx | 2 +- .../entities_grid/mock/entities_mock.ts | 10 ++--- .../public/components/entity_icon/index.tsx | 4 +- .../hooks/use_detail_view_redirect.test.ts | 20 +++++----- .../public/hooks/use_detail_view_redirect.ts | 38 ++++++------------- .../routes/entities/get_entity_types.ts | 5 ++- .../entities/get_identify_fields.test.ts | 16 ++++---- .../get_identity_fields_per_entity_type.ts | 4 +- .../routes/entities/get_latest_entities.ts | 14 +++---- .../inventory/server/routes/entities/route.ts | 8 ++-- 19 files changed, 84 insertions(+), 106 deletions(-) diff --git a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts index 7a205dcd2baa..7bfe505face1 100644 --- a/x-pack/packages/kbn-entities-schema/src/schema/entity.ts +++ b/x-pack/packages/kbn-entities-schema/src/schema/entity.ts @@ -42,3 +42,6 @@ export const entityLatestSchema = z ), }) .and(entityMetadataSchema); + +export type EntityInstance = z.infer; +export type EntityMetadata = z.infer; diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index 08794873ba93..c58b09354f9a 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { z } from '@kbn/zod'; import { CoreSetup, CoreStart } from '@kbn/core/public'; import { ClientRequestParamsOf, @@ -14,7 +13,7 @@ import { isHttpFetchError, } from '@kbn/server-route-repository-client'; import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; -import { entityLatestSchema } from '@kbn/entities-schema'; +import type { EntityInstance } from '@kbn/entities-schema'; import { castArray } from 'lodash'; import { DisableManagedEntityResponse, @@ -39,8 +38,6 @@ type CreateEntityDefinitionQuery = QueryParamOf< ClientRequestParamsOf >; -export type EnitityInstance = z.infer; - export class EntityClient { public readonly repositoryClient: EntityManagerRepositoryClient['fetch']; @@ -90,7 +87,7 @@ export class EntityClient { } } - asKqlFilter(entityLatest: EnitityInstance) { + asKqlFilter(entityLatest: EntityInstance) { const identityFieldsValue = this.getIdentityFieldsValue(entityLatest); const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => { @@ -104,7 +101,7 @@ export class EntityClient { return toKqlExpression(kqlExpression); } - getIdentityFieldsValue(entityLatest: EnitityInstance) { + getIdentityFieldsValue(entityLatest: EntityInstance) { const { identity_fields: identityFields } = entityLatest.entity; if (!identityFields) { diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 41e7200d5a7e..8e9b143da2b2 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -4,13 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { z } from '@kbn/zod'; -import { - ENTITY_LATEST, - entitiesAliasPattern, - entityLatestSchema, - entityMetadataSchema, -} from '@kbn/entities-schema'; +import { ENTITY_LATEST, entitiesAliasPattern, type EntityMetadata } from '@kbn/entities-schema'; import { decode, encode } from '@kbn/rison'; import { isRight } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; @@ -105,9 +99,7 @@ export type EntityGroup = { [key: string]: any; }; -export type EntityLatest = z.infer; - -export type InventoryEntityLatest = { +export type InventoryEntity = { entityId: string; entityType: string; entityIdentityFields: string | string[]; @@ -117,4 +109,4 @@ export type InventoryEntityLatest = { entityDefinitionVersion: string; entitySchemaVersion: string; alertsCount?: number; -} & z.infer; +} & EntityMetadata; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts index f15cbd99fa8c..3f82e8dbeb44 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts @@ -6,12 +6,12 @@ */ import type { AgentName } from '@kbn/elastic-agent-utils'; -import type { InventoryEntityLatest } from '../entities'; +import type { InventoryEntity } from '../entities'; interface EntityMap { - host: InventoryEntityLatest & { cloud?: { provider?: string[] } }; - container: InventoryEntityLatest & { cloud?: { provider?: string[] } }; - service: InventoryEntityLatest & { + host: InventoryEntity & { cloud?: { provider?: string[] } }; + container: InventoryEntity & { cloud?: { provider?: string[] } }; + service: InventoryEntity & { agent?: { name: AgentName[] }; service?: { name: string; environment?: string }; }; @@ -19,7 +19,7 @@ interface EntityMap { export const isEntityOfType = ( type: T, - entity: InventoryEntityLatest + entity: InventoryEntity ): entity is EntityMap[T] => { return entity.entityType === type; }; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts b/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts index ae7607341b19..f7eef809725a 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { EntityLatest, InventoryEntityLatest } from '../entities'; +import { EntityInstance } from '@kbn/entities-schema'; +import { InventoryEntity } from '../entities'; -export function toEntityLatest(inventoryEntityLatest: InventoryEntityLatest): EntityLatest { +export function toEntityLatest(inventoryEntityLatest: InventoryEntity): EntityInstance { const { entityDefinitionId, entityDisplayName, 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 index 282fc0d65274..5195a35b93f4 100644 --- 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 @@ -8,12 +8,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { AlertsBadge } from './alerts_badge'; import { useKibana } from '../../hooks/use_kibana'; -import type { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntity } from '../../../common/entities'; jest.mock('../../hooks/use_kibana'); const useKibanaMock = useKibana as jest.Mock; -const commonEntityFields: Partial = { +const commonEntityFields: Partial = { entityLastSeenTimestamp: 'foo', entityId: '1', }; @@ -45,8 +45,8 @@ describe('AlertsBadge', () => { }); it('render alerts badge for a host entity', () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType: 'host', entityDisplayName: 'foo', entityIdentityFields: 'host.name', @@ -68,8 +68,8 @@ describe('AlertsBadge', () => { expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('1'); }); it('render alerts badge for a service entity', () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType: 'service', entityDisplayName: 'foo', entityIdentityFields: 'service.name', @@ -95,8 +95,8 @@ describe('AlertsBadge', () => { expect(screen.queryByTestId('inventoryAlertsBadgeLink')?.textContent).toEqual('5'); }); it('render alerts badge for a service entity with multiple identity fields', () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType: 'service', entityDisplayName: 'foo', entityIdentityFields: ['service.name', 'service.environment'], 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 cb3062c3556b..826a3ea38d2c 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 @@ -8,11 +8,11 @@ import React from 'react'; import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntity } from '../../../common/entities'; import { toEntityLatest } from '../../../common/utils/mappers'; import { useKibana } from '../../hooks/use_kibana'; -export function AlertsBadge({ entity }: { entity: InventoryEntityLatest }) { +export function AlertsBadge({ entity }: { entity: InventoryEntity }) { const { services: { http: { basePath }, diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx index 7658f1239456..29a862646c4c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/entity_name.test.tsx @@ -9,14 +9,14 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { EntityName } from '.'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; -import type { InventoryEntityLatest } from '../../../../common/entities'; +import type { InventoryEntity } from '../../../../common/entities'; jest.mock('../../../hooks/use_detail_view_redirect'); const useDetailViewRedirectMock = useDetailViewRedirect as jest.Mock; describe('EntityName', () => { - const mockEntity: InventoryEntityLatest = { + const mockEntity: InventoryEntity = { entityLastSeenTimestamp: '2023-10-09T00:00:00Z', entityId: '1', entityType: 'service', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 75a2976b067f..6117f6e428bd 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -8,12 +8,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React, { useCallback } from 'react'; import { useKibana } from '../../../hooks/use_kibana'; -import type { InventoryEntityLatest } from '../../../../common/entities'; +import type { InventoryEntity } from '../../../../common/entities'; import { EntityIcon } from '../../entity_icon'; import { useDetailViewRedirect } from '../../../hooks/use_detail_view_redirect'; interface EntityNameProps { - entity: InventoryEntityLatest; + entity: InventoryEntity; } export function EntityName({ entity }: EntityNameProps) { 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 6dab7f340c47..afaf00d4617d 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 @@ -124,7 +124,7 @@ export function EntitiesGrid({ case 'entityDisplayName': return ; default: - return 'a'; + return null; } }, [entities, onFilterByType] 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 3ade818f1b0a..1048b18f82e9 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 @@ -6,7 +6,7 @@ */ import { faker } from '@faker-js/faker'; -import type { InventoryEntityLatest } from '../../../../common/entities'; +import type { InventoryEntity } from '../../../../common/entities'; const idGenerator = () => { let id = 0; @@ -33,8 +33,8 @@ const indentityFieldsPerType: Record = { const getEntityLatest = ( entityType: string, - overrides?: Partial -): InventoryEntityLatest => ({ + overrides?: Partial +): InventoryEntity => ({ entityLastSeenTimestamp: generateRandomTimestamp(), entityType, entityDisplayName: faker.person.fullName(), @@ -46,7 +46,7 @@ const getEntityLatest = ( ...overrides, }); -const alertsMock: InventoryEntityLatest[] = [ +const alertsMock: InventoryEntity[] = [ getEntityLatest('host', { alertsCount: 1, }), @@ -74,4 +74,4 @@ export const entitiesMock = [ ...hostsMock, ...containersMock, ...servicesMock, -] as InventoryEntityLatest[]; +] as InventoryEntity[]; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 6382c991ab9e..8c39c4900efb 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -10,11 +10,11 @@ import { type CloudProvider, CloudProviderIcon, AgentIcon } from '@kbn/custom-ic import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import { castArray } from 'lodash'; -import type { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntity } from '../../../common/entities'; import { isEntityOfType } from '../../../common/utils/entity_type_guards'; interface EntityIconProps { - entity: InventoryEntityLatest; + entity: InventoryEntity; } export function EntityIcon({ entity }: EntityIconProps) { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts index b25d1087757a..233c1a1076b7 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.test.ts @@ -14,13 +14,13 @@ import { HOST_NAME, SERVICE_NAME, } from '@kbn/observability-shared-plugin/common'; -import type { InventoryEntityLatest } from '../../common/entities'; +import type { InventoryEntity } from '../../common/entities'; jest.mock('./use_kibana'); const useKibanaMock = useKibana as jest.Mock; -const commonEntityFields: Partial = { +const commonEntityFields: Partial = { entityLastSeenTimestamp: '2023-10-09T00:00:00Z', entityId: '1', entityDisplayName: 'entity_name', @@ -59,8 +59,8 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for host entity', () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType: 'host', entityIdentityFields: ['host.name'], host: { @@ -82,8 +82,8 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for container entity', () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType: 'container', entityIdentityFields: ['container.id'], container: { @@ -108,8 +108,8 @@ describe('useDetailViewRedirect', () => { }); it('getEntityRedirectUrl should return the correct URL for service entity', () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType: 'service', entityIdentityFields: ['service.name'], agent: { @@ -145,8 +145,8 @@ describe('useDetailViewRedirect', () => { [ENTITY_TYPES.KUBERNETES.STATEFULSET.ecs, 'kubernetes-21694370-bcb2-11ec-b64f-7dd6e8e82013'], ].forEach(([entityType, dashboardId]) => { it(`getEntityRedirectUrl should return the correct URL for ${entityType} entity`, () => { - const entity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const entity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityType, entityIdentityFields: ['some.field'], some: { diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index b966a8a1fcaf..cbd3f62bc29e 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -16,7 +16,7 @@ import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; import { isEntityOfType } from '../../common/utils/entity_type_guards'; -import type { InventoryEntityLatest } from '../../common/entities'; +import type { InventoryEntity } from '../../common/entities'; import { toEntityLatest } from '../../common/utils/mappers'; import { useKibana } from './use_kibana'; @@ -42,50 +42,34 @@ export const useDetailViewRedirect = () => { const dashboardLocator = locators.get(DASHBOARD_APP_LOCATOR); const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); - const getSingleIdentityFieldValue = useCallback( - (latestEntity: InventoryEntityLatest) => { - const identityFields = castArray(latestEntity.entityIdentityFields); - if (identityFields.length > 1) { - throw new Error( - `Multiple identity fields are not supported for ${latestEntity.entityType}` - ); - } - - const identityField = identityFields[0]; - return entityManager.entityClient.getIdentityFieldsValue(toEntityLatest(latestEntity))[ - identityField - ]; - }, - [entityManager.entityClient] - ); - const getDetailViewRedirectUrl = useCallback( - (latestEntity: InventoryEntityLatest) => { - const identityValue = getSingleIdentityFieldValue(latestEntity); + (latestEntity: InventoryEntity) => { + const entityFields = entityManager.entityClient.getIdentityFieldsValue( + toEntityLatest(latestEntity) + ); + const identityFields = castArray(latestEntity.entityIdentityFields); if (isEntityOfType('host', latestEntity) || isEntityOfType('container', latestEntity)) { return assetDetailsLocator?.getRedirectUrl({ - assetId: identityValue, + assetId: entityFields[identityFields[0]], assetType: latestEntity.entityType, }); } if (isEntityOfType('service', latestEntity)) { return serviceOverviewLocator?.getRedirectUrl({ - serviceName: identityValue, - // service.environemnt is not part of entity.identityFields - // we need to manually get its value + serviceName: entityFields[identityFields[0]], environment: latestEntity.service?.environment, }); } return undefined; }, - [assetDetailsLocator, getSingleIdentityFieldValue, serviceOverviewLocator] + [assetDetailsLocator, entityManager.entityClient, serviceOverviewLocator] ); const getDashboardRedirectUrl = useCallback( - (latestEntity: InventoryEntityLatest) => { + (latestEntity: InventoryEntity) => { const type = latestEntity.entityType; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; @@ -103,7 +87,7 @@ export const useDetailViewRedirect = () => { ); const getEntityRedirectUrl = useCallback( - (entity: InventoryEntityLatest) => + (entity: InventoryEntity) => getDetailViewRedirectUrl(entity) ?? getDashboardRedirectUrl(entity), [getDashboardRedirectUrl, getDetailViewRedirectUrl] ); diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index 7a7b3b019df3..a44bc5a0fffa 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -7,7 +7,8 @@ import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { ENTITIES_LATEST_ALIAS, EntityLatest } from '../../../common/entities'; +import type { EntityInstance } from '@kbn/entities-schema'; +import { ENTITIES_LATEST_ALIAS } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; export async function getEntityTypes({ @@ -15,7 +16,7 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql>( + const entityTypesEsqlResponse = await inventoryEsClient.esql>( 'get_entity_types', { query: `FROM ${ENTITIES_LATEST_ALIAS} 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 9b050736b110..8b6b3b109352 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,10 +5,10 @@ * 2.0. */ -import type { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntity } from '../../../common/entities'; import { getIdentityFieldsPerEntityType } from './get_identity_fields_per_entity_type'; -const commonEntityFields: Partial = { +const commonEntityFields: Partial = { entityLastSeenTimestamp: '2023-10-09T00:00:00Z', entityId: '1', entityDisplayName: 'entity_name', @@ -22,8 +22,8 @@ describe('getIdentityFields', () => { expect(result.size).toBe(0); }); it('should return a Map with unique entity types and their respective identity fields', () => { - const serviceEntity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const serviceEntity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityIdentityFields: ['service.name', 'service.environment'], entityType: 'service', agent: { @@ -34,8 +34,8 @@ describe('getIdentityFields', () => { }, }; - const hostEntity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const hostEntity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityIdentityFields: ['host.name'], entityType: 'host', cloud: { @@ -46,8 +46,8 @@ describe('getIdentityFields', () => { }, }; - const containerEntity: InventoryEntityLatest = { - ...(commonEntityFields as InventoryEntityLatest), + const containerEntity: InventoryEntity = { + ...(commonEntityFields as InventoryEntity), entityIdentityFields: ['container.id'], entityType: 'container', host: { 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 16b00e5f96dd..06070b66bad1 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 @@ -6,11 +6,11 @@ */ import { castArray } from 'lodash'; -import type { InventoryEntityLatest } from '../../../common/entities'; +import type { InventoryEntity } from '../../../common/entities'; export type IdentityFieldsPerEntityType = Map; -export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntityLatest[]) => { +export const getIdentityFieldsPerEntityType = (latestEntities: InventoryEntity[]) => { const identityFieldsPerEntityType = new Map(); latestEntities.forEach((entity) => 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 b51114dcc6ca..a977816138db 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,16 +13,17 @@ import { ENTITY_DISPLAY_NAME, } from '@kbn/observability-shared-plugin/common'; import type { ScalarValue } from '@elastic/elasticsearch/lib/api/types'; +import type { EntityInstance } from '@kbn/entities-schema'; import { ENTITIES_LATEST_ALIAS, MAX_NUMBER_OF_ENTITIES, type EntityColumnIds, - type EntityLatest, - InventoryEntityLatest, + InventoryEntity, } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; -const SORT_FIELDS_TO_ES_FIELDS: Record, string> = { +type EntityColumnIdsWithoutAlertsCount = Exclude; +const SORT_FIELDS_TO_ES_FIELDS: Record = { entityLastSeenTimestamp: ENTITY_LAST_SEEN, entityDisplayName: ENTITY_DISPLAY_NAME, entityType: ENTITY_TYPE, @@ -40,11 +41,10 @@ export async function getLatestEntities({ sortField: EntityColumnIds; entityTypes?: string[]; kuery?: string; -}): Promise { +}): Promise { // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. const entitiesSortField = - SORT_FIELDS_TO_ES_FIELDS[sortField as Exclude] ?? - ENTITY_LAST_SEEN; + SORT_FIELDS_TO_ES_FIELDS[sortField as EntityColumnIdsWithoutAlertsCount] ?? ENTITY_LAST_SEEN; const from = `FROM ${ENTITIES_LATEST_ALIAS}`; const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()]; @@ -60,7 +60,7 @@ export async function getLatestEntities({ const query = [from, ...where, sort, limit].join(' | '); - const latestEntitiesEsqlResponse = await inventoryEsClient.esql( + const latestEntitiesEsqlResponse = await inventoryEsClient.esql( 'get_latest_entities', { query, 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 040868472881..0635ead6b850 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 @@ -11,7 +11,7 @@ import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import * as t from 'io-ts'; import { orderBy } from 'lodash'; import { joinByKey } from '@kbn/observability-utils/array/join_by_key'; -import { entityColumnIdsRt, InventoryEntityLatest } from '../../../common/entities'; +import { entityColumnIdsRt, InventoryEntity } from '../../../common/entities'; import { createInventoryServerRoute } from '../create_inventory_server_route'; import { getEntityTypes } from './get_entity_types'; import { getLatestEntities } from './get_latest_entities'; @@ -61,7 +61,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ logger, plugins, request, - }): Promise<{ entities: InventoryEntityLatest[] }> => { + }): Promise<{ entities: InventoryEntity[] }> => { const coreContext = await context.core; const inventoryEsClient = createObservabilityEsClient({ client: coreContext.elasticsearch.client.asCurrentUser, @@ -91,7 +91,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ }); const joined = joinByKey( - [...latestEntities, ...alerts] as InventoryEntityLatest[], + [...latestEntities, ...alerts] as InventoryEntity[], [...identityFieldsPerEntityType.values()].flat() ).filter((latestEntity) => latestEntity.entityId); @@ -100,7 +100,7 @@ export const listLatestEntitiesRoute = createInventoryServerRoute({ sortField === 'alertsCount' ? orderBy( joined, - [(item: InventoryEntityLatest) => item?.alertsCount === undefined, sortField], + [(item: InventoryEntity) => item?.alertsCount === undefined, sortField], ['asc', sortDirection] // push entities without alertsCount to the end ) : joined, From 185696f737b0d3ccb86bf2551b6924cd87e64be4 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 13:23:29 +0100 Subject: [PATCH 15/27] More CR adjustments --- .../inventory/common/utils/entity_type_guards.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts index 3f82e8dbeb44..c2e09e228850 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts @@ -13,7 +13,7 @@ interface EntityMap { container: InventoryEntity & { cloud?: { provider?: string[] } }; service: InventoryEntity & { agent?: { name: AgentName[] }; - service?: { name: string; environment?: string }; + service?: { environment?: string }; }; } From b463ccc90104a96ab59eb3fe5f7d431ba8cad72e Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 13:32:11 +0100 Subject: [PATCH 16/27] More CR adjustments --- .../infra/server/routes/entities/get_latest_entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts index 143394d8fbe0..31e778313f93 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -15,7 +15,7 @@ const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ dataset: ENTITY_LATEST, }); -interface LatestEntityResponse { +interface EntitySourceResponse { sourceDataStreamType?: string | string[]; } @@ -29,7 +29,7 @@ export async function getLatestEntity({ entityType: 'host' | 'container'; entityId: string; entityManagerClient: EntityClient; -}): Promise { +}): Promise { const { definitions } = await entityManagerClient.getEntityDefinitions({ builtIn: true, type: entityType, From 33832dc50e501260b21fb725abae06d942722580 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:42:50 +0000 Subject: [PATCH 17/27] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/observability_solution/inventory/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/inventory/tsconfig.json b/x-pack/plugins/observability_solution/inventory/tsconfig.json index bd77df478cad..5b9786e73eec 100644 --- a/x-pack/plugins/observability_solution/inventory/tsconfig.json +++ b/x-pack/plugins/observability_solution/inventory/tsconfig.json @@ -53,7 +53,6 @@ "@kbn/spaces-plugin", "@kbn/cloud-plugin", "@kbn/storybook", - "@kbn/zod", "@kbn/dashboard-plugin", "@kbn/deeplinks-analytics" ] From 1620988e583c0e4b5b0484fb3e2655d2c7bd23ca Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 14:08:56 +0100 Subject: [PATCH 18/27] Typo --- .../public/lib/entity_client.test.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts index dbaf1205cdf9..6679140314cb 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.test.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { EntityClient, EnitityInstance } from './entity_client'; +import { EntityClient } from './entity_client'; import { coreMock } from '@kbn/core/public/mocks'; +import type { EntityInstance } from '@kbn/entities-schema'; -const commonEntityFields: EnitityInstance = { +const commonEntityFields: EntityInstance = { entity: { last_seen_timestamp: '2023-10-09T00:00:00Z', id: '1', display_name: 'entity_name', definition_id: 'entity_definition_id', - } as EnitityInstance['entity'], + } as EntityInstance['entity'], }; describe('EntityClient', () => { @@ -26,7 +27,7 @@ describe('EntityClient', () => { describe('asKqlFilter', () => { it('should return the kql filter', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -42,7 +43,7 @@ describe('EntityClient', () => { }); it('should return the kql filter when indentity_fields is composed by multiple fields', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -59,7 +60,7 @@ describe('EntityClient', () => { }); it('should ignore fields that are not present in the entity', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['host.name', 'foo.bar'], @@ -76,7 +77,7 @@ describe('EntityClient', () => { describe('getIdentityFieldsValue', () => { it('should return identity fields values', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -93,7 +94,7 @@ describe('EntityClient', () => { }); it('should return identity fields values when indentity_fields is composed by multiple fields', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['service.name', 'service.environment'], @@ -112,7 +113,7 @@ describe('EntityClient', () => { }); it('should return identity fields when field is in the root', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { entity: { ...commonEntityFields.entity, identity_fields: ['name'], @@ -127,7 +128,7 @@ describe('EntityClient', () => { }); it('should throw an error when identity fields are missing', () => { - const entityLatest: EnitityInstance = { + const entityLatest: EntityInstance = { ...commonEntityFields, }; From a186a60c04fd862712cee91847766a2e5c278e6e Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 14:56:54 +0100 Subject: [PATCH 19/27] Fix join_by_key --- .../observability/observability_utils/array/join_by_key.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/packages/observability/observability_utils/array/join_by_key.ts b/x-pack/packages/observability/observability_utils/array/join_by_key.ts index 6a4b036277b4..93ec4261d04d 100644 --- a/x-pack/packages/observability/observability_utils/array/join_by_key.ts +++ b/x-pack/packages/observability/observability_utils/array/join_by_key.ts @@ -21,7 +21,7 @@ export type JoinedReturnType< function getValueByPath(obj: any, path: string): any { return path.split('.').reduce((acc, keyPart) => { // Check if acc is a valid object and has the key - return Object.prototype.hasOwnProperty.call(acc, keyPart) ? acc[keyPart] : undefined; + return acc && Object.prototype.hasOwnProperty.call(acc, keyPart) ? acc[keyPart] : undefined; }, obj); } From b046148c30f0658c1a3beb510189362c634239c2 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 17:10:51 +0100 Subject: [PATCH 20/27] Clean up --- .../public/lib/entity_client.ts | 20 +++++++--- .../inventory/common/utils/mappers.ts | 38 ------------------- .../components/alerts_badge/alerts_badge.tsx | 8 +++- .../public/hooks/use_detail_view_redirect.ts | 32 +++++++++------- 4 files changed, 39 insertions(+), 59 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts diff --git a/x-pack/plugins/entity_manager/public/lib/entity_client.ts b/x-pack/plugins/entity_manager/public/lib/entity_client.ts index c58b09354f9a..7132dc50330d 100644 --- a/x-pack/plugins/entity_manager/public/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/public/lib/entity_client.ts @@ -13,7 +13,7 @@ import { isHttpFetchError, } from '@kbn/server-route-repository-client'; import { type KueryNode, nodeTypes, toKqlExpression } from '@kbn/es-query'; -import type { EntityInstance } from '@kbn/entities-schema'; +import type { EntityInstance, EntityMetadata } from '@kbn/entities-schema'; import { castArray } from 'lodash'; import { DisableManagedEntityResponse, @@ -87,8 +87,12 @@ export class EntityClient { } } - asKqlFilter(entityLatest: EntityInstance) { - const identityFieldsValue = this.getIdentityFieldsValue(entityLatest); + asKqlFilter( + entityInstance: { + entity: Pick; + } & Required + ) { + const identityFieldsValue = this.getIdentityFieldsValue(entityInstance); const nodes: KueryNode[] = Object.entries(identityFieldsValue).map(([identityField, value]) => { return nodeTypes.function.buildNode('is', identityField, value); @@ -101,8 +105,12 @@ export class EntityClient { return toKqlExpression(kqlExpression); } - getIdentityFieldsValue(entityLatest: EntityInstance) { - const { identity_fields: identityFields } = entityLatest.entity; + getIdentityFieldsValue( + entityInstance: { + entity: Pick; + } & Required + ) { + const { identity_fields: identityFields } = entityInstance.entity; if (!identityFields) { throw new Error('Identity fields are missing'); @@ -111,7 +119,7 @@ export class EntityClient { return castArray(identityFields).reduce((acc, field) => { const value = field.split('.').reduce((obj: any, part: string) => { return obj && typeof obj === 'object' ? (obj as Record)[part] : undefined; - }, entityLatest); + }, entityInstance); if (value) { acc[field] = value; diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts b/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts deleted file mode 100644 index f7eef809725a..000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/utils/mappers.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EntityInstance } from '@kbn/entities-schema'; -import { InventoryEntity } from '../entities'; - -export function toEntityLatest(inventoryEntityLatest: InventoryEntity): EntityInstance { - const { - entityDefinitionId, - entityDisplayName, - entityId, - entityIdentityFields, - entityLastSeenTimestamp, - entityType, - entityDefinitionVersion, - entitySchemaVersion, - alertsCount: _, - ...metadata - } = inventoryEntityLatest; - - return { - entity: { - id: entityId, - type: entityType, - definition_id: entityDefinitionId, - display_name: entityDisplayName, - identity_fields: entityIdentityFields, - last_seen_timestamp: entityLastSeenTimestamp, - definition_version: entityDefinitionVersion, - schema_version: entitySchemaVersion, - }, - ...metadata, - }; -} 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 826a3ea38d2c..ed873bdb68c2 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,7 +9,6 @@ import rison from '@kbn/rison'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { InventoryEntity } from '../../../common/entities'; -import { toEntityLatest } from '../../../common/utils/mappers'; import { useKibana } from '../../hooks/use_kibana'; export function AlertsBadge({ entity }: { entity: InventoryEntity }) { @@ -22,7 +21,12 @@ export function AlertsBadge({ entity }: { entity: InventoryEntity }) { const activeAlertsHref = basePath.prepend( `/app/observability/alerts?_a=${rison.encode({ - kuery: entityManager.entityClient.asKqlFilter(toEntityLatest(entity)), + kuery: entityManager.entityClient.asKqlFilter({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }), status: 'active', })}` ); diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index cbd3f62bc29e..b0f6330d4db4 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -17,7 +17,6 @@ import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; import { isEntityOfType } from '../../common/utils/entity_type_guards'; import type { InventoryEntity } from '../../common/entities'; -import { toEntityLatest } from '../../common/utils/mappers'; import { useKibana } from './use_kibana'; const KUBERNETES_DASHBOARDS_IDS: Record = { @@ -43,23 +42,25 @@ export const useDetailViewRedirect = () => { const serviceOverviewLocator = locators.get(SERVICE_OVERVIEW_LOCATOR_ID); const getDetailViewRedirectUrl = useCallback( - (latestEntity: InventoryEntity) => { - const entityFields = entityManager.entityClient.getIdentityFieldsValue( - toEntityLatest(latestEntity) - ); - const identityFields = castArray(latestEntity.entityIdentityFields); + (entity: InventoryEntity) => { + const entityFields = entityManager.entityClient.getIdentityFieldsValue({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + }); + const identityFields = castArray(entity.entityIdentityFields); - if (isEntityOfType('host', latestEntity) || isEntityOfType('container', latestEntity)) { + if (isEntityOfType('host', entity) || isEntityOfType('container', entity)) { return assetDetailsLocator?.getRedirectUrl({ assetId: entityFields[identityFields[0]], - assetType: latestEntity.entityType, + assetType: entity.entityType, }); } - if (isEntityOfType('service', latestEntity)) { + if (isEntityOfType('service', entity)) { return serviceOverviewLocator?.getRedirectUrl({ serviceName: entityFields[identityFields[0]], - environment: latestEntity.service?.environment, + environment: entity.service?.environment, }); } @@ -69,8 +70,8 @@ export const useDetailViewRedirect = () => { ); const getDashboardRedirectUrl = useCallback( - (latestEntity: InventoryEntity) => { - const type = latestEntity.entityType; + (entity: InventoryEntity) => { + const type = entity.entityType; const dashboardId = KUBERNETES_DASHBOARDS_IDS[type]; return dashboardId @@ -78,7 +79,12 @@ export const useDetailViewRedirect = () => { dashboardId, query: { language: 'kuery', - query: entityManager.entityClient.asKqlFilter(toEntityLatest(latestEntity)), + query: entityManager.entityClient.asKqlFilter({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }), }, }) : undefined; From 3ccdac20650e5f67e8bcfaf20bfc32e949f2c421 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 21:25:34 +0100 Subject: [PATCH 21/27] Fix after merge --- .../components/entities_grid/grid_columns.tsx | 8 +++++++- .../public/components/entities_grid/index.tsx | 2 +- .../public/hooks/use_detail_view_redirect.ts | 7 ++++--- .../public/hooks/use_discover_redirect.ts | 15 ++++++++++----- 4 files changed, 22 insertions(+), 10 deletions(-) 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 5ec368d5e503..be5c50eba9c0 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 @@ -65,7 +65,13 @@ const CustomHeaderCell = ({ title, tooltipContent }: { title: string; tooltipCon ); -export const getColumns = ({ showAlertsColumn }: { showAlertsColumn: boolean }) => { +export const getColumns = ({ + showAlertsColumn, + showActions, +}: { + showAlertsColumn: boolean; + showActions: boolean; +}) => { return [ ...(showAlertsColumn ? [ 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 1d21ce78f010..4177eefe7805 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 @@ -135,7 +135,7 @@ export function EntitiesGrid({ discoverUrl && ( ) ); diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index b0f6330d4db4..15d839b5b30b 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -43,23 +43,24 @@ export const useDetailViewRedirect = () => { const getDetailViewRedirectUrl = useCallback( (entity: InventoryEntity) => { - const entityFields = entityManager.entityClient.getIdentityFieldsValue({ + const identityFieldsValue = entityManager.entityClient.getIdentityFieldsValue({ entity: { identity_fields: entity.entityIdentityFields, }, + ...entity, }); const identityFields = castArray(entity.entityIdentityFields); if (isEntityOfType('host', entity) || isEntityOfType('container', entity)) { return assetDetailsLocator?.getRedirectUrl({ - assetId: entityFields[identityFields[0]], + assetId: identityFieldsValue[identityFields[0]], assetType: entity.entityType, }); } if (isEntityOfType('service', entity)) { return serviceOverviewLocator?.getRedirectUrl({ - serviceName: entityFields[identityFields[0]], + serviceName: identityFieldsValue[identityFields[0]], environment: entity.service?.environment, }); } diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts index f2924a133212..a1c85a2e2d2a 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_discover_redirect.ts @@ -13,7 +13,7 @@ import { import { useCallback } from 'react'; import { type PhrasesFilter, buildPhrasesFilter } from '@kbn/es-query'; import type { DataViewField } from '@kbn/data-views-plugin/public'; -import type { Entity } from '../../common/entities'; +import type { InventoryEntity } from '../../common/entities'; import { useKibana } from './use_kibana'; import { useInventoryParams } from './use_inventory_params'; import { useInventorySearchBarContext } from '../context/inventory_search_bar_context_provider'; @@ -33,11 +33,11 @@ export const useDiscoverRedirect = () => { const discoverLocator = share.url.locators.get('DISCOVER_APP_LOCATOR'); const getDiscoverEntitiesRedirectUrl = useCallback( - (entity?: Entity) => { + (entity?: InventoryEntity) => { const filters: PhrasesFilter[] = []; const entityTypeField = (dataView?.getFieldByName(ENTITY_TYPE) ?? - entity?.[ENTITY_TYPE]) as DataViewField; + entity?.entityType) as DataViewField; if (entityTypes && entityTypeField && dataView) { const entityTypeFilter = buildPhrasesFilter(entityTypeField, entityTypes, dataView); @@ -45,7 +45,12 @@ export const useDiscoverRedirect = () => { } const entityKqlFilter = entity - ? entityManager.entityClient.asKqlFilter(unflattenEntity(entity)) + ? entityManager.entityClient.asKqlFilter({ + entity: { + identity_fields: entity.entityIdentityFields, + }, + ...entity, + }) : ''; const kueryWithEntityDefinitionFilters = [ @@ -76,7 +81,7 @@ export const useDiscoverRedirect = () => { ); const getDiscoverRedirectUrl = useCallback( - (entity?: Entity) => getDiscoverEntitiesRedirectUrl(entity), + (entity?: InventoryEntity) => getDiscoverEntitiesRedirectUrl(entity), [getDiscoverEntitiesRedirectUrl] ); From 38ffdc2f36d36613733561a2305703306178841a Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Fri, 8 Nov 2024 21:30:12 +0100 Subject: [PATCH 22/27] Type fix --- .../server/routes/entities/get_latest_entities.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 a977816138db..7f13753f49d0 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 @@ -22,8 +22,11 @@ import { } from '../../../common/entities'; import { getBuiltinEntityDefinitionIdESQLWhereClause } from './query_helper'; -type EntityColumnIdsWithoutAlertsCount = Exclude; -const SORT_FIELDS_TO_ES_FIELDS: Record = { +type EntitySortableColumnIds = Extract< + EntityColumnIds, + 'entityLastSeenTimestamp' | 'entityDisplayName' | 'entityType' +>; +const SORT_FIELDS_TO_ES_FIELDS: Record = { entityLastSeenTimestamp: ENTITY_LAST_SEEN, entityDisplayName: ENTITY_DISPLAY_NAME, entityType: ENTITY_TYPE, @@ -44,7 +47,7 @@ export async function getLatestEntities({ }): Promise { // alertsCount doesn't exist in entities index. Ignore it and sort by entity.lastSeenTimestamp by default. const entitiesSortField = - SORT_FIELDS_TO_ES_FIELDS[sortField as EntityColumnIdsWithoutAlertsCount] ?? ENTITY_LAST_SEEN; + SORT_FIELDS_TO_ES_FIELDS[sortField as EntitySortableColumnIds] ?? ENTITY_LAST_SEEN; const from = `FROM ${ENTITIES_LATEST_ALIAS}`; const where: string[] = [getBuiltinEntityDefinitionIdESQLWhereClause()]; From 423b2a800c513527bb5ef0847e32de7cf55e5d5d Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 11 Nov 2024 10:50:01 +0100 Subject: [PATCH 23/27] Replace API return type with InventoryEntity --- .../inventory/public/components/entities_grid/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 4177eefe7805..163a213096d8 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 @@ -16,8 +16,7 @@ import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react' import { last } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; -import { EntityColumnIds } from '../../../common/entities'; -import { APIReturnType } from '../../api'; +import { EntityColumnIds, InventoryEntity } from '../../../common/entities'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; import { getColumns } from './grid_columns'; import { AlertsBadge } from '../alerts_badge/alerts_badge'; @@ -25,12 +24,9 @@ import { EntityName } from './entity_name'; import { EntityActions } from '../entity_actions'; import { useDiscoverRedirect } from '../../hooks/use_discover_redirect'; -type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; -type InventoryLatestEntities = InventoryEntitiesAPIReturnType['entities']; - interface Props { loading: boolean; - entities: InventoryLatestEntities; + entities: InventoryEntity[]; sortDirection: 'asc' | 'desc'; sortField: string; pageIndex: number; From f087c7d10bc8cf61ce74bcff4e9a195628e61711 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 11 Nov 2024 13:54:05 +0100 Subject: [PATCH 24/27] Rename type guard function --- .../inventory/common/utils/entity_type_guards.ts | 6 +++--- .../inventory/public/components/entity_icon/index.tsx | 6 +++--- .../server/routes/entities/get_entity_types.ts | 11 +++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts index c2e09e228850..dccc888abd8d 100644 --- a/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts +++ b/x-pack/plugins/observability_solution/inventory/common/utils/entity_type_guards.ts @@ -8,7 +8,7 @@ import type { AgentName } from '@kbn/elastic-agent-utils'; import type { InventoryEntity } from '../entities'; -interface EntityMap { +interface BuiltinEntityMap { host: InventoryEntity & { cloud?: { provider?: string[] } }; container: InventoryEntity & { cloud?: { provider?: string[] } }; service: InventoryEntity & { @@ -17,9 +17,9 @@ interface EntityMap { }; } -export const isEntityOfType = ( +export const isBuiltinEntityOfType = ( type: T, entity: InventoryEntity -): entity is EntityMap[T] => { +): entity is BuiltinEntityMap[T] => { return entity.entityType === type; }; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx index 8c39c4900efb..4da8fd3103c4 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entity_icon/index.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; import { castArray } from 'lodash'; import type { InventoryEntity } from '../../../common/entities'; -import { isEntityOfType } from '../../../common/utils/entity_type_guards'; +import { isBuiltinEntityOfType } from '../../../common/utils/entity_type_guards'; interface EntityIconProps { entity: InventoryEntity; @@ -20,7 +20,7 @@ interface EntityIconProps { export function EntityIcon({ entity }: EntityIconProps) { const defaultIconSize = euiThemeVars.euiSizeL; - if (isEntityOfType('host', entity) || isEntityOfType('container', entity)) { + if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) { const cloudProvider = castArray(entity.cloud?.provider)[0]; return ( @@ -41,7 +41,7 @@ export function EntityIcon({ entity }: EntityIconProps) { ); } - if (isEntityOfType('service', entity)) { + if (isBuiltinEntityOfType('service', entity)) { return ; } diff --git a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts index a44bc5a0fffa..99b8829b600b 100644 --- a/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts +++ b/x-pack/plugins/observability_solution/inventory/server/routes/entities/get_entity_types.ts @@ -16,15 +16,14 @@ export async function getEntityTypes({ }: { inventoryEsClient: ObservabilityElasticsearchClient; }) { - const entityTypesEsqlResponse = await inventoryEsClient.esql>( - 'get_entity_types', - { - query: `FROM ${ENTITIES_LATEST_ALIAS} + const entityTypesEsqlResponse = await inventoryEsClient.esql<{ + entity: Pick; + }>('get_entity_types', { + query: `FROM ${ENTITIES_LATEST_ALIAS} | ${getBuiltinEntityDefinitionIdESQLWhereClause()} | STATS count = COUNT(${ENTITY_TYPE}) BY ${ENTITY_TYPE} `, - } - ); + }); return entityTypesEsqlResponse.map((response) => response.entity.type); } From 38c737081a29f3a99da4996ef71cccdf7e37e86c Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 11 Nov 2024 14:03:15 +0100 Subject: [PATCH 25/27] Fix after isEntityOfType was renamed --- .../inventory/public/hooks/use_detail_view_redirect.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts index 15d839b5b30b..4df4fa4ca1f9 100644 --- a/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts +++ b/x-pack/plugins/observability_solution/inventory/public/hooks/use_detail_view_redirect.ts @@ -15,7 +15,7 @@ import { useCallback } from 'react'; import type { DashboardLocatorParams } from '@kbn/dashboard-plugin/public'; import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; import { castArray } from 'lodash'; -import { isEntityOfType } from '../../common/utils/entity_type_guards'; +import { isBuiltinEntityOfType } from '../../common/utils/entity_type_guards'; import type { InventoryEntity } from '../../common/entities'; import { useKibana } from './use_kibana'; @@ -51,14 +51,14 @@ export const useDetailViewRedirect = () => { }); const identityFields = castArray(entity.entityIdentityFields); - if (isEntityOfType('host', entity) || isEntityOfType('container', entity)) { + if (isBuiltinEntityOfType('host', entity) || isBuiltinEntityOfType('container', entity)) { return assetDetailsLocator?.getRedirectUrl({ assetId: identityFieldsValue[identityFields[0]], assetType: entity.entityType, }); } - if (isEntityOfType('service', entity)) { + if (isBuiltinEntityOfType('service', entity)) { return serviceOverviewLocator?.getRedirectUrl({ serviceName: identityFieldsValue[identityFields[0]], environment: entity.service?.environment, From 535568babf9684eb722d5ed050397e0118402f8a Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 12 Nov 2024 13:58:03 +0100 Subject: [PATCH 26/27] Improve esql function type --- .../client/create_observability_es_client.ts | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts index b42de8da9460..09013dcd5a50 100644 --- a/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts +++ b/x-pack/packages/observability/observability_utils/es/client/create_observability_es_client.ts @@ -17,12 +17,19 @@ type SearchRequest = ESSearchRequest & { size: number | boolean; }; -type EsqlQueryColumnarRequest = EsqlQueryRequest & { columnar: true }; -type EsqlQueryRowBasedRequest = EsqlQueryRequest & { columnar?: false }; -export type InferESQLResponseOf< - TDocument = unknown, - TSearchRequest extends EsqlQueryRequest = EsqlQueryRequest -> = TSearchRequest['columnar'] extends true ? ESQLSearchResponse : TDocument[]; +type EsqlQueryParameters = EsqlQueryRequest & { parseOutput?: boolean }; +type EsqlOutputParameters = Omit & { + parseOutput?: true; + format?: 'json'; + columnar?: false; +}; + +type EsqlParameters = EsqlOutputParameters | EsqlQueryParameters; + +export type InferEsqlResponseOf< + TOutput = unknown, + TParameters extends EsqlParameters = EsqlParameters +> = TParameters extends EsqlOutputParameters ? TOutput[] : ESQLSearchResponse; /** * An Elasticsearch Client with a fully typed `search` method and built-in @@ -33,14 +40,14 @@ export interface ObservabilityElasticsearchClient { operationName: string, parameters: TSearchRequest ): Promise>; - esql( + esql( operationName: string, - parameters: TSearchRequest - ): Promise>; - esql( + parameters: TQueryParams + ): Promise>; + esql( operationName: string, - parameters: TSearchRequest - ): Promise>; + parameters: TQueryParams + ): Promise>; client: ElasticsearchClient; } @@ -55,14 +62,14 @@ export function createObservabilityEsClient({ }): ObservabilityElasticsearchClient { return { client, - esql( + esql( operationName: string, - parameters: EsqlQueryRequest + { parseOutput = true, format = 'json', columnar = false, ...parameters }: TSearchRequest ) { logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`); return withSpan({ name: operationName, labels: { plugin } }, () => { return client.esql.query( - { ...parameters }, + { ...parameters, format, columnar }, { querystring: { drop_null_columns: true, @@ -71,12 +78,12 @@ export function createObservabilityEsClient({ ); }) .then((response) => { - const esqlResponse = response as unknown as ESQLSearchResponse; logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`); - return ( - parameters.columnar ? esqlResponse : esqlResultToPlainObjects(esqlResponse) - ) as InferESQLResponseOf; + const esqlResponse = response as unknown as ESQLSearchResponse; + + const shouldParseOutput = parseOutput && !columnar && format === 'json'; + return shouldParseOutput ? esqlResultToPlainObjects(esqlResponse) : esqlResponse; }) .catch((error) => { throw error; From 21dd1a902bfd9e9f59b6064494262755b0722b81 Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Tue, 12 Nov 2024 14:18:38 +0100 Subject: [PATCH 27/27] Fix after merge --- .../components/grouped_inventory/index.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx index b939f0fa5c42..0964b7bb3946 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/grouped_inventory/index.tsx @@ -8,6 +8,7 @@ import { EuiSpacer } from '@elastic/eui'; import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import React from 'react'; import useEffectOnce from 'react-use/lib/useEffectOnce'; +import { flattenObject } from '@kbn/observability-utils/object/flatten_object'; import { useInventoryAbortableAsync } from '../../hooks/use_inventory_abortable_async'; import { useKibana } from '../../hooks/use_kibana'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context'; @@ -52,15 +53,18 @@ export function GroupedInventory() { <> - {value.groups.map((group) => ( - - ))} + {value.groups.map((group) => { + const groupValue = flattenObject(group)[value.groupBy]; + return ( + + ); + })} ); }