mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.7`: - [[Security Solution] Fix empty fields and tab titles on Alerts page charts (#152402)](https://github.com/elastic/kibana/pull/152402) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"christineweng","email":"18648970+christineweng@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-03-06T22:44:57Z","message":"[Security Solution] Fix empty fields and tab titles on Alerts page charts (#152402)\n\n## Summary\r\n\r\nThis PR contains fixes/enhancements on charts section on Alerts Page:\r\n\r\n1. Updated tab names\r\n\r\n\r\n\r\n\r\n \r\n\r\n2. Updated inspect modal titles to match actual tab name (from\r\nhttps://github.com/elastic/kibana/issues/151842)\r\n\r\n- `Counts` (used to be `Aggregations` on alerts page and `Table` in\r\ninspect modal, they are both `Counts` now)\r\n\r\n\r\n\r\n3. Updated `querySkip` in `Trend`, `Counts`, and `Summary` as mentioned\r\non https://github.com/elastic/kibana/issues/150382\r\n- `querySkip` followed the same pattern of `toggleStatus` that each\r\nchart keeps track of its own `querySkip` based on toggle status (skip\r\nquery if charts is collapsed). This is no longer necessary because\r\ntoggle is now managed at a higher level.\r\n \r\n4. Fixed a bug that the top alerts chart was calculating percentages\r\nbased on available fields\r\n- For instance, there are 100 alerts, 20 has `host.name=\"host-1\"`, 30\r\nhas `host.name=\"host-2\"`, the bars will show 40% and 60% for each, and\r\nit adds up to 100%. This does not factor in the 50 alerts with\r\nempty/null fields.\r\n- This PR added an info button that shows the percentage of available\r\nfields, as well as on-click action to add a filter to show alerts with\r\nempty fields\r\n \r\n\r\n, uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"012ec798f7c9b512478b55aec2dc686a37c8347c","branchLabelMapping":{"^v8.8.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Threat Hunting","Team: SecuritySolution","release_note:feature","Team:Threat Hunting:Investigations","v8.7.0","v8.8.0"],"number":152402,"url":"https://github.com/elastic/kibana/pull/152402","mergeCommit":{"message":"[Security Solution] Fix empty fields and tab titles on Alerts page charts (#152402)\n\n## Summary\r\n\r\nThis PR contains fixes/enhancements on charts section on Alerts Page:\r\n\r\n1. Updated tab names\r\n\r\n\r\n\r\n\r\n \r\n\r\n2. Updated inspect modal titles to match actual tab name (from\r\nhttps://github.com/elastic/kibana/issues/151842)\r\n\r\n- `Counts` (used to be `Aggregations` on alerts page and `Table` in\r\ninspect modal, they are both `Counts` now)\r\n\r\n\r\n\r\n3. Updated `querySkip` in `Trend`, `Counts`, and `Summary` as mentioned\r\non https://github.com/elastic/kibana/issues/150382\r\n- `querySkip` followed the same pattern of `toggleStatus` that each\r\nchart keeps track of its own `querySkip` based on toggle status (skip\r\nquery if charts is collapsed). This is no longer necessary because\r\ntoggle is now managed at a higher level.\r\n \r\n4. Fixed a bug that the top alerts chart was calculating percentages\r\nbased on available fields\r\n- For instance, there are 100 alerts, 20 has `host.name=\"host-1\"`, 30\r\nhas `host.name=\"host-2\"`, the bars will show 40% and 60% for each, and\r\nit adds up to 100%. This does not factor in the 50 alerts with\r\nempty/null fields.\r\n- This PR added an info button that shows the percentage of available\r\nfields, as well as on-click action to add a filter to show alerts with\r\nempty fields\r\n \r\n\r\n, uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"012ec798f7c9b512478b55aec2dc686a37c8347c"}},"sourceBranch":"main","suggestedTargetBranches":["8.7"],"targetPullRequestStates":[{"branch":"8.7","label":"v8.7.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.8.0","labelRegex":"^v8.8.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/152402","number":152402,"mergeCommit":{"message":"[Security Solution] Fix empty fields and tab titles on Alerts page charts (#152402)\n\n## Summary\r\n\r\nThis PR contains fixes/enhancements on charts section on Alerts Page:\r\n\r\n1. Updated tab names\r\n\r\n\r\n\r\n\r\n \r\n\r\n2. Updated inspect modal titles to match actual tab name (from\r\nhttps://github.com/elastic/kibana/issues/151842)\r\n\r\n- `Counts` (used to be `Aggregations` on alerts page and `Table` in\r\ninspect modal, they are both `Counts` now)\r\n\r\n\r\n\r\n3. Updated `querySkip` in `Trend`, `Counts`, and `Summary` as mentioned\r\non https://github.com/elastic/kibana/issues/150382\r\n- `querySkip` followed the same pattern of `toggleStatus` that each\r\nchart keeps track of its own `querySkip` based on toggle status (skip\r\nquery if charts is collapsed). This is no longer necessary because\r\ntoggle is now managed at a higher level.\r\n \r\n4. Fixed a bug that the top alerts chart was calculating percentages\r\nbased on available fields\r\n- For instance, there are 100 alerts, 20 has `host.name=\"host-1\"`, 30\r\nhas `host.name=\"host-2\"`, the bars will show 40% and 60% for each, and\r\nit adds up to 100%. This does not factor in the 50 alerts with\r\nempty/null fields.\r\n- This PR added an info button that shows the percentage of available\r\nfields, as well as on-click action to add a filter to show alerts with\r\nempty fields\r\n \r\n\r\n, uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"012ec798f7c9b512478b55aec2dc686a37c8347c"}}]}] BACKPORT-->
This commit is contained in:
parent
58ecbf8f34
commit
82fa5fb396
19 changed files with 289 additions and 143 deletions
|
@ -9,7 +9,7 @@ import { EuiProgress } from '@elastic/eui';
|
|||
import type { EuiComboBox } from '@elastic/eui';
|
||||
import type { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import React, { memo, useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import React, { memo, useMemo, useEffect, useCallback } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
|
@ -108,17 +108,6 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
|
|||
}, [query, filters]);
|
||||
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_ALERTS_COUNT_ID);
|
||||
const [querySkip, setQuerySkip] = useState(
|
||||
isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isAlertsPageChartsEnabled) {
|
||||
setQuerySkip(!isExpanded);
|
||||
} else {
|
||||
setQuerySkip(!toggleStatus);
|
||||
}
|
||||
}, [toggleStatus, isAlertsPageChartsEnabled, isExpanded]);
|
||||
|
||||
const toggleQuery = useCallback(
|
||||
(newToggleStatus: boolean) => {
|
||||
if (isAlertsPageChartsEnabled && setIsExpanded) {
|
||||
|
@ -126,10 +115,13 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
|
|||
} else {
|
||||
setToggleStatus(newToggleStatus);
|
||||
}
|
||||
// toggle on = skipQuery false
|
||||
setQuerySkip(!newToggleStatus);
|
||||
},
|
||||
[setQuerySkip, setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
|
||||
[setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
|
||||
);
|
||||
|
||||
const querySkip = useMemo(
|
||||
() => (isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus),
|
||||
[isAlertsPageChartsEnabled, isExpanded, toggleStatus]
|
||||
);
|
||||
|
||||
const timerange = useMemo(() => ({ from, to }), [from, to]);
|
||||
|
|
|
@ -181,16 +181,6 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
}, [defaultStackByOption, onlyField]);
|
||||
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_HISTOGRAM_ID);
|
||||
const [querySkip, setQuerySkip] = useState(
|
||||
isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isAlertsPageChartsEnabled && isExpanded !== undefined) {
|
||||
setQuerySkip(!isExpanded);
|
||||
} else {
|
||||
setQuerySkip(!toggleStatus);
|
||||
}
|
||||
}, [toggleStatus, isAlertsPageChartsEnabled, isExpanded]);
|
||||
|
||||
const toggleQuery = useCallback(
|
||||
(newToggleStatus: boolean) => {
|
||||
|
@ -199,10 +189,14 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
|
|||
} else {
|
||||
setToggleStatus(newToggleStatus);
|
||||
}
|
||||
// toggle on = skipQuery false
|
||||
setQuerySkip(!newToggleStatus);
|
||||
},
|
||||
[setQuerySkip, setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
|
||||
[setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
|
||||
);
|
||||
|
||||
const querySkip = useMemo(
|
||||
() =>
|
||||
isAlertsPageChartsEnabled && setIsExpanded !== undefined ? !isExpanded : !toggleStatus,
|
||||
[isAlertsPageChartsEnabled, setIsExpanded, isExpanded, toggleStatus]
|
||||
);
|
||||
|
||||
const timerange = useMemo(() => ({ from, to }), [from, to]);
|
||||
|
|
|
@ -64,15 +64,17 @@ describe('Alert by grouping', () => {
|
|||
).not.toBeInTheDocument();
|
||||
|
||||
parsedAlerts.forEach((alert, i) => {
|
||||
expect(
|
||||
container.querySelector(`[data-test-subj="progress-bar-${alert.key}"]`)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector(`[data-test-subj="progress-bar-${alert.key}"]`)?.textContent
|
||||
).toContain(parsedAlerts[i].label);
|
||||
expect(
|
||||
container.querySelector(`[data-test-subj="progress-bar-${alert.key}"]`)?.textContent
|
||||
).toContain(parsedAlerts[i].percentage.toString());
|
||||
if (alert.key !== '-') {
|
||||
expect(
|
||||
container.querySelector(`[data-test-subj="progress-bar-${alert.key}"]`)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector(`[data-test-subj="progress-bar-${alert.key}"]`)?.textContent
|
||||
).toContain(parsedAlerts[i].label);
|
||||
expect(
|
||||
container.querySelector(`[data-test-subj="progress-bar-${alert.key}"]`)?.textContent
|
||||
).toContain(parsedAlerts[i].percentage.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,10 +4,23 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiProgress, EuiSpacer, EuiText, EuiHorizontalRule } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
EuiPopoverTitle,
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { AlertsProgressBarData, GroupBySelection } from './types';
|
||||
import type { AddFilterProps } from '../common/types';
|
||||
import { getNonEmptyPercent } from './helpers';
|
||||
import { DefaultDraggable } from '../../../../common/components/draggables';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -15,80 +28,151 @@ const ProgressWrapper = styled.div`
|
|||
height: 160px;
|
||||
`;
|
||||
|
||||
const StyledEuiText = styled(EuiText)`
|
||||
const StyledEuiHorizontalRule = styled(EuiHorizontalRule)`
|
||||
margin-top: 0;
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-top: -${({ theme }) => theme.eui.euiSizeM};
|
||||
`;
|
||||
|
||||
const StyledEuiProgress = styled(EuiProgress)`
|
||||
margin-top: ${({ theme }) => theme.eui.euiSizeS};
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
const DataStatsWrapper = styled.div`
|
||||
width: 250px;
|
||||
`;
|
||||
export interface AlertsProcessBarProps {
|
||||
data: AlertsProgressBarData[];
|
||||
isLoading: boolean;
|
||||
addFilter?: ({ field, value }: { field: string; value: string | number }) => void;
|
||||
addFilter?: ({ field, value, negate }: AddFilterProps) => void;
|
||||
groupBySelection: GroupBySelection;
|
||||
}
|
||||
|
||||
export const AlertsProgressBar: React.FC<AlertsProcessBarProps> = ({
|
||||
data,
|
||||
isLoading,
|
||||
addFilter,
|
||||
groupBySelection,
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const onButtonClick = () => setIsPopoverOpen(!isPopoverOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
const validPercent = getNonEmptyPercent(data);
|
||||
|
||||
const dataStatsButton = (
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
iconType="iInCircle"
|
||||
aria-label="info"
|
||||
size="xs"
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
);
|
||||
|
||||
const dataStatsMessage = (
|
||||
<DataStatsWrapper>
|
||||
<EuiPopoverTitle>{i18n.DATA_STATISTICS_TITLE(validPercent.toString())}</EuiPopoverTitle>
|
||||
<EuiText size="s">
|
||||
{i18n.DATA_STATISTICS_MESSAGE(groupBySelection)}
|
||||
<EuiLink
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
if (addFilter) {
|
||||
addFilter({ field: groupBySelection, value: null, negate: true });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.NON_EMPTY_FILTER(groupBySelection)}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</DataStatsWrapper>
|
||||
);
|
||||
|
||||
const labelWithHoverActions = (key: string) => {
|
||||
return (
|
||||
<DefaultDraggable
|
||||
isDraggable={false}
|
||||
field={groupBySelection}
|
||||
hideTopN={true}
|
||||
id={`top-alerts-${key}`}
|
||||
value={key}
|
||||
queryValue={key}
|
||||
tooltipContent={null}
|
||||
>
|
||||
<EuiText size="xs" className="eui-textTruncate">
|
||||
{key}
|
||||
</EuiText>
|
||||
</DefaultDraggable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledEuiText size="s" data-test-subj="alerts-progress-bar-title">
|
||||
<h5>{groupBySelection}</h5>
|
||||
</StyledEuiText>
|
||||
<StyledEuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" data-test-subj="alerts-progress-bar-title">
|
||||
<h5>{groupBySelection}</h5>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={dataStatsButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="rightCenter"
|
||||
panelPaddingSize="s"
|
||||
>
|
||||
{dataStatsMessage}
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</StyledEuiFlexGroup>
|
||||
{isLoading ? (
|
||||
<StyledEuiProgress size="xs" color="primary" />
|
||||
) : (
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
)}
|
||||
{!isLoading && data.length === 0 ? (
|
||||
<>
|
||||
<EuiText size="s" textAlign="center" data-test-subj="empty-proress-bar">
|
||||
{i18n.EMPTY_DATA_MESSAGE}
|
||||
</EuiText>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<StyledEuiHorizontalRule />
|
||||
<ProgressWrapper data-test-subj="progress-bar" className="eui-yScroll">
|
||||
{validPercent === 0 ? (
|
||||
<>
|
||||
<EuiText size="s" textAlign="center" data-test-subj="empty-proress-bar">
|
||||
{i18n.EMPTY_DATA_MESSAGE}
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data.map(
|
||||
(item) =>
|
||||
item.key !== '-' && (
|
||||
<div key={`${item.key}`} data-test-subj={`progress-bar-${item.key}`}>
|
||||
<EuiProgress
|
||||
valueText={
|
||||
<EuiText size="xs" color="default">
|
||||
<strong>{`${item.percentage}%`}</strong>
|
||||
</EuiText>
|
||||
}
|
||||
max={100}
|
||||
color={`vis9`}
|
||||
size="s"
|
||||
value={item.percentage}
|
||||
label={
|
||||
item.key === 'Other' ? item.label : labelWithHoverActions(item.key)
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
</ProgressWrapper>
|
||||
</>
|
||||
) : (
|
||||
<ProgressWrapper data-test-subj="progress-bar" className="eui-yScroll">
|
||||
{data.map((item) => (
|
||||
<div key={`${item.key}`} data-test-subj={`progress-bar-${item.key}`}>
|
||||
<EuiProgress
|
||||
valueText={
|
||||
<EuiText size="xs" color="default">
|
||||
<strong>{`${item.percentage}%`}</strong>
|
||||
</EuiText>
|
||||
}
|
||||
max={100}
|
||||
color={`vis9`}
|
||||
size="s"
|
||||
value={item.percentage}
|
||||
label={
|
||||
item.key === 'Other' ? (
|
||||
item.label
|
||||
) : (
|
||||
<DefaultDraggable
|
||||
isDraggable={false}
|
||||
field={groupBySelection}
|
||||
hideTopN={true}
|
||||
id={`top-alerts-${item.key}`}
|
||||
value={item.key}
|
||||
queryValue={item.key}
|
||||
tooltipContent={null}
|
||||
>
|
||||
<EuiText size="xs" className="eui-textTruncate">
|
||||
{item.key}
|
||||
</EuiText>
|
||||
</DefaultDraggable>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</div>
|
||||
))}
|
||||
</ProgressWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { parseAlertsGroupingData } from './helpers';
|
||||
import { parseAlertsGroupingData, getNonEmptyPercent } from './helpers';
|
||||
import * as mock from './mock_data';
|
||||
import type { AlertsByGroupingAgg } from './types';
|
||||
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
|
||||
|
@ -24,3 +24,11 @@ describe('parse progress bar data', () => {
|
|||
expect(res).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test non-empty percentage', () => {
|
||||
test('should return correct non-empty percentage', () => {
|
||||
const expected = Math.round((620 / 630) * 100);
|
||||
const res = getNonEmptyPercent(mock.parsedAlerts);
|
||||
expect(res).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,13 +16,16 @@ export const parseAlertsGroupingData = (
|
|||
response: AlertSearchResponse<{}, AlertsByGroupingAgg>
|
||||
): AlertsProgressBarData[] => {
|
||||
const buckets = response?.aggregations?.alertsByGrouping?.buckets ?? [];
|
||||
if (buckets.length === 0) {
|
||||
const emptyFieldCount = response?.aggregations?.missingFields?.doc_count ?? 0;
|
||||
if (buckets.length === 0 && emptyFieldCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const other = response?.aggregations?.alertsByGrouping?.sum_other_doc_count ?? 0;
|
||||
const total =
|
||||
buckets.reduce((acc: number, group: BucketItem) => acc + group.doc_count, 0) + other;
|
||||
buckets.reduce((acc: number, group: BucketItem) => acc + group.doc_count, 0) +
|
||||
other +
|
||||
emptyFieldCount;
|
||||
|
||||
const topAlerts = buckets.map((group) => {
|
||||
return {
|
||||
|
@ -42,9 +45,34 @@ export const parseAlertsGroupingData = (
|
|||
});
|
||||
}
|
||||
|
||||
if (emptyFieldCount > 0) {
|
||||
topAlerts.push({
|
||||
key: '-',
|
||||
value: emptyFieldCount,
|
||||
percentage: Math.round((emptyFieldCount / total) * 1000) / 10,
|
||||
label: '-',
|
||||
});
|
||||
}
|
||||
|
||||
return topAlerts;
|
||||
};
|
||||
|
||||
export const getNonEmptyPercent = (topAlerts: AlertsProgressBarData[]): number => {
|
||||
const consolidated = topAlerts.reduce(
|
||||
(ret, cur) => {
|
||||
ret.total += cur.value;
|
||||
if (cur.key !== '-') {
|
||||
ret.nonEmpty += cur.value;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
{ total: 0, nonEmpty: 0 }
|
||||
);
|
||||
return consolidated.total > 0
|
||||
? Math.round((consolidated.nonEmpty / consolidated.total) * 100)
|
||||
: 0;
|
||||
};
|
||||
|
||||
export const getIsAlertsProgressBarData = (
|
||||
data: SummaryChartsData[]
|
||||
): data is AlertsProgressBarData[] => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import { alertsGroupingAggregations } from '../alerts_summary_charts_panel/aggre
|
|||
import { getIsAlertsProgressBarData } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
import type { GroupBySelection } from './types';
|
||||
import type { AddFilterProps } from '../common/types';
|
||||
|
||||
const TOP_ALERTS_CHART_ID = 'alerts-summary-top-alerts';
|
||||
const DEFAULT_COMBOBOX_WIDTH = 150;
|
||||
|
@ -31,6 +32,7 @@ interface Props {
|
|||
skip?: boolean;
|
||||
groupBySelection: GroupBySelection;
|
||||
setGroupBySelection: (groupBySelection: GroupBySelection) => void;
|
||||
addFilter?: ({ field, value, negate }: AddFilterProps) => void;
|
||||
}
|
||||
export const AlertsProgressBarPanel: React.FC<Props> = ({
|
||||
filters,
|
||||
|
@ -40,6 +42,7 @@ export const AlertsProgressBarPanel: React.FC<Props> = ({
|
|||
skip,
|
||||
groupBySelection,
|
||||
setGroupBySelection,
|
||||
addFilter,
|
||||
}) => {
|
||||
const uniqueQueryId = useMemo(() => `${TOP_ALERTS_CHART_ID}-${uuid()}`, []);
|
||||
const dropDownOptions = DEFAULT_OPTIONS.map((field) => {
|
||||
|
@ -87,7 +90,12 @@ export const AlertsProgressBarPanel: React.FC<Props> = ({
|
|||
dropDownoptions={dropDownOptions}
|
||||
/>
|
||||
</HeaderSection>
|
||||
<AlertsProgressBar data={data} isLoading={isLoading} groupBySelection={groupBySelection} />
|
||||
<AlertsProgressBar
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
groupBySelection={groupBySelection}
|
||||
addFilter={addFilter}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,7 @@ export const mockAlertsData = {
|
|||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 570,
|
||||
value: 630,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
|
@ -27,7 +27,7 @@ export const mockAlertsData = {
|
|||
aggregations: {
|
||||
alertsByGrouping: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
sum_other_doc_count: 50,
|
||||
buckets: [
|
||||
{
|
||||
key: 'Host-v5biklvcy8',
|
||||
|
@ -43,6 +43,9 @@ export const mockAlertsData = {
|
|||
},
|
||||
],
|
||||
},
|
||||
missingFields: {
|
||||
doc_count: 10,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -69,6 +72,9 @@ export const mockAlertsEmptyData = {
|
|||
sum_other_doc_count: 0,
|
||||
buckets: [],
|
||||
},
|
||||
missingFields: {
|
||||
doc_count: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -89,12 +95,19 @@ export const query = {
|
|||
size: 10,
|
||||
},
|
||||
},
|
||||
missingFields: {
|
||||
missing: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
runtime_mappings: undefined,
|
||||
};
|
||||
|
||||
export const parsedAlerts = [
|
||||
{ key: 'Host-v5biklvcy8', value: 234, label: 'Host-v5biklvcy8', percentage: 41.1 },
|
||||
{ key: 'Host-5y1uprxfv2', value: 186, label: 'Host-5y1uprxfv2', percentage: 32.6 },
|
||||
{ key: 'Host-ssf1mhgy5c', value: 150, label: 'Host-ssf1mhgy5c', percentage: 26.3 },
|
||||
{ key: 'Host-v5biklvcy8', value: 234, label: 'Host-v5biklvcy8', percentage: 37.1 },
|
||||
{ key: 'Host-5y1uprxfv2', value: 186, label: 'Host-5y1uprxfv2', percentage: 29.5 },
|
||||
{ key: 'Host-ssf1mhgy5c', value: 150, label: 'Host-ssf1mhgy5c', percentage: 23.8 },
|
||||
{ key: 'Other', value: 50, label: 'Other', percentage: 7.9 },
|
||||
{ key: '-', value: 10, label: '-', percentage: 1.6 },
|
||||
];
|
||||
|
|
|
@ -51,3 +51,24 @@ export const SOURCE_LABEL = i18n.translate(
|
|||
defaultMessage: 'source',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_STATISTICS_TITLE = (percent: string) =>
|
||||
i18n.translate('xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.dataStatsTitle', {
|
||||
values: { percent },
|
||||
defaultMessage: `This field exists in {percent} of alerts.`,
|
||||
});
|
||||
|
||||
export const DATA_STATISTICS_MESSAGE = (groupbySelection: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.dataStatsMessage',
|
||||
{
|
||||
values: { groupbySelection },
|
||||
defaultMessage: `To see alerts without {groupbySelection} you can filter in by `,
|
||||
}
|
||||
);
|
||||
|
||||
export const NON_EMPTY_FILTER = (groupBySelection: string) =>
|
||||
i18n.translate('xpack.securitySolution.detectionEngine.alerts.alertsByGrouping.nonEmptyFilter', {
|
||||
values: { groupBySelection },
|
||||
defaultMessage: `NOT {groupBySelection}: exists`,
|
||||
});
|
||||
|
|
|
@ -13,6 +13,9 @@ export interface AlertsByGroupingAgg {
|
|||
sum_other_doc_count: number;
|
||||
buckets: BucketItem[];
|
||||
};
|
||||
missingFields: {
|
||||
doc_count: number;
|
||||
};
|
||||
}
|
||||
export interface AlertsProgressBarData {
|
||||
key: string;
|
||||
|
|
|
@ -42,5 +42,8 @@ export const alertsGroupingAggregations = (stackByField: GroupBySelection) => {
|
|||
size: 10,
|
||||
},
|
||||
},
|
||||
missingFields: {
|
||||
missing: { field: stackByField },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import styled from 'styled-components';
|
||||
|
@ -18,6 +18,7 @@ import { AlertsProgressBarPanel } from '../alerts_progress_bar_panel';
|
|||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import type { GroupBySelection } from '../alerts_progress_bar_panel/types';
|
||||
import type { AddFilterProps } from '../common/types';
|
||||
|
||||
const StyledFlexGroup = styled(EuiFlexGroup)`
|
||||
@media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.l});
|
||||
|
@ -32,7 +33,7 @@ const DETECTIONS_ALERTS_CHARTS_ID = 'detections-alerts-charts';
|
|||
interface Props {
|
||||
alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd';
|
||||
filters?: Filter[];
|
||||
addFilter?: ({ field, value }: { field: string; value: string | number }) => void;
|
||||
addFilter?: ({ field, value, negate }: AddFilterProps) => void;
|
||||
panelHeight?: number;
|
||||
query?: Query;
|
||||
signalIndexName: string | null;
|
||||
|
@ -61,17 +62,6 @@ export const AlertsSummaryChartsPanel: React.FC<Props> = ({
|
|||
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
|
||||
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_ALERTS_CHARTS_ID);
|
||||
const [querySkip, setQuerySkip] = useState(
|
||||
isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isAlertsPageChartsEnabled) {
|
||||
setQuerySkip(!isExpanded);
|
||||
} else {
|
||||
setQuerySkip(!toggleStatus);
|
||||
}
|
||||
}, [toggleStatus, isAlertsPageChartsEnabled, isExpanded]);
|
||||
|
||||
const toggleQuery = useCallback(
|
||||
(status: boolean) => {
|
||||
if (isAlertsPageChartsEnabled && setIsExpanded) {
|
||||
|
@ -79,10 +69,13 @@ export const AlertsSummaryChartsPanel: React.FC<Props> = ({
|
|||
} else {
|
||||
setToggleStatus(status);
|
||||
}
|
||||
// toggle on = skipQuery false
|
||||
setQuerySkip(!status);
|
||||
},
|
||||
[setQuerySkip, setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
|
||||
[setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
|
||||
);
|
||||
|
||||
const querySkip = useMemo(
|
||||
() => (isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus),
|
||||
[isAlertsPageChartsEnabled, isExpanded, toggleStatus]
|
||||
);
|
||||
|
||||
const status: boolean = useMemo(() => {
|
||||
|
@ -149,6 +142,7 @@ export const AlertsSummaryChartsPanel: React.FC<Props> = ({
|
|||
skip={querySkip}
|
||||
groupBySelection={groupBySelection}
|
||||
setGroupBySelection={setGroupBySelection}
|
||||
addFilter={addFilter}
|
||||
/>
|
||||
</StyledFlexItem>
|
||||
</StyledFlexGroup>
|
||||
|
|
|
@ -97,7 +97,7 @@ const renderUseSummaryChartData = (props: Partial<UseAlertsQueryProps> = {}) =>
|
|||
}
|
||||
);
|
||||
|
||||
describe('get severity chart data', () => {
|
||||
describe('get summary charts data', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDateNow.mockReturnValue(dateNow);
|
||||
|
|
|
@ -20,3 +20,9 @@ export type AlertsStackByField =
|
|||
| 'process.name'
|
||||
| 'file.name'
|
||||
| 'hash.sha256';
|
||||
|
||||
export interface AddFilterProps {
|
||||
field: string;
|
||||
value: string | number | null;
|
||||
negate?: boolean;
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ describe('helpers', () => {
|
|||
expect(getOptionProperties(TREND_ID)).toEqual({
|
||||
id: TREND_ID,
|
||||
'data-test-subj': `chart-select-${TREND_ID}`,
|
||||
label: i18n.TREND_TITLE,
|
||||
label: i18n.TREND,
|
||||
value: TREND_ID,
|
||||
});
|
||||
});
|
||||
|
@ -115,7 +115,7 @@ describe('helpers', () => {
|
|||
expect(getOptionProperties(TABLE_ID)).toEqual({
|
||||
id: TABLE_ID,
|
||||
'data-test-subj': `chart-select-${TABLE_ID}`,
|
||||
label: i18n.TABLE_TITLE,
|
||||
label: i18n.COUNTS,
|
||||
value: TABLE_ID,
|
||||
});
|
||||
});
|
||||
|
@ -124,7 +124,7 @@ describe('helpers', () => {
|
|||
expect(getOptionProperties(TREEMAP_ID)).toEqual({
|
||||
id: TREEMAP_ID,
|
||||
'data-test-subj': `chart-select-${TREEMAP_ID}`,
|
||||
label: i18n.TREEMAP_TITLE,
|
||||
label: i18n.TREEMAP,
|
||||
value: TREEMAP_ID,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -108,21 +108,21 @@ export const getOptionProperties = (
|
|||
return {
|
||||
id: TABLE_ID,
|
||||
'data-test-subj': `chart-select-${TABLE_ID}`,
|
||||
label: i18n.TABLE_TITLE,
|
||||
label: i18n.COUNTS,
|
||||
value: TABLE_ID,
|
||||
};
|
||||
case TREND_ID:
|
||||
return {
|
||||
id: TREND_ID,
|
||||
'data-test-subj': `chart-select-${TREND_ID}`,
|
||||
label: i18n.TREND_TITLE,
|
||||
label: i18n.TREND,
|
||||
value: TREND_ID,
|
||||
};
|
||||
case TREEMAP_ID:
|
||||
return {
|
||||
id: TREEMAP_ID,
|
||||
'data-test-subj': `chart-select-${TREEMAP_ID}`,
|
||||
label: i18n.TREEMAP_TITLE,
|
||||
label: i18n.TREEMAP,
|
||||
value: TREEMAP_ID,
|
||||
};
|
||||
case CHARTS_ID:
|
||||
|
|
|
@ -33,24 +33,10 @@ export const CHARTS = i18n.translate('xpack.securitySolution.components.chartSel
|
|||
defaultMessage: 'Charts',
|
||||
});
|
||||
|
||||
export const TABLE_TITLE = i18n.translate(
|
||||
export const COUNTS = i18n.translate(
|
||||
'xpack.securitySolution.components.chartSelect.tableOptionTitle',
|
||||
{
|
||||
defaultMessage: 'Aggregations',
|
||||
}
|
||||
);
|
||||
|
||||
export const TREND_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.components.chartSelect.trendOptionTitle',
|
||||
{
|
||||
defaultMessage: 'Trend Analysis',
|
||||
}
|
||||
);
|
||||
|
||||
export const TREEMAP_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.components.chartSelect.treemapOptionTitle',
|
||||
{
|
||||
defaultMessage: 'Multi-dimensional',
|
||||
defaultMessage: 'Counts',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import { AlertsCountPanel } from '../../../components/alerts_kpis/alerts_count_p
|
|||
import { GROUP_BY_LABEL } from '../../../components/alerts_kpis/common/translations';
|
||||
import { RESET_GROUP_BY_FIELDS } from '../../../../common/components/chart_settings_popover/configurations/default/translations';
|
||||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import type { AddFilterProps } from '../../../components/alerts_kpis/common/types';
|
||||
|
||||
const TREND_CHART_HEIGHT = 280; // px
|
||||
const CHART_PANEL_HEIGHT = 375; // px
|
||||
|
@ -47,7 +48,7 @@ const ChartSelectContainer = styled.div`
|
|||
`;
|
||||
|
||||
export interface Props {
|
||||
addFilter: ({ field, value }: { field: string; value: string | number }) => void;
|
||||
addFilter: ({ field, value, negate }: AddFilterProps) => void;
|
||||
alertsDefaultFilters: Filter[];
|
||||
isLoadingIndexPattern: boolean;
|
||||
query: Query;
|
||||
|
@ -252,7 +253,7 @@ const ChartPanelsComponent: React.FC<Props> = ({
|
|||
chartOptionsContextMenu={chartOptionsContextMenu}
|
||||
extraActions={resetGroupByFieldAction}
|
||||
filters={alertsDefaultFilters}
|
||||
inspectTitle={i18n.TABLE}
|
||||
inspectTitle={isAlertsPageChartsEnabled ? i18n.COUNTS : i18n.TABLE}
|
||||
panelHeight={CHART_PANEL_HEIGHT}
|
||||
query={query}
|
||||
runtimeMappings={runtimeMappings}
|
||||
|
|
|
@ -77,6 +77,7 @@ import { DetectionPageFilterSet } from '../../components/detection_page_filters'
|
|||
import type { FilterGroupHandler } from '../../../common/components/filter_group/types';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { AlertsTableFilterGroup } from '../../components/alerts_table/alerts_filter_group';
|
||||
import type { AddFilterProps } from '../../components/alerts_kpis/common/types';
|
||||
/**
|
||||
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
|
||||
*/
|
||||
|
@ -177,15 +178,17 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
}, [isTableLoading, detectionPageFilterHandler]);
|
||||
|
||||
const addFilter = useCallback(
|
||||
({ field, value }: { field: string; value: string | number }) => {
|
||||
({ field, value, negate }: AddFilterProps) => {
|
||||
filterManager.addFilters([
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
negate: false,
|
||||
negate: negate ?? false,
|
||||
},
|
||||
query: { match_phrase: { [field]: value } },
|
||||
...(value != null
|
||||
? { query: { match_phrase: { [field]: value } } }
|
||||
: { exists: { field } }),
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue