[Infra] Add 'Show in Hosts list' button in for host anomaly drop-down (#191168)

Closes #174856

## Summary

This PR adds new 'Show affected Hosts' button in host anomaly drop-down
in ML anomaly detection fly out. It applies `host.name` filer in hosts
list and the same date range that user selected in ML fly out.

## Testing

1. Go to Inventory or Hosts page 
2. Click ML anomaly detection button at the top
3. Select Anomaly tab from the fly out
4. Change date range to make sure it applies to Hosts page
5. Click three dots button (more actions) and click 'Show affected
Hosts'
6. User is taken to Hosts page, `host.name` filters are applied to
display relevant hosts and the same date range as in ML fly out is
applied

![Screen Recording 2024-08-30 at 12 33
36](https://github.com/user-attachments/assets/f8397d11-8fae-4f27-82ec-5b28da40db14)
This commit is contained in:
Milosz Marcinkowski 2024-09-02 12:29:57 +02:00 committed by GitHub
parent 51a4cf92ea
commit a48701029c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 103 additions and 17 deletions

View file

@ -20,16 +20,14 @@ export const Anomalies = () => {
const { isActiveTab } = useTabSwitcherContext();
const { request$ } = useRequestObservable();
const { getParsedDateRange } = useDatePickerContext();
const { asset, overrides } = useAssetDetailsRenderPropsContext();
const { asset } = useAssetDetailsRenderPropsContext();
const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext);
const { onClose = () => {} } = overrides?.anomalies ?? {};
const parsedDateRange = useMemo(() => getParsedDateRange(), [getParsedDateRange]);
return (
<div ref={ref}>
<AnomaliesTable
closeFlyout={onClose}
hostName={asset.name}
dateRange={parsedDateRange}
hideDatePicker

View file

@ -35,9 +35,6 @@ export interface OverridableTabState {
metadata?: {
showActionsColumn?: boolean;
};
anomalies?: {
onClose?: () => void;
};
alertRule?: {
options?: Partial<Pick<InfraWaffleMapOptions, 'groupBy' | 'metric'>>;
};

View file

@ -27,9 +27,13 @@ import {
} from '@elastic/eui';
import { FormattedMessage, FormattedDate } from '@kbn/i18n-react';
import { useLinkProps, useUiTracker } from '@kbn/observability-shared-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import type { Filter, TimeRange } from '@kbn/es-query';
import { css } from '@emotion/react';
import type { SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common';
import { type HostsLocatorParams, HOSTS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common';
import { HOST_NAME_FIELD } from '../../../../../common/constants';
import { buildCombinedAssetFilter } from '../../../../utils/filters/build';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { FetcherOptions } from '../../../../hooks/use_fetcher';
import { datemathToEpochMillis } from '../../../../utils/datemath';
import { useSorting } from '../../../../hooks/use_sorting';
@ -43,7 +47,7 @@ import type {
import { PaginationControls } from './pagination';
import { AnomalySummary } from './annomaly_summary';
import { AnomalySeverityIndicator } from '../../../logging/log_analysis_results/anomaly_severity_indicator';
import { useSourceContext } from '../../../../containers/metrics_source';
import { useMetricsDataViewContext, useSourceContext } from '../../../../containers/metrics_source';
import { createResultsUrl } from '../flyout_home';
import {
useWaffleViewState,
@ -66,19 +70,28 @@ const AnomalyActionMenu = ({
influencerField,
influencers,
disableShowInInventory,
hostName,
timeRange,
}: {
jobId: string;
type: string;
startTime: number;
closeFlyout: () => void;
closeFlyout?: () => void;
influencerField: string;
influencers: string[];
disableShowInInventory?: boolean;
hostName?: string;
timeRange: { start: string; end: string };
}) => {
const [isOpen, setIsOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const { onViewChange } = useWaffleViewState();
const { metricsView } = useMetricsDataViewContext();
const {
services: { share },
} = useKibanaContextForPlugin();
const hostsLocator = share.url.locators.get<HostsLocatorParams>(HOSTS_LOCATOR_ID);
const showInInventory = useCallback(() => {
const metricTypeMap: { [key in Metric]: SnapshotMetricType } = {
@ -116,7 +129,7 @@ const AnomalyActionMenu = ({
time: startTime,
};
onViewChange({ attributes: anomalyViewParams });
closeFlyout();
if (closeFlyout) closeFlyout();
}, [jobId, onViewChange, startTime, type, influencers, influencerField, closeFlyout]);
const anomaliesUrl = useLinkProps({
@ -125,7 +138,12 @@ const AnomalyActionMenu = ({
});
const items = [
<EuiContextMenuItem key="openInAnomalyExplorer" icon="popout" {...anomaliesUrl}>
<EuiContextMenuItem
key="openInAnomalyExplorer"
icon="popout"
data-test-subj="infraAnomalyFlyoutOpenInAnomalyExplorer"
{...anomaliesUrl}
>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.openInAnomalyExplorer"
defaultMessage="Open in Anomaly Explorer"
@ -134,13 +152,54 @@ const AnomalyActionMenu = ({
];
if (!disableShowInInventory) {
items.push(
<EuiContextMenuItem key="showInInventory" icon="search" onClick={showInInventory}>
const buildFilter = buildCombinedAssetFilter({
field: HOST_NAME_FIELD,
values: influencers,
dataView: metricsView?.dataViewReference,
});
let newFilter: Filter[] = [];
if (!Array.isArray(buildFilter)) {
newFilter = [buildFilter];
}
const showInHostsItem = !hostName ? (
<EuiContextMenuItem
key="showAffectedHosts"
icon="search"
data-test-subj="infraAnomalyFlyoutShowAffectedHosts"
href={hostsLocator?.getRedirectUrl({
dateRange: {
from: timeRange.start,
to: timeRange.end,
},
filters: newFilter,
})}
>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.showInInventory"
defaultMessage="Show in Inventory"
id="xpack.infra.ml.anomalyFlyout.actions.showAffectedHosts"
defaultMessage="Show affected Hosts"
/>
</EuiContextMenuItem>
) : (
<></>
);
items.push(
influencerField === HOST_NAME_FIELD ? (
showInHostsItem
) : (
<EuiContextMenuItem
icon="search"
data-test-subj="infraAnomalyFlyoutShowInInventory"
onClick={showInInventory}
>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.showInInventory"
defaultMessage="Show in Inventory"
/>
</EuiContextMenuItem>
)
);
}
@ -195,7 +254,7 @@ export const NoAnomaliesFound = () => {
);
};
export interface Props {
closeFlyout(): void;
closeFlyout?(): void;
hostName?: string;
dateRange?: TimeRange;
// In case the date picker is managed outside this component
@ -440,6 +499,7 @@ export const AnomaliesTable = ({
textOnly: true,
truncateText: true,
render: (influencers: string[]) => influencers.join(','),
'data-test-subj': 'nodeNameRow',
},
{
name: i18n.translate('xpack.infra.ml.anomalyFlyout.columnActionsName', {
@ -460,6 +520,8 @@ export const AnomaliesTable = ({
influencers={anomaly.influencers}
startTime={anomaly.startTime}
closeFlyout={closeFlyout}
hostName={hostName}
timeRange={timeRange}
/>
);
},

View file

@ -182,4 +182,5 @@ export {
FlamegraphLocatorDefinition,
StacktracesLocatorDefinition,
TopNFunctionsLocatorDefinition,
HOSTS_LOCATOR_ID,
} from './locators';

View file

@ -36,7 +36,7 @@ export interface HostsLocatorParams extends SerializableRecord {
};
}
const HOSTS_LOCATOR_ID = 'HOSTS_LOCATOR';
export const HOSTS_LOCATOR_ID = 'HOSTS_LOCATOR';
const DEFAULT_HOST_LIMIT = 100;
export class HostsLocatorDefinition implements LocatorDefinition<HostsLocatorParams> {

View file

@ -15,6 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['assetDetails', 'common', 'infraHome', 'infraHostsView']);
const infraSourceConfigurationForm = getService('infraSourceConfigurationForm');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
describe('Metrics UI Anomaly Flyout', function () {
before(async () => {
@ -135,6 +136,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const k8sAnomalies = await pageObjects.infraHome.findAnomalies();
expect(k8sAnomalies.length).to.be(3);
});
it("should take users to hosts list when 'Show affected Hosts' is clicked", async () => {
await pageObjects.infraHome.goToInventory();
await pageObjects.infraHome.openAnomalyFlyout();
await pageObjects.infraHome.goToAnomaliesTab();
await pageObjects.infraHome.clickHostsAnomaliesDropdown();
await pageObjects.infraHome.setAnomaliesDate('Apr 21, 2021 @ 00:00:00.000');
const hostName = await pageObjects.infraHome.getAnomalyHostName();
await pageObjects.infraHome.clickShowAffectedHostsButton();
const currentUrl = await browser.getCurrentUrl();
expect(currentUrl).to.contain(
encodeURIComponent(`query:(terms:(host.name:!(${hostName})))`)
);
});
});
});
});

View file

@ -508,5 +508,19 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide
const button = await testSubjects.find('infraModeSwitcherAddMetricButton');
expect(await button.getAttribute('disabled')).to.be('true');
},
async clickAnomalyActionMenuButton() {
await testSubjects.click('infraAnomalyActionMenuButton');
},
async clickShowAffectedHostsButton() {
await this.clickAnomalyActionMenuButton();
await testSubjects.click('infraAnomalyFlyoutShowAffectedHosts');
},
async getAnomalyHostName() {
const element = await testSubjects.find('nodeNameRow');
return await element.getVisibleText();
},
};
}