Skip to content

Commit

Permalink
[Metrics UI] Add anomalies tab to enhanced node details (#96967)
Browse files Browse the repository at this point in the history
* Adapt the anomalies table to work in overlay

* Wire up the onClose function

* Make "show in inventory" filter waffle map

* Remove unused variable

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
phillipb and kibanamachine committed Apr 15, 2021
1 parent 5bb9eec commit e11ac98
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({
}),
rt.partial({
query: rt.string,
hostName: rt.string,
metric: metricRT,
// Pagination properties
pagination: paginationRT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { FormattedDate, FormattedMessage } from 'react-intl';
import { datemathToEpochMillis } from '../../../../../../../utils/datemath';
import { SnapshotMetricType } from '../../../../../../../../common/inventory_models/types';
import { withTheme } from '../../../../../../../../../../../src/plugins/kibana_react/common';
import { PrefilledAnomalyAlertFlyout } from '../../../../../../../alerting/metric_anomaly/components/alert_flyout';
import { useLinkProps } from '../../../../../../../hooks/use_link_props';
import { useSorting } from '../../../../../../../hooks/use_sorting';
import { useMetricsK8sAnomaliesResults } from '../../../../hooks/use_metrics_k8s_anomalies';
Expand All @@ -46,6 +45,7 @@ import { AnomalySeverityIndicator } from '../../../../../../../components/loggin
import { useSourceContext } from '../../../../../../../containers/metrics_source';
import { createResultsUrl } from '../flyout_home';
import { useWaffleViewState, WaffleViewState } from '../../../../hooks/use_waffle_view_state';
import { useUiTracker } from '../../../../../../../../../observability/public';
type JobType = 'k8s' | 'hosts';
type SortField = 'anomalyScore' | 'startTime';
interface JobOption {
Expand All @@ -57,22 +57,21 @@ const AnomalyActionMenu = ({
type,
startTime,
closeFlyout,
partitionFieldName,
partitionFieldValue,
influencerField,
influencers,
disableShowInInventory,
}: {
jobId: string;
type: string;
startTime: number;
closeFlyout: () => void;
partitionFieldName?: string;
partitionFieldValue?: string;
influencerField: string;
influencers: string[];
disableShowInInventory?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const [isAlertOpen, setIsAlertOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const openAlert = useCallback(() => setIsAlertOpen(true), [setIsAlertOpen]);
const closeAlert = useCallback(() => setIsAlertOpen(false), [setIsAlertOpen]);
const { onViewChange } = useWaffleViewState();

const showInInventory = useCallback(() => {
Expand All @@ -99,45 +98,46 @@ const AnomalyActionMenu = ({
region: '',
autoReload: false,
filterQuery: {
expression:
partitionFieldName && partitionFieldValue
? `${partitionFieldName}: "${partitionFieldValue}"`
: ``,
expression: influencers.reduce((query, i) => {
if (query) {
query = `${query} or `;
}
return `${query} ${influencerField}: "${i}"`;
}, ''),
kind: 'kuery',
},
legend: { palette: 'cool', reverseColors: false, steps: 10 },
time: startTime,
};
onViewChange(anomalyViewParams);
closeFlyout();
}, [jobId, onViewChange, startTime, type, partitionFieldName, partitionFieldValue, closeFlyout]);
}, [jobId, onViewChange, startTime, type, influencers, influencerField, closeFlyout]);

const anomaliesUrl = useLinkProps({
app: 'ml',
pathname: `/explorer?_g=${createResultsUrl([jobId.toString()])}`,
});

const items = [
<EuiContextMenuItem key="showInInventory" icon="search" onClick={showInInventory}>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.showInInventory"
defaultMessage="Show in Inventory"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem key="openInAnomalyExplorer" icon="popout" {...anomaliesUrl}>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.openInAnomalyExplorer"
defaultMessage="Open in Anomaly Explorer"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem key="createAlert" icon="bell" onClick={openAlert}>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.createAlert"
defaultMessage="Create Alert"
/>
</EuiContextMenuItem>,
];

if (!disableShowInInventory) {
items.push(
<EuiContextMenuItem key="showInInventory" icon="search" onClick={showInInventory}>
<FormattedMessage
id="xpack.infra.ml.anomalyFlyout.actions.showInInventory"
defaultMessage="Show in Inventory"
/>
</EuiContextMenuItem>
);
}

return (
<>
<EuiPopover
Expand All @@ -152,12 +152,11 @@ const AnomalyActionMenu = ({
})}
/>
}
isOpen={isOpen && !isAlertOpen}
isOpen={isOpen}
closePopover={close}
>
<EuiContextMenuPanel items={items} />
</EuiPopover>
{isAlertOpen && <PrefilledAnomalyAlertFlyout onClose={closeAlert} />}
</>
);
};
Expand All @@ -184,12 +183,14 @@ export const NoAnomaliesFound = withTheme(({ theme }) => (
));
interface Props {
closeFlyout(): void;
hostName?: string;
}
export const AnomaliesTable = (props: Props) => {
const { closeFlyout } = props;
const { closeFlyout, hostName } = props;
const [search, setSearch] = useState('');
const [start, setStart] = useState('now-30d');
const [end, setEnd] = useState('now');
const trackMetric = useUiTracker({ app: 'infra_metrics' });
const [timeRange, setTimeRange] = useState<{ start: number; end: number }>({
start: datemathToEpochMillis(start) || 0,
end: datemathToEpochMillis(end, 'up') || 0,
Expand Down Expand Up @@ -321,6 +322,16 @@ export const AnomaliesTable = (props: Props) => {
[hostChangeSort, k8sChangeSort, jobType]
);

useEffect(() => {
if (results) {
results.forEach((r) => {
if (r.influencers.length > 100) {
trackMetric({ metric: 'metrics_ml_anomaly_detection_more_than_100_influencers' });
}
});
}
}, [results, trackMetric]);

const onTableChange = (criteria: Criteria<MetricsHostsAnomaly>) => {
setSorting(criteria.sort);
changeSortOptions({
Expand All @@ -329,7 +340,7 @@ export const AnomaliesTable = (props: Props) => {
});
};

const columns: Array<
let columns: Array<
| EuiTableFieldDataColumnType<MetricsHostsAnomaly>
| EuiTableActionsColumnType<MetricsHostsAnomaly>
> = [
Expand Down Expand Up @@ -394,8 +405,11 @@ export const AnomaliesTable = (props: Props) => {
<AnomalyActionMenu
jobId={anomaly.jobId}
type={anomaly.type}
partitionFieldName={anomaly.partitionFieldName}
partitionFieldValue={anomaly.partitionFieldValue}
influencerField={
anomaly.type === 'metrics_hosts' ? 'host.name' : 'kubernetes.pod.uid'
}
disableShowInInventory={anomaly.influencers.length > 100}
influencers={anomaly.influencers}
startTime={anomaly.startTime}
closeFlyout={closeFlyout}
/>
Expand All @@ -406,11 +420,20 @@ export const AnomaliesTable = (props: Props) => {
},
];

columns = hostName
? columns.filter((c) => {
if ('field' in c) {
return c.field !== 'influencers';
}
return true;
})
: columns;

useEffect(() => {
if (getAnomalies) {
getAnomalies(undefined, search);
getAnomalies(undefined, search, hostName);
}
}, [getAnomalies, search]);
}, [getAnomalies, search, hostName]);

return (
<div>
Expand All @@ -425,31 +448,33 @@ export const AnomaliesTable = (props: Props) => {
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={3}>
<EuiFieldSearch
fullWidth
placeholder={i18n.translate('xpack.infra.ml.anomalyFlyout.searchPlaceholder', {
defaultMessage: 'Search',
})}
value={search}
onChange={onSearchChange}
isClearable={true}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiComboBox
placeholder={i18n.translate('xpack.infra.ml.anomalyFlyout.jobTypeSelect', {
defaultMessage: 'Select group',
})}
singleSelection={{ asPlainText: true }}
options={jobOptions}
selectedOptions={selectedJobType}
onChange={changeJobType}
isClearable={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
{!hostName && (
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={3}>
<EuiFieldSearch
fullWidth
placeholder={i18n.translate('xpack.infra.ml.anomalyFlyout.searchPlaceholder', {
defaultMessage: 'Search',
})}
value={search}
onChange={onSearchChange}
isClearable={true}
/>
</EuiFlexItem>
<EuiFlexItem grow={1}>
<EuiComboBox
placeholder={i18n.translate('xpack.infra.ml.anomalyFlyout.jobTypeSelect', {
defaultMessage: 'Select group',
})}
singleSelection={{ asPlainText: true }}
options={jobOptions}
selectedOptions={selectedJobType}
onChange={changeJobType}
isClearable={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}

<EuiSpacer size={'m'} />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { MetricsTab } from './tabs/metrics/metrics';
import { LogsTab } from './tabs/logs';
import { ProcessesTab } from './tabs/processes';
import { PropertiesTab } from './tabs/properties/index';
import { AnomaliesTab } from './tabs/anomalies/anomalies';
import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN } from './tabs/shared';
import { useLinkProps } from '../../../../../hooks/use_link_props';
import { getNodeDetailUrl } from '../../../../link_to';
Expand All @@ -44,7 +45,7 @@ export const NodeContextPopover = ({
openAlertFlyout,
}: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab];
const tabConfigs = [MetricsTab, LogsTab, ProcessesTab, PropertiesTab, AnomaliesTab];
const inventoryModel = findInventoryModel(nodeType);
const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000;
const uiCapabilities = useKibana().services.application?.capabilities;
Expand All @@ -58,11 +59,17 @@ export const NodeContextPopover = ({
return {
...m,
content: (
<TabContent node={node} nodeType={nodeType} currentTime={currentTime} options={options} />
<TabContent
onClose={onClose}
node={node}
nodeType={nodeType}
currentTime={currentTime}
options={options}
/>
),
};
});
}, [tabConfigs, node, nodeType, currentTime, options]);
}, [tabConfigs, node, nodeType, currentTime, onClose, options]);

const [selectedTab, setSelectedTab] = useState(0);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React from 'react';
import { AnomaliesTable } from '../../../ml/anomaly_detection/anomalies_table/anomalies_table';
import { TabContent, TabProps } from '../shared';

const TabComponent = (props: TabProps) => {
const { node, onClose } = props;

return (
<TabContent>
<AnomaliesTable closeFlyout={onClose} hostName={node.name} />
</TabContent>
);
};

export const AnomaliesTab = {
id: 'anomalies',
name: i18n.translate('xpack.infra.nodeDetails.tabs.anomalies', {
defaultMessage: 'Anomalies',
}),
content: TabComponent,
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface TabProps {
currentTime: number;
node: InfraWaffleMapNode;
nodeType: InventoryItemType;
onClose(): void;
}

export const OVERLAY_Y_START = 266;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const useMetricsHostsAnomaliesResults = ({
const [getMetricsHostsAnomaliesRequest, getMetricsHostsAnomalies] = useTrackedPromise(
{
cancelPreviousOn: 'creation',
createPromise: async (metric?: Metric, query?: string) => {
createPromise: async (metric?: Metric, query?: string, hostName?: string) => {
const {
timeRange: { start: queryStartTime, end: queryEndTime },
sortOptions,
Expand All @@ -195,6 +195,7 @@ export const useMetricsHostsAnomaliesResults = ({
...paginationOptions,
cursor: paginationCursor,
},
hostName,
},
services.http.fetch
);
Expand Down Expand Up @@ -309,6 +310,7 @@ interface RequestArgs {
endTime: number;
metric?: Metric;
query?: string;
hostName?: string;
sort: Sort;
pagination: Pagination;
}
Expand All @@ -326,6 +328,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
sort,
pagination,
query,
hostName,
} = requestArgs;
const response = await fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, {
method: 'POST',
Expand All @@ -342,6 +345,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
metric,
sort,
pagination,
hostName,
},
})
),
Expand Down
Loading

0 comments on commit e11ac98

Please sign in to comment.