Skip to content

Commit

Permalink
[Infra][Hosts] Display N/A badge for APM hosts without system metrics (
Browse files Browse the repository at this point in the history
…elastic#191181)

## Summary

Closes elastic#190516

This PR introduces an "N/A" badge for hosts that are returned by the
`metrics/infra/host` API with `hasSystemMetrics` set to `false`. This
enhancement will help users quickly identify and address issues with APM
hosts that are not monitored by the system integration. The badge will
provide an explanation of the problem and suggest troubleshooting steps.

![Screen Recording 2024-08-23 at 11 58
29](https://github.com/user-attachments/assets/e4e72ac5-5244-4863-915d-51b14ebf078a)

**Automated tests**
Upon reviewing the code, I noticed that this screen is covered by
functional tests.

My initial idea was to add some functional tests to verify the badge
display as well as the button and link redirections. However, I found
that this is quite complex, as it involves modifying the data used in
the tests, and I’m still not very familiar with that process.

Given the deadline for this epic, I’ve decided not to include tests in
this issue. If we believe they are really necessary, I would need to
spend some time understanding how data is managed in our functional
testing layer. Any feedback on this would be greatly appreciated.
  • Loading branch information
iblancof authored Aug 27, 2024
1 parent b5570ec commit 1e428ad
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import {
EuiBadge,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPopover,
EuiPopoverFooter,
EuiPopoverTitle,
EuiText,
} from '@elastic/eui';

import {
ObservabilityOnboardingLocatorParams,
OBSERVABILITY_ONBOARDING_LOCATOR,
} from '@kbn/deeplinks-observability';
import { i18n } from '@kbn/i18n';
import { useBoolean } from '@kbn/react-hooks';

import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
import { APM_HOST_TROUBLESHOOTING_LINK } from '../../../../../components/asset_details/constants';

const popoverContent = {
title: i18n.translate('xpack.infra.addDataPopover.wantToSeeMorePopoverTitleLabel', {
defaultMessage: 'Want to see more?',
}),
content: i18n.translate('xpack.infra.addDataPopover.understandHostPerformanceByTextLabel', {
defaultMessage: 'Understand host performance by collecting more metrics.',
}),
button: i18n.translate('xpack.infra.addDataPopover.understandHostPerformanceByTextLabel', {
defaultMessage: 'Add data',
}),
link: i18n.translate('xpack.infra.addDataPopover.troubleshootingLinkLabel', {
defaultMessage: 'Troubleshooting',
}),
};

const badgeContent = i18n.translate('xpack.infra.addDataPopover.naBadgeLabel', {
defaultMessage: 'N/A',
});

export const AddDataTroubleshootingPopover = () => {
const [isPopoverOpen, { off: closePopover, toggle: togglePopover }] = useBoolean(false);

const {
services: { share },
} = useKibanaContextForPlugin();
const addDataLinkHref = share.url.locators
.get<ObservabilityOnboardingLocatorParams>(OBSERVABILITY_ONBOARDING_LOCATOR)
?.getRedirectUrl({ category: 'logs' });

const onButtonClick = () => togglePopover();

return (
<EuiPopover
button={
<EuiBadge
color="hollow"
iconType="iInCircle"
iconSide="left"
onClick={onButtonClick}
onClickAriaLabel={popoverContent.title}
iconOnClick={onButtonClick}
iconOnClickAriaLabel={popoverContent.title}
>
{badgeContent}
</EuiBadge>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
>
<EuiPopoverTitle>{popoverContent.title}</EuiPopoverTitle>
<EuiText size="s" style={{ width: 300 }}>
{popoverContent.content}
</EuiText>
<EuiPopoverFooter>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButton
size="s"
href={addDataLinkHref}
data-test-subj="infraHostsTableWithoutSystemMetricsPopoverAddMoreButton"
>
{popoverContent.button}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiLink
href={APM_HOST_TROUBLESHOOTING_LINK}
target="_blank"
data-test-subj="infraHostsTableWithoutSystemMetricsPopoverTroubleshootingLink"
external
>
{popoverContent.link}
</EuiLink>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverFooter>
</EuiPopover>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { useHostsTable } from './use_hosts_table';
import { type HostNodeRow, useHostsTable } from './use_hosts_table';
import { renderHook } from '@testing-library/react-hooks';
import { InfraAssetMetricsItem } from '../../../../../common/http_api';
import * as useUnifiedSearchHooks from './use_unified_search';
Expand Down Expand Up @@ -158,7 +158,7 @@ describe('useHostTable hook', () => {
} as unknown as ReturnType<typeof useKibanaContextForPluginHook.useKibanaContextForPlugin>);
});
it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => {
const expected = [
const expected: Array<Partial<HostNodeRow>> = [
{
name: 'host-0',
os: '-',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ColumnHeader } from '../components/table/column_header';
import { TABLE_COLUMN_LABEL, TABLE_CONTENT_LABEL } from '../translations';
import { METRICS_TOOLTIP } from '../../../../common/visualizations';
import { buildCombinedAssetFilter } from '../../../../utils/filters/build';
import { AddDataTroubleshootingPopover } from '../components/table/add_data_troubleshooting_popover';

/**
* Columns and items types
Expand All @@ -64,10 +65,31 @@ export type HostNodeRow = HostMetadata &
* Helper functions
*/
const formatMetric = (type: InfraAssetMetricType, value: number | undefined | null) => {
return value || value === 0 ? createInventoryMetricFormatter({ type })(value) : 'N/A';
const defaultValue = value ?? 0;
return createInventoryMetricFormatter({ type })(defaultValue);
};

const buildMetricCell = (
value: number | null,
formatType: InfraAssetMetricType,
hasSystemMetrics?: boolean
) => {
if (!hasSystemMetrics && value === null) {
return <AddDataTroubleshootingPopover />;
}

return formatMetric(formatType, value);
};

const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => {
nodes.map((node) => {
if (node.name === 'instance') {
node.metrics[0].value = 0;
node.hasSystemMetrics = true;
}
return node;
});

return nodes.map(({ metrics, metadata, name, alertsCount, hasSystemMetrics }) => {
const metadataKeyValue = metadata.reduce(
(acc, curr) => ({
Expand All @@ -89,7 +111,7 @@ const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => {
...metrics.reduce(
(acc, curr) => ({
...acc,
[curr.name]: curr.value ?? 0,
[curr.name]: curr.value,
}),
{} as HostMetrics
),
Expand All @@ -104,12 +126,15 @@ const isTitleColumn = (cell: HostNodeRow[keyof HostNodeRow]): cell is HostNodeRo
};

const sortValues = (aValue: any, bValue: any, { direction }: Sorting) => {
if (typeof aValue === 'string' && typeof bValue === 'string') {
return direction === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue);
const a = aValue ?? -1;
const b = bValue ?? -1;

if (typeof a === 'string' && typeof b === 'string') {
return direction === 'desc' ? b.localeCompare(a) : a.localeCompare(b);
}

if (isNumber(aValue) && isNumber(bValue)) {
return direction === 'desc' ? bValue - aValue : aValue - bValue;
if (isNumber(a) && isNumber(b)) {
return direction === 'desc' ? b - a : a - b;
}

return 1;
Expand Down Expand Up @@ -358,7 +383,8 @@ export const useHostsTable = () => {
field: 'cpuV2',
sortable: true,
'data-test-subj': 'hostsView-tableRow-cpuUsage',
render: (avg: number) => formatMetric('cpuV2', avg),
render: (avg: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(avg, 'cpuV2', hasSystemMetrics),
align: 'right',
},
{
Expand All @@ -373,7 +399,8 @@ export const useHostsTable = () => {
field: 'normalizedLoad1m',
sortable: true,
'data-test-subj': 'hostsView-tableRow-normalizedLoad1m',
render: (avg: number) => formatMetric('normalizedLoad1m', avg),
render: (avg: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(avg, 'normalizedLoad1m', hasSystemMetrics),
align: 'right',
},
{
Expand All @@ -388,7 +415,8 @@ export const useHostsTable = () => {
field: 'memory',
sortable: true,
'data-test-subj': 'hostsView-tableRow-memoryUsage',
render: (avg: number) => formatMetric('memory', avg),
render: (avg: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(avg, 'memory', hasSystemMetrics),
align: 'right',
},
{
Expand All @@ -403,7 +431,8 @@ export const useHostsTable = () => {
field: 'memoryFree',
sortable: true,
'data-test-subj': 'hostsView-tableRow-memoryFree',
render: (avg: number) => formatMetric('memoryFree', avg),
render: (avg: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(avg, 'memoryFree', hasSystemMetrics),
align: 'right',
},
{
Expand All @@ -418,7 +447,8 @@ export const useHostsTable = () => {
field: 'diskSpaceUsage',
sortable: true,
'data-test-subj': 'hostsView-tableRow-diskSpaceUsage',
render: (max: number) => formatMetric('diskSpaceUsage', max),
render: (max: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(max, 'diskSpaceUsage', hasSystemMetrics),
align: 'right',
},
{
Expand All @@ -433,7 +463,8 @@ export const useHostsTable = () => {
field: 'rxV2',
sortable: true,
'data-test-subj': 'hostsView-tableRow-rx',
render: (avg: number) => formatMetric('rx', avg),
render: (avg: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(avg, 'rx', hasSystemMetrics),
align: 'right',
},
{
Expand All @@ -448,7 +479,8 @@ export const useHostsTable = () => {
field: 'txV2',
sortable: true,
'data-test-subj': 'hostsView-tableRow-tx',
render: (avg: number) => formatMetric('tx', avg),
render: (avg: number, { hasSystemMetrics }: HostNodeRow) =>
buildMetricCell(avg, 'tx', hasSystemMetrics),
align: 'right',
},
],
Expand Down

0 comments on commit 1e428ad

Please sign in to comment.