Skip to content

Commit

Permalink
[Inventory] Entity names redirect on click to respective pages (#193602)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
Bluefinger and elasticmachine authored Sep 27, 2024
1 parent 0b1e9f4 commit 9561698
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<infer Entity> ? Entity : never;

export type EntityColumnIds =
| typeof ENTITY_DISPLAY_NAME
| typeof ENTITY_LAST_SEEN
Expand Down Expand Up @@ -103,7 +115,7 @@ const columns: EuiDataGridColumn[] = [

interface Props {
loading: boolean;
entities: InventoryEntitiesAPIReturnType['entities'];
entities: LatestEntities;
sortDirection: 'asc' | 'desc';
sortField: string;
pageIndex: number;
Expand All @@ -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<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);

const serviceOverviewLocator =
services.share?.url.locators.get<ServiceOverviewParams>('serviceOverviewLocator');

const onSort: EuiDataGridSorting['onSort'] = useCallback(
(newSortingColumns) => {
Expand All @@ -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];
Expand Down Expand Up @@ -183,16 +227,19 @@ 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
<EuiLink data-test-subj="inventoryCellValueLink" className="eui-textTruncate">
<EuiLink
data-test-subj="inventoryCellValueLink"
className="eui-textTruncate"
href={getEntityRedirectUrl(entity)}
>
{entity[columnEntityTableId]}
</EuiLink>
);
default:
return entity[columnId as EntityColumnIds] || '';
}
},
[entities, onFilterByType]
[entities, onFilterByType, getEntityRedirectUrl]
);

if (loading) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -23,8 +24,9 @@ export class ServiceOverviewLocatorDefinition implements LocatorDefinition<Servi
rangeFrom,
rangeTo,
serviceName,
environment,
}: ServiceOverviewParams) => {
const params = { rangeFrom, rangeTo };
const params = { rangeFrom, rangeTo, environment };
return {
app: 'apm',
path: `/services/${serviceName}/overview?${qs.stringify(params)}`,
Expand Down

0 comments on commit 9561698

Please sign in to comment.