Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce hook to rediect to alerts page from within security solution and implement in Detection Response dashboard #152714

Merged
Expand Up @@ -111,6 +111,7 @@ describe('DonutChart', () => {
outerSizeRatio: 1,
},
},
onElementClick: expect.any(Function),
});
});

Expand Down
Expand Up @@ -7,7 +7,7 @@

import type { EuiFlexGroupProps } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui';
import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';

import type { Datum, NodeColorAccessor, PartialTheme, ElementClickListener } from '@elastic/charts';
import {
Expand All @@ -17,8 +17,10 @@ import {
PartitionLayout,
defaultPartitionValueFormatter,
} from '@elastic/charts';
import { isEmpty } from 'lodash';
import type { FlattenSimpleInterpolation } from 'styled-components';
import styled from 'styled-components';

import { useTheme } from './common';
import { DraggableLegend } from './draggable_legend';
import type { LegendItem } from './draggable_legend_item';
Expand Down Expand Up @@ -48,7 +50,11 @@ export interface DonutChartProps {
height?: number;
label: React.ReactElement | string;
legendItems?: LegendItem[] | null | undefined;
onElementClick?: ElementClickListener;
/**
* provides the section name of a clicked donut ring partition
*/
onDonutPartitionClicked?: (level: string) => void;
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
onLabelClicked?: (label: string) => void;
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
title: React.ReactElement | string | number | null;
totalCount: number | null | undefined;
}
Expand Down Expand Up @@ -105,6 +111,7 @@ const DonutChartWrapperComponent: React.FC<DonutChartWrapperProps> = ({
[euiTheme.colors.disabled]
);
const className = isChartEmbeddablesEnabled ? undefined : 'eui-textTruncate';

return (
<EuiFlexGroup
alignItems="center"
Expand Down Expand Up @@ -152,12 +159,31 @@ export const DonutChart = ({
height = 90,
label,
legendItems,
onElementClick,
onDonutPartitionClicked,
title,
totalCount,
}: DonutChartProps) => {
const theme = useTheme();

const onElementClicked: ElementClickListener = useCallback(
(event) => {
if (onDonutPartitionClicked) {
const flattened = event.flat(2);
const level =
flattened.length > 0 &&
'groupByRollup' in flattened[0] &&
flattened[0].groupByRollup != null
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
? `${flattened[0].groupByRollup}`
: '';

if (!isEmpty(level.trim())) {
onDonutPartitionClicked(level.toLowerCase());
}
}
},
[onDonutPartitionClicked]
);

return (
<DonutChartWrapper
dataExists={data != null && data.length > 0}
Expand All @@ -170,7 +196,7 @@ export const DonutChart = ({
<DonutChartEmpty size={height} />
) : (
<Chart size={height}>
<Settings theme={donutTheme} baseTheme={theme} onElementClick={onElementClick} />
<Settings theme={donutTheme} baseTheme={theme} onElementClick={onElementClicked} />
<Partition
id="donut-chart"
data={data}
Expand Down
@@ -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 { encode } from '@kbn/rison';

import { SecurityPageName } from '../../../common/constants';
import { formatPageFilterSearchParam } from '../../../common/utils/format_page_filter_search_param';
import type { FilterItemObj } from '../components/filter_group/types';
import { useNavigation } from '../lib/kibana';
import { URL_PARAM_KEY } from './use_url_state';

export const useNavigateToAlertsPageWithFilters = () => {
const { navigateTo } = useNavigation();

return (filterItems: FilterItemObj | FilterItemObj[]) => {
const url = encode(
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
formatPageFilterSearchParam(Array.isArray(filterItems) ? filterItems : [filterItems])
);

navigateTo({
deepLinkId: SecurityPageName.alerts,
path: `?${URL_PARAM_KEY.pageFilter}=${url}`,
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
});
};
};
Expand Up @@ -5,12 +5,11 @@
* 2.0.
*/
import React, { useCallback, useMemo } from 'react';
import { isEmpty } from 'lodash/fp';
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLoadingSpinner } from '@elastic/eui';
import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { ShapeTreeNode, ElementClickListener } from '@elastic/charts';
import type { ShapeTreeNode } from '@elastic/charts';
import type { SeverityBuckets as SeverityData } from '../../../../overview/components/detection_response/alerts_by_status/types';
import type { FillColor } from '../../../../common/components/charts/donutchart';
import { DonutChart } from '../../../../common/components/charts/donutchart';
Expand Down Expand Up @@ -56,17 +55,9 @@ export const SeverityLevelChart: React.FC<SeverityLevelProps> = ({
},
};

const onElementClick: ElementClickListener = useCallback(
(event) => {
const flattened = event.flat(2);
const level =
flattened.length > 0 &&
'groupByRollup' in flattened[0] &&
flattened[0].groupByRollup != null
? `${flattened[0].groupByRollup}`
: '';

if (addFilter != null && !isEmpty(level.trim())) {
const onDonutPartitionClicked = useCallback(
(level: string) => {
if (addFilter) {
addFilter({ field: ALERT_SEVERITY, value: level.toLowerCase() });
}
},
Expand Down Expand Up @@ -95,7 +86,7 @@ export const SeverityLevelChart: React.FC<SeverityLevelProps> = ({
label={TOTAL_COUNT_OF_ALERTS}
title={<ChartLabel count={count} />}
totalCount={count}
onElementClick={onElementClick}
onDonutPartitionClicked={onDonutPartitionClicked}
/>
)}
</EuiFlexItem>
Expand Down
Expand Up @@ -20,6 +20,7 @@ import type { ShapeTreeNode } from '@elastic/charts';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import styled from 'styled-components';

import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters';
import type { ESBoolQuery } from '../../../../../common/typed_json';
import type { FillColor } from '../../../../common/components/charts/donutchart';
import { DonutChart } from '../../../../common/components/charts/donutchart';
Expand Down Expand Up @@ -80,6 +81,13 @@ interface AlertsByStatusProps {
signalIndexName: string | null;
}

enum Statuses {
OPEN = 'open',
CLOSED = 'closed',
ACKNOWLEDGED = 'acknowledged',
}
jamster10 marked this conversation as resolved.
Show resolved Hide resolved

const KIBANA_WORKFLOW_STATUS = 'kibana.alert.workflow_status';
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
const legendField = 'kibana.alert.severity';
const chartConfigs: Array<{ key: Severity; label: string; color: string }> = [
{ key: 'critical', label: STATUS_CRITICAL_LABEL, color: SEVERITY_COLOR.critical },
Expand All @@ -105,6 +113,7 @@ export const AlertsByStatus = ({
}: AlertsByStatusProps) => {
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTION_RESPONSE_ALERTS_BY_STATUS_ID);
const { openTimelineWithFilters } = useNavigateToTimeline();
const navigateToAlerts = useNavigateToAlertsPageWithFilters();
const { onClick: goToAlerts, href } = useGetSecuritySolutionLinkProps()({
deepLinkId: SecurityPageName.alerts,
});
Expand Down Expand Up @@ -151,6 +160,54 @@ export const AlertsByStatus = ({
[]
);

const onDonutPartitionClick = useCallback(
(level: string, status: Status) => {
if (level && status) {
navigateToAlerts([
{
title: 'Severity',
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
selectedOptions: [level],
fieldName: legendField,
},
{
title: 'Status',
selectedOptions: [status],
fieldName: KIBANA_WORKFLOW_STATUS,
},
...(entityFilter
? [
{
selectedOptions: [entityFilter.value],
fieldName: entityFilter.field,
},
]
: []),
]);
}
},
[entityFilter, navigateToAlerts]
);

const navigateToAlertsWithStatus = useCallback(
(status: Status) =>
navigateToAlerts([
{
title: 'Status',
selectedOptions: [status],
fieldName: KIBANA_WORKFLOW_STATUS,
},
...(entityFilter
? [
{
selectedOptions: [entityFilter.value],
fieldName: entityFilter.field,
},
]
: []),
]),
[entityFilter, navigateToAlerts]
);

const openCount = donutData?.open?.total ?? 0;
const acknowledgedCount = donutData?.acknowledged?.total ?? 0;
const closedCount = donutData?.closed?.total ?? 0;
Expand Down Expand Up @@ -230,17 +287,25 @@ export const AlertsByStatus = ({
isDonut={true}
label={STATUS_OPEN}
scopeId={SourcererScopeName.detections}
stackByField="kibana.alert.workflow_status"
stackByField={KIBANA_WORKFLOW_STATUS}
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
timerange={timerange}
width={ChartSize}
/>
) : (
<DonutChart
onDonutPartitionClicked={(level) =>
jamster10 marked this conversation as resolved.
Show resolved Hide resolved
onDonutPartitionClick(level, Statuses.OPEN)
}
data={donutData?.open?.severities}
fillColor={fillColor}
height={donutHeight}
label={STATUS_OPEN}
title={<ChartLabel count={openCount} />}
title={
<ChartLabel
onClick={() => navigateToAlertsWithStatus(Statuses.OPEN)}
count={openCount}
/>
}
totalCount={openCount}
/>
)}
Expand All @@ -259,7 +324,7 @@ export const AlertsByStatus = ({
isDonut={true}
label={STATUS_ACKNOWLEDGED}
scopeId={SourcererScopeName.detections}
stackByField="kibana.alert.workflow_status"
stackByField={KIBANA_WORKFLOW_STATUS}
timerange={timerange}
width={ChartSize}
/>
Expand All @@ -269,7 +334,15 @@ export const AlertsByStatus = ({
fillColor={fillColor}
height={donutHeight}
label={STATUS_ACKNOWLEDGED}
title={<ChartLabel count={acknowledgedCount} />}
onDonutPartitionClicked={(level) =>
onDonutPartitionClick(level, Statuses.ACKNOWLEDGED)
}
title={
<ChartLabel
onClick={() => navigateToAlertsWithStatus(Statuses.ACKNOWLEDGED)}
count={acknowledgedCount}
/>
}
totalCount={acknowledgedCount}
/>
)}
Expand All @@ -285,17 +358,25 @@ export const AlertsByStatus = ({
isDonut={true}
label={STATUS_CLOSED}
scopeId={SourcererScopeName.detections}
stackByField="kibana.alert.workflow_status"
stackByField={KIBANA_WORKFLOW_STATUS}
timerange={timerange}
width={ChartSize}
/>
) : (
<DonutChart
data={donutData?.acknowledged?.severities}
data={donutData?.closed?.severities}
fillColor={fillColor}
height={donutHeight}
label={STATUS_CLOSED}
title={<ChartLabel count={closedCount} />}
onDonutPartitionClicked={(level) =>
onDonutPartitionClick(level, Statuses.CLOSED)
}
title={
<ChartLabel
onClick={() => navigateToAlertsWithStatus(Statuses.CLOSED)}
count={closedCount}
/>
}
totalCount={closedCount}
/>
)}
Expand Down
Expand Up @@ -4,26 +4,35 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiLink } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
import { FormattedCount } from '../../../../common/components/formatted_number';

interface ChartLabelProps {
count: number | null | undefined;
onClick?: () => void;
}

const PlaceHolder = styled.div`
padding: ${(props) => props.theme.eui.euiSizeS};
`;

const ChartLabelComponent: React.FC<ChartLabelProps> = ({ count }) => {
return count != null ? (
<b>
<FormattedCount count={count} />
</b>
) : (
<PlaceHolder />
);
const ChartLabelComponent: React.FC<ChartLabelProps> = ({ count, onClick }) => {
if (count) {
return onClick ? (
<EuiLink onClick={onClick}>
<b>
<FormattedCount count={count} />
</b>
</EuiLink>
) : (
<b>
<FormattedCount count={count} />
</b>
);
}
return <PlaceHolder />;
};

ChartLabelComponent.displayName = 'ChartLabelComponent';
Expand Down