From 9561698109fb8382625a58f43ab9e66fd2a1642a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Fri, 27 Sep 2024 17:23:12 +0200 Subject: [PATCH] [Inventory] Entity names redirect on click to respective pages (#193602) ## Summary Adds the ability to click through to the overview pages for entities on the Entity Name cell for the Entity Grid on the new Inventory page. https://github.com/user-attachments/assets/e712d3ef-370f-4353-a398-2365176eb582 Closes #192676 ### How to test - Go to Inventory Page. - Click on an Entity Name. **Expected**: Should redirect to the overview page of that Entity, regardless it is a `host`, `container`, or `service`. --------- Co-authored-by: Elastic Machine --- .../public/components/entities_grid/index.tsx | 55 +++++++++++++++++-- .../public/utils/parse_service_params.test.ts | 39 +++++++++++++ .../public/utils/parse_service_params.ts | 30 ++++++++++ .../locators/apm/service_overview_locator.ts | 4 +- 4 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts create mode 100644 x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.ts diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index dbd1f0806895a..caf83c0976cd6 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 @@ -17,6 +17,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { + ASSET_DETAILS_LOCATOR_ID, + type AssetDetailsLocatorParams, + type ServiceOverviewParams, +} from '@kbn/observability-shared-plugin/common'; + import { last } from 'lodash'; import React, { useCallback, useState } from 'react'; import { EntityType } from '../../../common/entities'; @@ -27,10 +35,14 @@ import { } from '../../../common/es_fields/entities'; import { APIReturnType } from '../../api'; import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; +import { parseServiceParams } from '../../utils/parse_service_params'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; +type LatestEntities = InventoryEntitiesAPIReturnType['entities']; +type LatestEntity = LatestEntities extends Array ? Entity : never; + export type EntityColumnIds = | typeof ENTITY_DISPLAY_NAME | typeof ENTITY_LAST_SEEN @@ -103,7 +115,7 @@ const columns: EuiDataGridColumn[] = [ interface Props { loading: boolean; - entities: InventoryEntitiesAPIReturnType['entities']; + entities: LatestEntities; sortDirection: 'asc' | 'desc'; sortField: string; pageIndex: number; @@ -125,6 +137,13 @@ export function EntitiesGrid({ onFilterByType, }: Props) { const [visibleColumns, setVisibleColumns] = useState(columns.map(({ id }) => id)); + const { services } = useKibana<{ share?: SharePluginStart }>(); + + const assetDetailsLocator = + services.share?.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + + const serviceOverviewLocator = + services.share?.url.locators.get('serviceOverviewLocator'); const onSort: EuiDataGridSorting['onSort'] = useCallback( (newSortingColumns) => { @@ -136,6 +155,31 @@ export function EntitiesGrid({ [onChangeSort] ); + const getEntityRedirectUrl = useCallback( + (entity: LatestEntity) => { + const type = entity[ENTITY_TYPE] as EntityType; + + // Any unrecognised types will always return undefined + switch (type) { + case 'host': + case 'container': + return assetDetailsLocator?.getRedirectUrl({ + assetId: entity[ENTITY_DISPLAY_NAME], + assetType: type, + }); + + case 'service': + // For services, the format of the display name is `service.name:service.environment`. + // We just want the first part of the name for the locator. + // TODO: Replace this with a better approach for handling service names. See https://github.com/elastic/kibana/issues/194131 + return serviceOverviewLocator?.getRedirectUrl( + parseServiceParams(entity[ENTITY_DISPLAY_NAME]) + ); + } + }, + [assetDetailsLocator, serviceOverviewLocator] + ); + const renderCellValue = useCallback( ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { const entity = entities[rowIndex]; @@ -183,8 +227,11 @@ export function EntitiesGrid({ ); case ENTITY_DISPLAY_NAME: return ( - // TODO: link to the appropriate page based on entity type https://github.com/elastic/kibana/issues/192676 - + {entity[columnEntityTableId]} ); @@ -192,7 +239,7 @@ export function EntitiesGrid({ return entity[columnId as EntityColumnIds] || ''; } }, - [entities, onFilterByType] + [entities, onFilterByType, getEntityRedirectUrl] ); if (loading) { diff --git a/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts b/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts new file mode 100644 index 0000000000000..217b28480feb1 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseServiceParams } from './parse_service_params'; + +describe('parseServiceParams', () => { + it('should return only serviceName with a simple name string', () => { + const params = parseServiceParams('service.name'); + + expect(params).toEqual({ serviceName: 'service.name' }); + }); + + it('should return both serviceName and environment with a full name string', () => { + const params = parseServiceParams('service.name:service.environment'); + + expect(params).toEqual({ serviceName: 'service.name', environment: 'service.environment' }); + }); + + it('should ignore multiple colons in the environment portion of the displayName', () => { + const params = parseServiceParams('service.name:synthtrace: service.environment'); + + expect(params).toEqual({ + serviceName: 'service.name', + environment: 'synthtrace: service.environment', + }); + }); + + it('should ignore empty environment names and return only the service.name', () => { + const params = parseServiceParams('service.name:'); + + expect(params).toEqual({ + serviceName: 'service.name', + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.ts b/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.ts new file mode 100644 index 0000000000000..637957c578272 --- /dev/null +++ b/x-pack/plugins/observability_solution/inventory/public/utils/parse_service_params.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ServiceOverviewParams } from '@kbn/observability-shared-plugin/common'; + +/** + * Parses a displayName string with the format `service.name:service.environment`, + * returning a valid `ServiceOverviewParams` object. + * @param displayName A string from a `entity.displayName` field. + * @returns + */ +export const parseServiceParams = (displayName: string): ServiceOverviewParams => { + const separatorIndex = displayName.indexOf(':'); + + const hasEnvironmentName = separatorIndex !== -1; + + const serviceName = hasEnvironmentName ? displayName.slice(0, separatorIndex) : displayName; + // Exclude the separator from the sliced string for the environment name. + // If the string is empty however, then we default to undefined. + const environment = (hasEnvironmentName && displayName.slice(separatorIndex + 1)) || undefined; + + return { + serviceName, + environment, + }; +}; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts index a96d67840ce37..2a4e8aac330ec 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/service_overview_locator.ts @@ -10,6 +10,7 @@ import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public' export interface ServiceOverviewParams extends SerializableRecord { serviceName: string; + environment?: string; rangeFrom?: string; rangeTo?: string; } @@ -23,8 +24,9 @@ export class ServiceOverviewLocatorDefinition implements LocatorDefinition { - const params = { rangeFrom, rangeTo }; + const params = { rangeFrom, rangeTo, environment }; return { app: 'apm', path: `/services/${serviceName}/overview?${qs.stringify(params)}`,