[8.7] [Security Solution] Fix empty fields and tab titles on Alerts page charts (#152402) (#152769)

# 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![image](https://user-images.githubusercontent.com/18648970/222000232-e8681a19-3986-4b7a-a7f1-e92b805ad965.png)\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![image](https://user-images.githubusercontent.com/18648970/222000544-575b33ee-dddd-4e8b-b7f6-8bc2b2c67545.png)\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![image](222000307-764b1e90-ac88-40c7-9f26-a9372e8592a8.mov\r\n\r\n\r\n
\r\n### Checklist\r\n\r\nDelete any items that are not applicable to
this PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
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![image](https://user-images.githubusercontent.com/18648970/222000232-e8681a19-3986-4b7a-a7f1-e92b805ad965.png)\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![image](https://user-images.githubusercontent.com/18648970/222000544-575b33ee-dddd-4e8b-b7f6-8bc2b2c67545.png)\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![image](222000307-764b1e90-ac88-40c7-9f26-a9372e8592a8.mov\r\n\r\n\r\n
\r\n### Checklist\r\n\r\nDelete any items that are not applicable to
this PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
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![image](https://user-images.githubusercontent.com/18648970/222000232-e8681a19-3986-4b7a-a7f1-e92b805ad965.png)\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![image](https://user-images.githubusercontent.com/18648970/222000544-575b33ee-dddd-4e8b-b7f6-8bc2b2c67545.png)\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![image](222000307-764b1e90-ac88-40c7-9f26-a9372e8592a8.mov\r\n\r\n\r\n
\r\n### Checklist\r\n\r\nDelete any items that are not applicable to
this PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
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:
christineweng 2023-03-06 19:27:18 -06:00 committed by GitHub
parent 58ecbf8f34
commit 82fa5fb396
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 289 additions and 143 deletions

View file

@ -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]);

View file

@ -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]);

View file

@ -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());
}
});
});
});

View file

@ -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>
)}
</>
);

View file

@ -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);
});
});

View file

@ -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[] => {

View file

@ -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>
);

View file

@ -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 },
];

View file

@ -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`,
});

View file

@ -13,6 +13,9 @@ export interface AlertsByGroupingAgg {
sum_other_doc_count: number;
buckets: BucketItem[];
};
missingFields: {
doc_count: number;
};
}
export interface AlertsProgressBarData {
key: string;

View file

@ -42,5 +42,8 @@ export const alertsGroupingAggregations = (stackByField: GroupBySelection) => {
size: 10,
},
},
missingFields: {
missing: { field: stackByField },
},
};
};

View file

@ -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>

View file

@ -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);

View file

@ -20,3 +20,9 @@ export type AlertsStackByField =
| 'process.name'
| 'file.name'
| 'hash.sha256';
export interface AddFilterProps {
field: string;
value: string | number | null;
negate?: boolean;
}

View file

@ -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,
});
});

View file

@ -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:

View file

@ -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',
}
);

View file

@ -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}

View file

@ -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 } }),
},
]);
},