mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Metrics UI] Add anomalies tab to enhanced node details (#96967)
* 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>
This commit is contained in:
parent
5bb9eecd26
commit
e11ac98b3a
7 changed files with 136 additions and 62 deletions
|
@ -74,6 +74,7 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({
|
|||
}),
|
||||
rt.partial({
|
||||
query: rt.string,
|
||||
hostName: rt.string,
|
||||
metric: metricRT,
|
||||
// Pagination properties
|
||||
pagination: paginationRT,
|
||||
|
|
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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(() => {
|
||||
|
@ -99,10 +98,12 @@ 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 },
|
||||
|
@ -110,7 +111,7 @@ const AnomalyActionMenu = ({
|
|||
};
|
||||
onViewChange(anomalyViewParams);
|
||||
closeFlyout();
|
||||
}, [jobId, onViewChange, startTime, type, partitionFieldName, partitionFieldValue, closeFlyout]);
|
||||
}, [jobId, onViewChange, startTime, type, influencers, influencerField, closeFlyout]);
|
||||
|
||||
const anomaliesUrl = useLinkProps({
|
||||
app: 'ml',
|
||||
|
@ -118,26 +119,25 @@ const AnomalyActionMenu = ({
|
|||
});
|
||||
|
||||
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
|
||||
|
@ -152,12 +152,11 @@ const AnomalyActionMenu = ({
|
|||
})}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen && !isAlertOpen}
|
||||
isOpen={isOpen}
|
||||
closePopover={close}
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
{isAlertOpen && <PrefilledAnomalyAlertFlyout onClose={closeAlert} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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({
|
||||
|
@ -329,7 +340,7 @@ export const AnomaliesTable = (props: Props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const columns: Array<
|
||||
let columns: Array<
|
||||
| EuiTableFieldDataColumnType<MetricsHostsAnomaly>
|
||||
| EuiTableActionsColumnType<MetricsHostsAnomaly>
|
||||
> = [
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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'} />
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -14,6 +14,7 @@ export interface TabProps {
|
|||
currentTime: number;
|
||||
node: InfraWaffleMapNode;
|
||||
nodeType: InventoryItemType;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export const OVERLAY_Y_START = 266;
|
||||
|
|
|
@ -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,
|
||||
|
@ -195,6 +195,7 @@ export const useMetricsHostsAnomaliesResults = ({
|
|||
...paginationOptions,
|
||||
cursor: paginationCursor,
|
||||
},
|
||||
hostName,
|
||||
},
|
||||
services.http.fetch
|
||||
);
|
||||
|
@ -309,6 +310,7 @@ interface RequestArgs {
|
|||
endTime: number;
|
||||
metric?: Metric;
|
||||
query?: string;
|
||||
hostName?: string;
|
||||
sort: Sort;
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -342,6 +345,7 @@ export const callGetMetricHostsAnomaliesAPI = async (
|
|||
metric,
|
||||
sort,
|
||||
pagination,
|
||||
hostName,
|
||||
},
|
||||
})
|
||||
),
|
||||
|
|
|
@ -40,6 +40,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
|
|||
pagination: paginationParam,
|
||||
metric,
|
||||
query,
|
||||
hostName,
|
||||
},
|
||||
} = request.body;
|
||||
|
||||
|
@ -63,6 +64,12 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
|
|||
query,
|
||||
sort,
|
||||
pagination,
|
||||
influencerFilter: hostName
|
||||
? {
|
||||
fieldName: 'host.name',
|
||||
fieldValue: hostName,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue