-
Notifications
You must be signed in to change notification settings - Fork 8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution] KPI visualizations on Alerts Page (#149173)
## Summary This PR is a part 2 of #146938 that populates the remaining 2 charts for the summary section on Alerts Page. Capabilities added - Alerts by type: alert count by rule and by type (prevention vs. detection) - Top alerts: top 10 alert grouping based on user selected drop down Changes from previous PR - Refactor `useSeverityChartData` to `useSummaryChartData` so that it can be used by all 3 charts to fetch data - Move `SeverityLevel` chart up one level to `alerts_kpi` folder to better isolate components for testing. Feature flag: `alertsPageChartsEnabled` ![image](https://user-images.githubusercontent.com/18648970/213945018-57a15c60-ed53-4e86-90f5-c1909e88420d.png) ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
- Loading branch information
1 parent
4a4138d
commit dda650f
Showing
45 changed files
with
2,303 additions
and
560 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
...ion/public/detections/components/alerts_kpis/alerts_by_type_panel/alerts_by_type.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* | ||
* 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 { act, render } from '@testing-library/react'; | ||
import React from 'react'; | ||
import { TestProviders } from '../../../../common/mock'; | ||
import { AlertsByType } from './alerts_by_type'; | ||
import { parsedAlerts } from './mock_data'; | ||
|
||
const display = 'alerts-by-type-palette-display'; | ||
|
||
jest.mock('../../../../common/lib/kibana'); | ||
|
||
jest.mock('react-router-dom', () => { | ||
const actual = jest.requireActual('react-router-dom'); | ||
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; | ||
}); | ||
|
||
describe('Alert by type chart', () => { | ||
const defaultProps = { | ||
data: [], | ||
isLoading: false, | ||
}; | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
test('renders health and pallette display correctly without data', () => { | ||
act(() => { | ||
const { container } = render( | ||
<TestProviders> | ||
<AlertsByType {...defaultProps} /> | ||
</TestProviders> | ||
); | ||
expect(container.querySelector(`[data-test-subj="${display}"]`)).toBeInTheDocument(); | ||
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain( | ||
'Detection:0' | ||
); | ||
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain( | ||
'Prevention:0' | ||
); | ||
}); | ||
}); | ||
|
||
test('renders table correctly without data', () => { | ||
act(() => { | ||
const { container } = render( | ||
<TestProviders> | ||
<AlertsByType {...defaultProps} /> | ||
</TestProviders> | ||
); | ||
expect( | ||
container.querySelector('[data-test-subj="alerts-by-type-table"]') | ||
).toBeInTheDocument(); | ||
expect( | ||
container.querySelector('[data-test-subj="alerts-by-type-table"] tbody')?.textContent | ||
).toEqual('No items found'); | ||
}); | ||
}); | ||
|
||
test('renders health and pallette display correctly with data', () => { | ||
act(() => { | ||
const { container } = render( | ||
<TestProviders> | ||
<AlertsByType data={parsedAlerts} isLoading={false} /> | ||
</TestProviders> | ||
); | ||
expect(container.querySelector(`[data-test-subj="${display}"]`)).toBeInTheDocument(); | ||
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain( | ||
'Detection:583' | ||
); | ||
expect(container.querySelector(`[data-test-subj="${display}"]`)?.textContent).toContain( | ||
'Prevention:6' | ||
); | ||
}); | ||
}); | ||
|
||
test('renders table correctly with data', () => { | ||
act(() => { | ||
const { queryAllByRole } = render( | ||
<TestProviders> | ||
<AlertsByType data={parsedAlerts} isLoading={false} /> | ||
</TestProviders> | ||
); | ||
|
||
parsedAlerts.forEach((_, i) => { | ||
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].rule); | ||
expect(queryAllByRole('row')[i + 1].textContent).toContain(parsedAlerts[i].type); | ||
expect(queryAllByRole('row')[i + 1].textContent).toContain( | ||
parsedAlerts[i].value.toString() | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
134 changes: 134 additions & 0 deletions
134
...solution/public/detections/components/alerts_kpis/alerts_by_type_panel/alerts_by_type.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* | ||
* 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 { | ||
EuiFlexItem, | ||
EuiInMemoryTable, | ||
EuiColorPaletteDisplay, | ||
EuiSpacer, | ||
EuiFlexGroup, | ||
EuiHealth, | ||
EuiText, | ||
} from '@elastic/eui'; | ||
import React, { useMemo } from 'react'; | ||
import styled from 'styled-components'; | ||
import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; | ||
import type { AlertsTypeData, AlertType } from './types'; | ||
import { FormattedCount } from '../../../../common/components/formatted_number'; | ||
import { getAlertsTypeTableColumns } from './columns'; | ||
import { ALERT_TYPE_COLOR } from './helpers'; | ||
|
||
const Wrapper = styled.div` | ||
margin-top: -${({ theme }) => theme.eui.euiSizeM}; | ||
`; | ||
const TableWrapper = styled.div` | ||
height: 178px; | ||
`; | ||
const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` | ||
border: none; | ||
border-radius: 0; | ||
`; | ||
interface PalletteObject { | ||
stop: number; | ||
color: string; | ||
} | ||
|
||
export interface AlertsByTypeProps { | ||
data: AlertsTypeData[]; | ||
isLoading: boolean; | ||
} | ||
|
||
export const AlertsByType: React.FC<AlertsByTypeProps> = ({ data, isLoading }) => { | ||
const columns = useMemo(() => getAlertsTypeTableColumns(), []); | ||
|
||
const subtotals = useMemo( | ||
() => | ||
data.reduce( | ||
(acc: { Detection: number; Prevention: number }, item: AlertsTypeData) => { | ||
if (item.type === 'Detection') { | ||
acc.Detection += item.value; | ||
} | ||
if (item.type === 'Prevention') { | ||
acc.Prevention += item.value; | ||
} | ||
return acc; | ||
}, | ||
{ Detection: 0, Prevention: 0 } | ||
), | ||
[data] | ||
); | ||
|
||
const palette: PalletteObject[] = useMemo( | ||
() => | ||
(Object.keys(subtotals) as AlertType[]).reduce((acc: PalletteObject[], type: AlertType) => { | ||
const previousStop = acc.length > 0 ? acc[acc.length - 1].stop : 0; | ||
if (subtotals[type]) { | ||
const newEntry: PalletteObject = { | ||
stop: previousStop + (subtotals[type] || 0), | ||
color: ALERT_TYPE_COLOR[type], | ||
}; | ||
acc.push(newEntry); | ||
} | ||
return acc; | ||
}, [] as PalletteObject[]), | ||
[subtotals] | ||
); | ||
|
||
const sorting: { sort: { field: keyof AlertsTypeData; direction: SortOrder } } = { | ||
sort: { | ||
field: 'value', | ||
direction: 'desc', | ||
}, | ||
}; | ||
|
||
const pagination: {} = { | ||
pageSize: 25, | ||
showPerPageOptions: false, | ||
}; | ||
|
||
return ( | ||
<Wrapper data-test-subj="alerts-by-type"> | ||
<EuiFlexGroup gutterSize="xs" data-test-subj="alerts-by-type-palette-display"> | ||
{(Object.keys(subtotals) as AlertType[]).map((type) => ( | ||
<EuiFlexItem key={type} grow={false}> | ||
<EuiFlexGroup alignItems="center" gutterSize="xs"> | ||
<EuiFlexItem grow={false}> | ||
<EuiHealth className="eui-alignMiddle" color={ALERT_TYPE_COLOR[type]}> | ||
<EuiText size="xs"> | ||
<h4>{`${type}:`}</h4> | ||
</EuiText> | ||
</EuiHealth> | ||
</EuiFlexItem> | ||
<EuiFlexItem grow={false}> | ||
<EuiText size="xs"> | ||
<FormattedCount count={subtotals[type] || 0} /> | ||
</EuiText> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
</EuiFlexItem> | ||
))} | ||
<EuiSpacer size="xs" /> | ||
</EuiFlexGroup> | ||
<EuiSpacer size="xs" /> | ||
<StyledEuiColorPaletteDisplay size="xs" palette={palette} /> | ||
|
||
<EuiSpacer size="xs" /> | ||
<TableWrapper className="eui-yScroll"> | ||
<EuiInMemoryTable | ||
data-test-subj="alerts-by-type-table" | ||
columns={columns} | ||
items={data} | ||
loading={isLoading} | ||
sorting={sorting} | ||
pagination={pagination} | ||
/> | ||
</TableWrapper> | ||
</Wrapper> | ||
); | ||
}; | ||
|
||
AlertsByType.displayName = 'AlertsByType'; |
69 changes: 69 additions & 0 deletions
69
...curity_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* | ||
* 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 React from 'react'; | ||
import { EuiHealth, EuiText } from '@elastic/eui'; | ||
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; | ||
import type { EuiBasicTableColumn } from '@elastic/eui'; | ||
import type { AlertsTypeData, AlertType } from './types'; | ||
import { DefaultDraggable } from '../../../../common/components/draggables'; | ||
import { FormattedCount } from '../../../../common/components/formatted_number'; | ||
import { ALERTS_HEADERS_RULE_NAME } from '../../alerts_table/translations'; | ||
import { ALERT_TYPE_COLOR } from './helpers'; | ||
import { COUNT_TABLE_TITLE } from '../alerts_count_panel/translations'; | ||
import * as i18n from './translations'; | ||
|
||
export const getAlertsTypeTableColumns = (): Array<EuiBasicTableColumn<AlertsTypeData>> => [ | ||
{ | ||
field: 'rule', | ||
name: ALERTS_HEADERS_RULE_NAME, | ||
'data-test-subj': 'detectionsTable-rule', | ||
truncateText: true, | ||
render: (rule: string) => ( | ||
<EuiText size="xs" className="eui-textTruncate"> | ||
<DefaultDraggable | ||
isDraggable={false} | ||
field={ALERT_RULE_NAME} | ||
hideTopN={true} | ||
id={`alert-detection-draggable-${rule}`} | ||
value={rule} | ||
queryValue={rule} | ||
tooltipContent={null} | ||
truncate={true} | ||
/> | ||
</EuiText> | ||
), | ||
}, | ||
{ | ||
field: 'type', | ||
name: i18n.ALERTS_TYPE_COLUMN_TITLE, | ||
'data-test-subj': 'detectionsTable-type', | ||
truncateText: true, | ||
render: (type: string) => { | ||
return ( | ||
<EuiHealth color={ALERT_TYPE_COLOR[type as AlertType]}> | ||
<EuiText grow={false} size="xs"> | ||
{type} | ||
</EuiText> | ||
</EuiHealth> | ||
); | ||
}, | ||
width: '30%', | ||
}, | ||
{ | ||
field: 'value', | ||
name: COUNT_TABLE_TITLE, | ||
dataType: 'number', | ||
sortable: true, | ||
'data-test-subj': 'detectionsTable-count', | ||
render: (count: number) => ( | ||
<EuiText grow={false} size="xs"> | ||
<FormattedCount count={count} /> | ||
</EuiText> | ||
), | ||
width: '22%', | ||
}, | ||
]; |
26 changes: 26 additions & 0 deletions
26
...y_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/helpers.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* | ||
* 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 { parseAlertsTypeData } from './helpers'; | ||
import * as mock from './mock_data'; | ||
import type { AlertsByTypeAgg } from './types'; | ||
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types'; | ||
|
||
describe('parse alerts by type data', () => { | ||
test('parse alerts with data', () => { | ||
const res = parseAlertsTypeData( | ||
mock.mockAlertsData as AlertSearchResponse<{}, AlertsByTypeAgg> | ||
); | ||
expect(res).toEqual(mock.parsedAlerts); | ||
}); | ||
|
||
test('parse alerts without data', () => { | ||
const res = parseAlertsTypeData( | ||
mock.mockAlertsEmptyData as AlertSearchResponse<{}, AlertsByTypeAgg> | ||
); | ||
expect(res).toEqual([]); | ||
}); | ||
}); |
Oops, something went wrong.