mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security solution] Grouping UI package (#152385)
This commit is contained in:
parent
54d6321246
commit
2a1740d035
46 changed files with 491 additions and 398 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -533,6 +533,7 @@ packages/kbn-securitysolution-autocomplete @elastic/security-solution-platform
|
|||
packages/kbn-securitysolution-ecs @elastic/security-threat-hunting-explore
|
||||
packages/kbn-securitysolution-es-utils @elastic/security-solution-platform
|
||||
packages/kbn-securitysolution-exception-list-components @elastic/security-solution-platform
|
||||
packages/kbn-securitysolution-grouping @elastic/security-threat-hunting-explore
|
||||
packages/kbn-securitysolution-hook-utils @elastic/security-solution-platform
|
||||
packages/kbn-securitysolution-io-ts-alerting-types @elastic/security-solution-platform
|
||||
packages/kbn-securitysolution-io-ts-list-types @elastic/security-solution-platform
|
||||
|
@ -1007,7 +1008,6 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations
|
|||
|
||||
/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/components/charts @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/components/grouping @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/components/header_page @elastic/security-threat-hunting-explore
|
||||
/x-pack/plugins/security_solution/public/common/components/header_section @elastic/security-threat-hunting-explore
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"files": "src/plugins/files",
|
||||
"filesManagement": "src/plugins/files_management",
|
||||
"flot": "packages/kbn-flot-charts/lib",
|
||||
"grouping": "packages/kbn-securitysolution-grouping/src",
|
||||
"guidedOnboarding": "src/plugins/guided_onboarding",
|
||||
"guidedOnboardingPackage": "packages/kbn-guided-onboarding",
|
||||
"home": "src/plugins/home",
|
||||
|
|
|
@ -535,6 +535,7 @@
|
|||
"@kbn/securitysolution-ecs": "link:packages/kbn-securitysolution-ecs",
|
||||
"@kbn/securitysolution-es-utils": "link:packages/kbn-securitysolution-es-utils",
|
||||
"@kbn/securitysolution-exception-list-components": "link:packages/kbn-securitysolution-exception-list-components",
|
||||
"@kbn/securitysolution-grouping": "link:packages/kbn-securitysolution-grouping",
|
||||
"@kbn/securitysolution-hook-utils": "link:packages/kbn-securitysolution-hook-utils",
|
||||
"@kbn/securitysolution-io-ts-alerting-types": "link:packages/kbn-securitysolution-io-ts-alerting-types",
|
||||
"@kbn/securitysolution-io-ts-list-types": "link:packages/kbn-securitysolution-io-ts-list-types",
|
||||
|
|
3
packages/kbn-securitysolution-grouping/README.md
Normal file
3
packages/kbn-securitysolution-grouping/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/securitysolution-grouping
|
||||
|
||||
Grouping component and query. Currently only consumed by security solution alerts table. Package is a WIP. Refactoring to make generic https://github.com/elastic/kibana/issues/152491
|
31
packages/kbn-securitysolution-grouping/index.tsx
Normal file
31
packages/kbn-securitysolution-grouping/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
GroupSelectorProps,
|
||||
Grouping,
|
||||
GroupingProps,
|
||||
GroupSelector,
|
||||
RawBucket,
|
||||
getGroupingQuery,
|
||||
isNoneGroup,
|
||||
} from './src';
|
||||
import type { NamedAggregation, GroupingFieldTotalAggregation, GroupingAggregation } from './src';
|
||||
|
||||
export const getGrouping = (props: GroupingProps): React.ReactElement<GroupingProps> => (
|
||||
<Grouping {...props} />
|
||||
);
|
||||
|
||||
export const getGroupSelector = (
|
||||
props: GroupSelectorProps
|
||||
): React.ReactElement<GroupSelectorProps> => <GroupSelector {...props} />;
|
||||
|
||||
export { isNoneGroup, getGroupingQuery };
|
||||
|
||||
export type { GroupingAggregation, GroupingFieldTotalAggregation, NamedAggregation, RawBucket };
|
26
packages/kbn-securitysolution-grouping/jest.config.js
Normal file
26
packages/kbn-securitysolution-grouping/jest.config.js
Normal file
|
@ -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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-securitysolution-grouping'],
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/packages/kbn-securitysolution-grouping/**/*.{ts,tsx}',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/*.test',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/types/*',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/*.type',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/*.styles',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/mocks/*',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/*.config',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/translations',
|
||||
'!<rootDir>/packages/kbn-securitysolution-grouping/**/types/*',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/packages/kbn-securitysolution-grouping/setup_test.ts'],
|
||||
};
|
5
packages/kbn-securitysolution-grouping/kibana.jsonc
Normal file
5
packages/kbn-securitysolution-grouping/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/securitysolution-grouping",
|
||||
"owner": "@elastic/security-threat-hunting-explore"
|
||||
}
|
6
packages/kbn-securitysolution-grouping/package.json
Normal file
6
packages/kbn-securitysolution-grouping/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/securitysolution-grouping",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
9
packages/kbn-securitysolution-grouping/setup_test.ts
Normal file
9
packages/kbn-securitysolution-grouping/setup_test.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import '@testing-library/jest-dom';
|
|
@ -1,14 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { GroupStats } from './group_stats';
|
||||
import { TestProviders } from '../../../mock';
|
||||
|
||||
const onTakeActionsOpen = jest.fn();
|
||||
const testProps = {
|
||||
|
@ -49,11 +49,7 @@ describe('Group stats', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
it('renders each stat item', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupStats {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = render(<GroupStats {...testProps} />);
|
||||
expect(getByTestId('group-stats')).toBeInTheDocument();
|
||||
testProps.badgeMetricStats.forEach(({ title: stat }) => {
|
||||
expect(getByTestId(`metric-${stat}`)).toBeInTheDocument();
|
||||
|
@ -63,11 +59,7 @@ describe('Group stats', () => {
|
|||
});
|
||||
});
|
||||
it('when onTakeActionsOpen is defined, call onTakeActionsOpen on popover click', () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupStats {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId, queryByTestId } = render(<GroupStats {...testProps} />);
|
||||
fireEvent.click(getByTestId('take-action-button'));
|
||||
expect(onTakeActionsOpen).toHaveBeenCalled();
|
||||
['takeActionItem-1', 'takeActionItem-2'].forEach((actionItem) => {
|
||||
|
@ -75,11 +67,7 @@ describe('Group stats', () => {
|
|||
});
|
||||
});
|
||||
it('when onTakeActionsOpen is undefined, render take actions dropdown on popover click', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupStats {...testProps} onTakeActionsOpen={undefined} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = render(<GroupStats {...testProps} onTakeActionsOpen={undefined} />);
|
||||
fireEvent.click(getByTestId('take-action-button'));
|
||||
['takeActionItem-1', 'takeActionItem-2'].forEach((actionItem) => {
|
||||
expect(getByTestId(actionItem)).toBeInTheDocument();
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
|
@ -16,7 +17,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { BadgeMetric, CustomMetric } from '.';
|
||||
import { StatsContainer } from '../styles';
|
||||
import { statsContainerCss } from '../styles';
|
||||
import { TAKE_ACTION } from '../translations';
|
||||
import type { RawBucket } from '../types';
|
||||
|
||||
|
@ -46,7 +47,7 @@ const GroupStatsComponent = ({
|
|||
() =>
|
||||
badgeMetricStats?.map((metric) => (
|
||||
<EuiFlexItem grow={false} key={metric.title}>
|
||||
<StatsContainer data-test-subj={`metric-${metric.title}`}>
|
||||
<span css={statsContainerCss} data-test-subj={`metric-${metric.title}`}>
|
||||
<>
|
||||
{metric.title}
|
||||
<EuiToolTip position="top" content={metric.value}>
|
||||
|
@ -58,7 +59,7 @@ const GroupStatsComponent = ({
|
|||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
</>
|
||||
</StatsContainer>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
)),
|
||||
[badgeMetricStats]
|
||||
|
@ -68,10 +69,10 @@ const GroupStatsComponent = ({
|
|||
() =>
|
||||
customMetricStats?.map((customMetric) => (
|
||||
<EuiFlexItem grow={false} key={customMetric.title}>
|
||||
<StatsContainer data-test-subj={`customMetric-${customMetric.title}`}>
|
||||
<span css={statsContainerCss} data-test-subj={`customMetric-${customMetric.title}`}>
|
||||
{customMetric.title}
|
||||
{customMetric.customStatRenderer}
|
||||
</StatsContainer>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
)),
|
||||
[customMetricStats]
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const createGroupFilter = (selectedGroup: string, query?: string) =>
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
|
@ -1,14 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
|
||||
import { firstNonNullValue } from '../../helpers';
|
||||
import type { RawBucket } from '../types';
|
||||
import { createGroupFilter } from './helpers';
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { noResultsIllustrationLight } from '@kbn/shared-svg';
|
||||
|
||||
const panelStyle = {
|
||||
maxWidth: 500,
|
||||
|
@ -23,8 +23,6 @@ const heights = {
|
|||
export const EmptyGroupingComponent: React.FC<{ height?: keyof typeof heights }> = ({
|
||||
height = 'tall',
|
||||
}) => {
|
||||
const { http } = useKibana<CoreStart>().services;
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" data-test-subj="empty-results-panel">
|
||||
<EuiFlexGroup style={{ height: heights[height] }} alignItems="center" justifyContent="center">
|
||||
|
@ -36,27 +34,21 @@ export const EmptyGroupingComponent: React.FC<{ height?: keyof typeof heights }>
|
|||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.grouping.empty.title"
|
||||
id="grouping.empty.title"
|
||||
defaultMessage="No grouping results match your selected Group alerts field"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.grouping.empty.description"
|
||||
id="grouping.empty.description"
|
||||
defaultMessage="Try searching over a longer period of time or modifying your Group alerts field"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiImage
|
||||
size="200"
|
||||
alt=""
|
||||
url={http.basePath.prepend(
|
||||
'/plugins/timelines/assets/illustration_product_no_results_magnifying_glass.svg'
|
||||
)}
|
||||
/>
|
||||
<EuiImage size="200" alt="" src={noResultsIllustrationLight} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
@ -50,22 +51,19 @@ export class CustomFieldPanel extends React.PureComponent<Props, State> {
|
|||
<div data-test-subj="custom-field-panel" style={{ padding: 16 }}>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.securitySolution.groupsSelector.customGroupByFieldLabel', {
|
||||
label={i18n.translate('grouping.groupsSelector.customGroupByFieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
helpText={i18n.translate(
|
||||
'xpack.securitySolution.groupsSelector.customGroupByHelpText',
|
||||
{
|
||||
defaultMessage: 'This is the field used for the terms aggregation',
|
||||
}
|
||||
)}
|
||||
helpText={i18n.translate('grouping.groupsSelector.customGroupByHelpText', {
|
||||
defaultMessage: 'This is the field used for the terms aggregation',
|
||||
})}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="groupByCustomField"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.securitySolution.groupsSelector.customGroupByDropdownPlacehoder',
|
||||
'grouping.groupsSelector.customGroupByDropdownPlacehoder',
|
||||
{
|
||||
defaultMessage: 'Select one',
|
||||
}
|
||||
|
@ -86,7 +84,7 @@ export class CustomFieldPanel extends React.PureComponent<Props, State> {
|
|||
fill
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{i18n.translate('xpack.securitySolution.selector.grouping.label.add', {
|
||||
{i18n.translate('grouping.selector.grouping.label.add', {
|
||||
defaultMessage: 'Add',
|
||||
})}
|
||||
</EuiButton>
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { GroupsSelector } from '..';
|
||||
import { GroupSelector } from '..';
|
||||
import React from 'react';
|
||||
|
||||
const onGroupChange = jest.fn();
|
||||
|
@ -69,19 +69,11 @@ describe('group selector', () => {
|
|||
jest.clearAllMocks();
|
||||
});
|
||||
it('Sets the selected group from the groupSelected prop', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupsSelector {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = render(<GroupSelector {...testProps} />);
|
||||
expect(getByTestId('group-selector-dropdown').textContent).toBe('Group alerts by: Rule name');
|
||||
});
|
||||
it('Presents correct option when group selector dropdown is clicked', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupsSelector {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = render(<GroupSelector {...testProps} />);
|
||||
fireEvent.click(getByTestId('group-selector-dropdown'));
|
||||
[
|
||||
...testProps.options,
|
||||
|
@ -92,11 +84,7 @@ describe('group selector', () => {
|
|||
});
|
||||
});
|
||||
it('Presents fields dropdown when custom field option is selected', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupsSelector {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const { getByTestId } = render(<GroupSelector {...testProps} />);
|
||||
fireEvent.click(getByTestId('group-selector-dropdown'));
|
||||
fireEvent.click(getByTestId('panel-none'));
|
||||
expect(onGroupChange).toHaveBeenCalled();
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type {
|
||||
|
@ -16,7 +17,8 @@ import { CustomFieldPanel } from './custom_field_panel';
|
|||
import * as i18n from '../translations';
|
||||
import { StyledContextMenu, StyledEuiButtonEmpty } from '../styles';
|
||||
|
||||
interface GroupSelectorProps {
|
||||
export interface GroupSelectorProps {
|
||||
'data-test-subj'?: string;
|
||||
fields: FieldSpec[];
|
||||
groupSelected: string;
|
||||
onGroupChange: (groupSelection: string) => void;
|
||||
|
@ -24,7 +26,8 @@ interface GroupSelectorProps {
|
|||
title?: string;
|
||||
}
|
||||
|
||||
const GroupsSelectorComponent = ({
|
||||
const GroupSelectorComponent = ({
|
||||
'data-test-subj': dataTestSubj,
|
||||
fields,
|
||||
groupSelected = 'none',
|
||||
onGroupChange,
|
||||
|
@ -131,6 +134,7 @@ const GroupsSelectorComponent = ({
|
|||
|
||||
return (
|
||||
<EuiPopover
|
||||
data-test-subj={dataTestSubj ?? 'groupByPopover'}
|
||||
button={button}
|
||||
closePopover={closePopover}
|
||||
isOpen={isPopoverOpen}
|
||||
|
@ -145,4 +149,4 @@ const GroupsSelectorComponent = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const GroupsSelector = React.memo(GroupsSelectorComponent);
|
||||
export const GroupSelector = React.memo(GroupSelectorComponent);
|
|
@ -1,15 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { fireEvent, render, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { GroupingContainer } from '..';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { createGroupFilter } from '../accordion_panel/helpers';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { Grouping } from './grouping';
|
||||
import { createGroupFilter } from './accordion_panel/helpers';
|
||||
|
||||
const renderChildComponent = jest.fn();
|
||||
const takeActionItems = jest.fn();
|
||||
|
@ -103,6 +104,7 @@ const testProps = {
|
|||
pageSize: 25,
|
||||
onChangeItemsPerPage: jest.fn(),
|
||||
onChangePage: jest.fn(),
|
||||
itemsPerPageOptions: [10, 25, 50, 100],
|
||||
},
|
||||
renderChildComponent,
|
||||
selectedGroup: 'kibana.alert.rule.name',
|
||||
|
@ -115,9 +117,9 @@ describe('grouping container', () => {
|
|||
});
|
||||
it('Renders group counts when groupsNumber > 0', () => {
|
||||
const { getByTestId, getAllByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupingContainer {...testProps} />
|
||||
</TestProviders>
|
||||
<I18nProvider>
|
||||
<Grouping {...testProps} />
|
||||
</I18nProvider>
|
||||
);
|
||||
expect(getByTestId('alert-count').textContent).toBe('2 alerts');
|
||||
expect(getByTestId('groups-count').textContent).toBe('2 groups');
|
||||
|
@ -140,9 +142,9 @@ describe('grouping container', () => {
|
|||
},
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupingContainer {...testProps} data={data} />
|
||||
</TestProviders>
|
||||
<I18nProvider>
|
||||
<Grouping {...testProps} data={data} />
|
||||
</I18nProvider>
|
||||
);
|
||||
expect(queryByTestId('alert-count')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('groups-count')).not.toBeInTheDocument();
|
||||
|
@ -152,9 +154,9 @@ describe('grouping container', () => {
|
|||
|
||||
it('Opens one group at a time when each group is clicked', () => {
|
||||
const { getAllByTestId } = render(
|
||||
<TestProviders>
|
||||
<GroupingContainer {...testProps} />
|
||||
</TestProviders>
|
||||
<I18nProvider>
|
||||
<Grouping {...testProps} />
|
||||
</I18nProvider>
|
||||
);
|
||||
const group1 = within(getAllByTestId('grouping-accordion')[0]).getAllByRole('button')[0];
|
||||
const group2 = within(getAllByTestId('grouping-accordion')[1]).getAllByRole('button')[0];
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
|
@ -14,31 +15,20 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
|
||||
import { createGroupFilter } from '../accordion_panel/helpers';
|
||||
import { tableDefaults } from '../../../store/data_table/defaults';
|
||||
import { defaultUnit } from '../../toolbar/unit';
|
||||
import type { BadgeMetric, CustomMetric } from '../accordion_panel';
|
||||
import { GroupPanel } from '../accordion_panel';
|
||||
import { GroupStats } from '../accordion_panel/group_stats';
|
||||
import { EmptyGroupingComponent } from '../empty_resuls_panel';
|
||||
import { GroupingStyledContainer, GroupsUnitCount } from '../styles';
|
||||
import { GROUPS_UNIT } from '../translations';
|
||||
import type { GroupingTableAggregation, RawBucket } from '../types';
|
||||
import { defaultUnit, firstNonNullValue } from '../helpers';
|
||||
import { createGroupFilter } from './accordion_panel/helpers';
|
||||
import type { BadgeMetric, CustomMetric } from './accordion_panel';
|
||||
import { GroupPanel } from './accordion_panel';
|
||||
import { GroupStats } from './accordion_panel/group_stats';
|
||||
import { EmptyGroupingComponent } from './empty_resuls_panel';
|
||||
import { groupingContainerCss, groupsUnitCountCss } from './styles';
|
||||
import { GROUPS_UNIT } from './translations';
|
||||
import type { GroupingAggregation, GroupingFieldTotalAggregation, RawBucket } from './types';
|
||||
|
||||
export interface GroupingContainerProps {
|
||||
export interface GroupingProps {
|
||||
badgeMetricStats?: (fieldBucket: RawBucket) => BadgeMetric[];
|
||||
customMetricStats?: (fieldBucket: RawBucket) => CustomMetric[];
|
||||
data: GroupingTableAggregation &
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
value?: number | null;
|
||||
buckets?: Array<{
|
||||
doc_count?: number | null;
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
data?: GroupingAggregation & GroupingFieldTotalAggregation;
|
||||
groupPanelRenderer?: (fieldBucket: RawBucket) => JSX.Element | undefined;
|
||||
groupsSelector?: JSX.Element;
|
||||
inspectButton?: JSX.Element;
|
||||
|
@ -48,6 +38,7 @@ export interface GroupingContainerProps {
|
|||
pageSize: number;
|
||||
onChangeItemsPerPage: (itemsPerPageNumber: number) => void;
|
||||
onChangePage: (pageNumber: number) => void;
|
||||
itemsPerPageOptions: number[];
|
||||
};
|
||||
renderChildComponent: (groupFilter: Filter[]) => React.ReactNode;
|
||||
selectedGroup: string;
|
||||
|
@ -55,7 +46,7 @@ export interface GroupingContainerProps {
|
|||
unit?: (n: number) => string;
|
||||
}
|
||||
|
||||
const GroupingContainerComponent = ({
|
||||
const GroupingComponent = ({
|
||||
badgeMetricStats,
|
||||
customMetricStats,
|
||||
data,
|
||||
|
@ -68,7 +59,7 @@ const GroupingContainerComponent = ({
|
|||
selectedGroup,
|
||||
takeActionItems,
|
||||
unit = defaultUnit,
|
||||
}: GroupingContainerProps) => {
|
||||
}: GroupingProps) => {
|
||||
const [trigger, setTrigger] = useState<
|
||||
Record<string, { state: 'open' | 'closed' | undefined; selectedBucket: RawBucket }>
|
||||
>({});
|
||||
|
@ -86,7 +77,7 @@ const GroupingContainerComponent = ({
|
|||
|
||||
const groupPanels = useMemo(
|
||||
() =>
|
||||
data.stackByMultipleFields0?.buckets?.map((groupBucket) => {
|
||||
data?.stackByMultipleFields0?.buckets?.map((groupBucket) => {
|
||||
const group = firstNonNullValue(groupBucket.key);
|
||||
const groupKey = `group0-${group}`;
|
||||
|
||||
|
@ -128,7 +119,7 @@ const GroupingContainerComponent = ({
|
|||
[
|
||||
badgeMetricStats,
|
||||
customMetricStats,
|
||||
data.stackByMultipleFields0?.buckets,
|
||||
data?.stackByMultipleFields0?.buckets,
|
||||
groupPanelRenderer,
|
||||
isLoading,
|
||||
renderChildComponent,
|
||||
|
@ -153,12 +144,18 @@ const GroupingContainerComponent = ({
|
|||
{groupsNumber > 0 ? (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<GroupsUnitCount data-test-subj="alert-count">{unitCountText}</GroupsUnitCount>
|
||||
<span css={groupsUnitCountCss} data-test-subj="alert-count">
|
||||
{unitCountText}
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<GroupsUnitCount data-test-subj="groups-count" style={{ borderRight: 'none' }}>
|
||||
<span
|
||||
css={groupsUnitCountCss}
|
||||
data-test-subj="groups-count"
|
||||
style={{ borderRight: 'none' }}
|
||||
>
|
||||
{unitGroupsCountText}
|
||||
</GroupsUnitCount>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
|
@ -170,7 +167,7 @@ const GroupingContainerComponent = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<GroupingStyledContainer className="eui-xScroll">
|
||||
<div css={groupingContainerCss} className="eui-xScroll">
|
||||
{groupsNumber > 0 ? (
|
||||
<>
|
||||
{groupPanels}
|
||||
|
@ -179,7 +176,7 @@ const GroupingContainerComponent = ({
|
|||
activePage={pagination.pageIndex}
|
||||
data-test-subj="grouping-table-pagination"
|
||||
itemsPerPage={pagination.pageSize}
|
||||
itemsPerPageOptions={tableDefaults.itemsPerPageOptions}
|
||||
itemsPerPageOptions={pagination.itemsPerPageOptions}
|
||||
onChangeItemsPerPage={pagination.onChangeItemsPerPage}
|
||||
onChangePage={pagination.onChangePage}
|
||||
pageCount={pageCount}
|
||||
|
@ -194,9 +191,9 @@ const GroupingContainerComponent = ({
|
|||
<EmptyGroupingComponent />
|
||||
</>
|
||||
)}
|
||||
</GroupingStyledContainer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const GroupingContainer = React.memo(GroupingContainerComponent);
|
||||
export const Grouping = React.memo(GroupingComponent);
|
|
@ -1,16 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { NONE_GROUP_KEY } from './types';
|
||||
|
||||
export * from './container';
|
||||
export * from './query';
|
||||
export * from './query/types';
|
||||
export * from './groups_selector';
|
||||
export * from './group_selector';
|
||||
export * from './types';
|
||||
export * from './grouping';
|
||||
|
||||
export const isNoneGroup = (groupKey: string) => groupKey === NONE_GROUP_KEY;
|
|
@ -1,26 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiContextMenu } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
||||
export const GroupsUnitCount = styled.span`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
border-right: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
export const groupsUnitCountCss = css`
|
||||
font-size: ${euiThemeVars.euiFontSizeXS};
|
||||
font-weight: ${euiThemeVars.euiFontWeightSemiBold};
|
||||
border-right: ${euiThemeVars.euiBorderThin};
|
||||
margin-right: 16px;
|
||||
padding-right: 16px;
|
||||
`;
|
||||
|
||||
export const StatsContainer = styled.span`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||
border-right: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
export const statsContainerCss = css`
|
||||
font-size: ${euiThemeVars.euiFontSizeXS};
|
||||
font-weight: ${euiThemeVars.euiFontWeightSemiBold};
|
||||
border-right: ${euiThemeVars.euiBorderThin};
|
||||
margin-right: 16px;
|
||||
padding-right: 16px;
|
||||
.smallDot {
|
||||
|
@ -33,26 +35,26 @@ export const StatsContainer = styled.span`
|
|||
}
|
||||
`;
|
||||
|
||||
export const GroupingStyledContainer = styled.div`
|
||||
export const groupingContainerCss = css`
|
||||
.euiAccordion__childWrapper .euiAccordion__padding--m {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
border-left: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-right: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-left: ${euiThemeVars.euiBorderThin};
|
||||
border-right: ${euiThemeVars.euiBorderThin};
|
||||
border-bottom: ${euiThemeVars.euiBorderThin};
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.euiAccordion__triggerWrapper {
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-left: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-right: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-bottom: ${euiThemeVars.euiBorderThin};
|
||||
border-left: ${euiThemeVars.euiBorderThin};
|
||||
border-right: ${euiThemeVars.euiBorderThin};
|
||||
border-radius: 6px;
|
||||
min-height: 78px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.groupingAccordionForm {
|
||||
border-top: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-top: ${euiThemeVars.euiBorderThin};
|
||||
border-bottom: none;
|
||||
border-radius: 6px;
|
||||
min-width: 1090px;
|
||||
|
@ -75,7 +77,7 @@ export const StyledContextMenu = euiStyled(EuiContextMenu)`
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
.euiContextMenuItem {
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
border-bottom: ${euiThemeVars.euiBorderThin};
|
||||
}
|
||||
.euiContextMenuItem:last-child {
|
||||
border: none;
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GROUPS_UNIT = (totalCount: number) =>
|
||||
i18n.translate('grouping.total.unit', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `{totalCount, plural, =1 {group} other {groups}}`,
|
||||
});
|
||||
|
||||
export const TAKE_ACTION = i18n.translate('grouping.additionalActions.takeAction', {
|
||||
defaultMessage: 'Take actions',
|
||||
});
|
||||
|
||||
export const BETA = i18n.translate('grouping.betaLabel', {
|
||||
defaultMessage: 'Beta',
|
||||
});
|
||||
|
||||
export const BETA_TOOL_TIP = i18n.translate('grouping.betaToolTip', {
|
||||
defaultMessage:
|
||||
'Grouping may show only a subset of alerts while in beta. To see all alerts, use the list view by selecting "None"',
|
||||
});
|
||||
|
||||
export const GROUP_BY = i18n.translate('grouping.alerts.label', {
|
||||
defaultMessage: 'Group alerts by',
|
||||
});
|
||||
|
||||
export const GROUP_BY_CUSTOM_FIELD = i18n.translate('grouping.customGroupByPanelTitle', {
|
||||
defaultMessage: 'Group By Custom Field',
|
||||
});
|
||||
|
||||
export const SELECT_FIELD = i18n.translate('grouping.groupByPanelTitle', {
|
||||
defaultMessage: 'Select Field',
|
||||
});
|
||||
|
||||
export const NONE = i18n.translate('grouping.noneGroupByOptionName', {
|
||||
defaultMessage: 'None',
|
||||
});
|
||||
|
||||
export const CUSTOM_FIELD = i18n.translate('grouping.customGroupByOptionName', {
|
||||
defaultMessage: 'Custom field',
|
||||
});
|
||||
|
||||
export const ALERTS_UNIT = (totalCount: number) =>
|
||||
i18n.translate('grouping.eventsTab.unit', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`,
|
||||
});
|
|
@ -1,13 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { GenericBuckets } from '../../../../common/search_strategy/common';
|
||||
|
||||
export const DEFAULT_GROUPING_QUERY_ID = 'defaultGroupingQuery';
|
||||
// copied from common/search_strategy/common
|
||||
export interface GenericBuckets {
|
||||
key: string | string[];
|
||||
key_as_string?: string; // contains, for example, formatted dates
|
||||
doc_count: number;
|
||||
}
|
||||
|
||||
export const NONE_GROUP_KEY = 'none';
|
||||
|
||||
|
@ -43,7 +47,7 @@ export type RawBucket = GenericBuckets & {
|
|||
};
|
||||
|
||||
/** Defines the shape of the aggregation returned by Elasticsearch */
|
||||
export interface GroupingTableAggregation {
|
||||
export interface GroupingAggregation {
|
||||
stackByMultipleFields0?: {
|
||||
buckets?: RawBucket[];
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './query';
|
||||
export * from './query/types';
|
|
@ -1,12 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { GroupingQueryArgs } from '..';
|
||||
import { getGroupingQuery, MAX_QUERY_SIZE } from '..';
|
||||
import type { GroupingQueryArgs } from './types';
|
||||
import { getGroupingQuery, MAX_QUERY_SIZE } from '.';
|
||||
|
||||
const testProps: GroupingQueryArgs = {
|
||||
additionalFilters: [],
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
|
@ -1,8 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
32
packages/kbn-securitysolution-grouping/src/helpers.ts
Normal file
32
packages/kbn-securitysolution-grouping/src/helpers.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import * as i18n from './components/translations';
|
||||
|
||||
/**
|
||||
* All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error.
|
||||
* When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField<string>`.
|
||||
*/
|
||||
export type ECSField<T> = T | null | undefined | Array<T | null>;
|
||||
/**
|
||||
* Return first non-null value. If the field contains an array, this will return the first value that isn't null. If the field isn't an array it'll be returned unless it's null.
|
||||
*/
|
||||
export function firstNonNullValue<T>(valueOrCollection: ECSField<T>): T | undefined {
|
||||
if (valueOrCollection === null) {
|
||||
return undefined;
|
||||
} else if (Array.isArray(valueOrCollection)) {
|
||||
for (const value of valueOrCollection) {
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return valueOrCollection;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n);
|
11
packages/kbn-securitysolution-grouping/src/index.ts
Normal file
11
packages/kbn-securitysolution-grouping/src/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './components';
|
||||
export * from './containers';
|
||||
export * from './helpers';
|
28
packages/kbn-securitysolution-grouping/tsconfig.json
Normal file
28
packages/kbn-securitysolution-grouping/tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/shared-svg",
|
||||
"@kbn/ui-theme"
|
||||
]
|
||||
}
|
|
@ -1060,6 +1060,8 @@
|
|||
"@kbn/securitysolution-es-utils/*": ["packages/kbn-securitysolution-es-utils/*"],
|
||||
"@kbn/securitysolution-exception-list-components": ["packages/kbn-securitysolution-exception-list-components"],
|
||||
"@kbn/securitysolution-exception-list-components/*": ["packages/kbn-securitysolution-exception-list-components/*"],
|
||||
"@kbn/securitysolution-grouping": ["packages/kbn-securitysolution-grouping"],
|
||||
"@kbn/securitysolution-grouping/*": ["packages/kbn-securitysolution-grouping/*"],
|
||||
"@kbn/securitysolution-hook-utils": ["packages/kbn-securitysolution-hook-utils"],
|
||||
"@kbn/securitysolution-hook-utils/*": ["packages/kbn-securitysolution-hook-utils/*"],
|
||||
"@kbn/securitysolution-io-ts-alerting-types": ["packages/kbn-securitysolution-io-ts-alerting-types"],
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GROUPS_UNIT = (totalCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.grouping.total.unit', {
|
||||
values: { totalCount },
|
||||
defaultMessage: `{totalCount, plural, =1 {group} other {groups}}`,
|
||||
});
|
||||
|
||||
export const TAKE_ACTION = i18n.translate(
|
||||
'xpack.securitySolution.grouping.additionalActions.takeAction',
|
||||
{
|
||||
defaultMessage: 'Take actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const BETA = i18n.translate('xpack.securitySolution.grouping.betaLabel', {
|
||||
defaultMessage: 'Beta',
|
||||
});
|
||||
|
||||
export const BETA_TOOL_TIP = i18n.translate('xpack.securitySolution.grouping.betaToolTip', {
|
||||
defaultMessage:
|
||||
'Grouping may show only a subset of alerts while in beta. To see all alerts, use the list view by selecting "None"',
|
||||
});
|
||||
|
||||
export const GROUP_BY = i18n.translate('xpack.securitySolution.selector.grouping.label', {
|
||||
defaultMessage: 'Group alerts by',
|
||||
});
|
||||
|
||||
export const GROUP_BY_CUSTOM_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.groupsSelector.customGroupByPanelTitle',
|
||||
{
|
||||
defaultMessage: 'Group By Custom Field',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.groupsSelector.groupByPanelTitle',
|
||||
{
|
||||
defaultMessage: 'Select Field',
|
||||
}
|
||||
);
|
||||
|
||||
export const NONE = i18n.translate('xpack.securitySolution.groupsSelector.noneGroupByOptionName', {
|
||||
defaultMessage: 'None',
|
||||
});
|
||||
|
||||
export const CUSTOM_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.groupsSelector.customGroupByOptionName',
|
||||
{
|
||||
defaultMessage: 'Custom field',
|
||||
}
|
||||
);
|
|
@ -6,16 +6,15 @@
|
|||
*/
|
||||
|
||||
import type { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { GROUP_BY } from '../../../components/grouping/translations';
|
||||
import { getGroupSelector, isNoneGroup } from '@kbn/securitysolution-grouping';
|
||||
import type { TableId } from '../../../../../common/types';
|
||||
import { getDefaultGroupingOptions } from '../../../../detections/components/alerts_table/grouping_settings';
|
||||
import type { State } from '../../../store';
|
||||
import { defaultGroup } from '../../../store/grouping/defaults';
|
||||
import type { GroupOption } from '../../../store/grouping';
|
||||
import { groupActions, groupSelectors } from '../../../store/grouping';
|
||||
import { GroupsSelector, isNoneGroup } from '../../../components/grouping';
|
||||
|
||||
export interface UseGetGroupSelectorArgs {
|
||||
fields: FieldSpec[];
|
||||
|
@ -23,11 +22,7 @@ export interface UseGetGroupSelectorArgs {
|
|||
tableId: TableId;
|
||||
}
|
||||
|
||||
export const useGetGroupingSelector = ({
|
||||
fields,
|
||||
groupingId,
|
||||
tableId,
|
||||
}: UseGetGroupSelectorArgs) => {
|
||||
export const useGetGroupSelector = ({ fields, groupingId, tableId }: UseGetGroupSelectorArgs) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getGroupByIdSelector = groupSelectors.getGroupByIdSelector();
|
||||
|
@ -76,45 +71,31 @@ export const useGetGroupingSelector = ({
|
|||
);
|
||||
}, [defaultGroupingOptions, selectedGroup, setOptions, options]);
|
||||
|
||||
const groupsSelector = useMemo(
|
||||
() => (
|
||||
<GroupsSelector
|
||||
groupSelected={selectedGroup}
|
||||
data-test-subj="alerts-table-group-selector"
|
||||
onGroupChange={(groupSelection: string) => {
|
||||
if (groupSelection === selectedGroup) {
|
||||
return;
|
||||
}
|
||||
setGroupsActivePage(0);
|
||||
setSelectedGroup(groupSelection);
|
||||
const groupsSelector = getGroupSelector({
|
||||
groupSelected: selectedGroup,
|
||||
'data-test-subj': 'alerts-table-group-selector',
|
||||
onGroupChange: (groupSelection: string) => {
|
||||
if (groupSelection === selectedGroup) {
|
||||
return;
|
||||
}
|
||||
setGroupsActivePage(0);
|
||||
setSelectedGroup(groupSelection);
|
||||
|
||||
if (!isNoneGroup(groupSelection) && !options.find((o) => o.key === groupSelection)) {
|
||||
setOptions([
|
||||
...defaultGroupingOptions,
|
||||
{
|
||||
label: groupSelection,
|
||||
key: groupSelection,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setOptions(defaultGroupingOptions);
|
||||
}
|
||||
}}
|
||||
fields={fields}
|
||||
options={options}
|
||||
title={GROUP_BY}
|
||||
/>
|
||||
),
|
||||
[
|
||||
defaultGroupingOptions,
|
||||
fields,
|
||||
options,
|
||||
selectedGroup,
|
||||
setGroupsActivePage,
|
||||
setOptions,
|
||||
setSelectedGroup,
|
||||
]
|
||||
);
|
||||
if (!isNoneGroup(groupSelection) && !options.find((o) => o.key === groupSelection)) {
|
||||
setOptions([
|
||||
...defaultGroupingOptions,
|
||||
{
|
||||
label: groupSelection,
|
||||
key: groupSelection,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setOptions(defaultGroupingOptions);
|
||||
}
|
||||
},
|
||||
fields,
|
||||
options,
|
||||
});
|
||||
|
||||
return groupsSelector;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { tableDefaults } from '../../../store/data_table/defaults';
|
||||
import { groupActions, groupSelectors } from '../../../store/grouping';
|
||||
import type { State } from '../../../store';
|
||||
import { defaultGroup } from '../../../store/grouping/defaults';
|
||||
|
@ -45,6 +46,7 @@ export const useGroupingPagination = ({ groupingId }: UseGroupingPaginationArgs)
|
|||
pageSize: itemsPerPage,
|
||||
onChangeItemsPerPage: setGroupsItemsPerPage,
|
||||
onChangePage: setGroupsActivePage,
|
||||
itemsPerPageOptions: tableDefaults.itemsPerPageOptions,
|
||||
}),
|
||||
[activePage, itemsPerPage, setGroupsActivePage, setGroupsItemsPerPage]
|
||||
);
|
||||
|
|
|
@ -34,7 +34,7 @@ import { isTab } from '@kbn/timelines-plugin/public';
|
|||
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import { AlertsTableComponent } from '../../../../detections/components/alerts_table';
|
||||
import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/grouped_alerts';
|
||||
import { GroupedAlertsTable } from '../../../../detections/components/alerts_table/alerts_grouping';
|
||||
import { useDataTableFilters } from '../../../../common/hooks/use_data_table_filters';
|
||||
import { FILTER_OPEN, TableId } from '../../../../../common/types';
|
||||
import { isMlRule } from '../../../../../common/machine_learning/helpers';
|
||||
|
@ -840,17 +840,19 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
</Display>
|
||||
{ruleId != null && (
|
||||
<GroupedAlertsTable
|
||||
tableId={TableId.alertsOnRuleDetailsPage}
|
||||
defaultFilters={alertMergedFilters}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
from={from}
|
||||
loading={loading}
|
||||
to={to}
|
||||
signalIndexName={signalIndexName}
|
||||
runtimeMappings={runtimeMappings}
|
||||
currentAlertStatusFilterValue={filterGroup}
|
||||
defaultFilters={alertMergedFilters}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={query}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
loading={loading}
|
||||
renderChildComponent={renderGroupedAlertTable}
|
||||
runtimeMappings={runtimeMappings}
|
||||
signalIndexName={signalIndexName}
|
||||
tableId={TableId.alertsOnRuleDetailsPage}
|
||||
to={to}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -8,32 +8,30 @@
|
|||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import { useGetGroupingSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
|
||||
import type {
|
||||
GroupingFieldTotalAggregation,
|
||||
GroupingAggregation,
|
||||
RawBucket,
|
||||
} from '@kbn/securitysolution-grouping';
|
||||
import { getGrouping, isNoneGroup } from '@kbn/securitysolution-grouping';
|
||||
import { useGetGroupSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
|
||||
import type { Status } from '../../../../common/detection_engine/schemas/common';
|
||||
import { defaultGroup } from '../../../common/store/grouping/defaults';
|
||||
import { groupSelectors } from '../../../common/store/grouping';
|
||||
import { InspectButton } from '../../../common/components/inspect';
|
||||
import { defaultUnit } from '../../../common/components/toolbar/unit';
|
||||
import type {
|
||||
GroupingFieldTotalAggregation,
|
||||
GroupingTableAggregation,
|
||||
RawBucket,
|
||||
} from '../../../common/components/grouping';
|
||||
import { GroupingContainer, isNoneGroup } from '../../../common/components/grouping';
|
||||
import { useGlobalTime } from '../../../common/containers/use_global_time';
|
||||
import { combineQueries } from '../../../common/lib/kuery';
|
||||
import type { TableIdLiteral } from '../../../../common/types';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type { inputsModel, State } from '../../../common/store';
|
||||
import { inputsSelectors } from '../../../common/store';
|
||||
import type { State } from '../../../common/store';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useInspectButton } from '../alerts_kpis/common/hooks';
|
||||
|
||||
|
@ -51,26 +49,25 @@ import {
|
|||
import { initGrouping } from '../../../common/store/grouping/actions';
|
||||
import { useGroupingPagination } from '../../../common/containers/grouping/hooks/use_grouping_pagination';
|
||||
|
||||
/** This local storage key stores the `Grid / Event rendered view` selection */
|
||||
export const ALERTS_TABLE_GROUPS_SELECTION_KEY = 'securitySolution.alerts.table.group-selection';
|
||||
|
||||
const ALERTS_GROUPING_ID = 'alerts-grouping';
|
||||
|
||||
interface OwnProps {
|
||||
currentAlertStatusFilterValue?: Status;
|
||||
defaultFilters?: Filter[];
|
||||
from: string;
|
||||
globalFilters: Filter[];
|
||||
globalQuery: Query;
|
||||
hasIndexMaintenance: boolean;
|
||||
hasIndexWrite: boolean;
|
||||
loading: boolean;
|
||||
tableId: TableIdLiteral;
|
||||
to: string;
|
||||
renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement;
|
||||
runtimeMappings: MappingRuntimeFields;
|
||||
signalIndexName: string | null;
|
||||
currentAlertStatusFilterValue?: Status;
|
||||
renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement;
|
||||
tableId: TableIdLiteral;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export type AlertsTableComponentProps = OwnProps & PropsFromRedux;
|
||||
export type AlertsTableComponentProps = OwnProps;
|
||||
|
||||
export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
||||
defaultFilters = [],
|
||||
|
@ -95,20 +92,18 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
|
|||
const { activeGroup: selectedGroup } =
|
||||
useSelector((state: State) => getGroupByIdSelector(state, groupingId)) ?? defaultGroup;
|
||||
|
||||
const {
|
||||
browserFields,
|
||||
indexPattern: indexPatterns,
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.detections);
|
||||
const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView(
|
||||
SourcererScopeName.detections
|
||||
);
|
||||
const kibana = useKibana();
|
||||
|
||||
const getGlobalQuery = useCallback(
|
||||
(customFilters: Filter[]) => {
|
||||
if (browserFields != null && indexPatterns != null) {
|
||||
if (browserFields != null && indexPattern != null) {
|
||||
return combineQueries({
|
||||
config: getEsQueryConfig(kibana.services.uiSettings),
|
||||
dataProviders: [],
|
||||
indexPattern: indexPatterns,
|
||||
indexPattern,
|
||||
browserFields,
|
||||
filters: [
|
||||
...(defaultFilters ?? []),
|
||||
|
@ -122,7 +117,7 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
|
|||
}
|
||||
return null;
|
||||
},
|
||||
[browserFields, defaultFilters, globalFilters, globalQuery, indexPatterns, kibana, to, from]
|
||||
[browserFields, defaultFilters, globalFilters, globalQuery, indexPattern, kibana, to, from]
|
||||
);
|
||||
|
||||
useInvalidFilterQuery({
|
||||
|
@ -188,7 +183,7 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
|
|||
request,
|
||||
response,
|
||||
setQuery: setAlertsQuery,
|
||||
} = useQueryAlerts<{}, GroupingTableAggregation & GroupingFieldTotalAggregation>({
|
||||
} = useQueryAlerts<{}, GroupingAggregation & GroupingFieldTotalAggregation>({
|
||||
query: queryGroups,
|
||||
indexName: signalIndexName,
|
||||
queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING,
|
||||
|
@ -218,14 +213,14 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
|
|||
[uniqueQueryId]
|
||||
);
|
||||
|
||||
const groupsSelector = useGetGroupingSelector({
|
||||
const groupsSelector = useGetGroupSelector({
|
||||
tableId,
|
||||
groupingId,
|
||||
fields: indexPatterns.fields,
|
||||
fields: indexPattern.fields,
|
||||
});
|
||||
|
||||
const takeActionItems = useGroupTakeActionsItems({
|
||||
indexName: indexPatterns.title,
|
||||
indexName: indexPattern.title,
|
||||
currentStatus: currentAlertStatusFilterValue,
|
||||
showAlertStatusActions: hasIndexWrite && hasIndexMaintenance,
|
||||
});
|
||||
|
@ -238,30 +233,25 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
|
|||
|
||||
const groupedAlerts = useMemo(
|
||||
() =>
|
||||
isNoneGroup(selectedGroup) ? (
|
||||
renderChildComponent([])
|
||||
) : (
|
||||
<GroupingContainer
|
||||
badgeMetricStats={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupBadgeMetrics(selectedGroup, fieldBucket)
|
||||
}
|
||||
customMetricStats={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupCustomMetrics(selectedGroup, fieldBucket)
|
||||
}
|
||||
data={alertsGroupsData?.aggregations ?? {}}
|
||||
groupPanelRenderer={(fieldBucket: RawBucket) =>
|
||||
getSelectedGroupButtonContent(selectedGroup, fieldBucket)
|
||||
}
|
||||
groupsSelector={groupsSelector}
|
||||
inspectButton={inspect}
|
||||
isLoading={loading || isLoadingGroups}
|
||||
pagination={pagination}
|
||||
renderChildComponent={renderChildComponent}
|
||||
selectedGroup={selectedGroup}
|
||||
takeActionItems={getTakeActionItems}
|
||||
unit={defaultUnit}
|
||||
/>
|
||||
),
|
||||
isNoneGroup(selectedGroup)
|
||||
? renderChildComponent([])
|
||||
: getGrouping({
|
||||
badgeMetricStats: (fieldBucket: RawBucket) =>
|
||||
getSelectedGroupBadgeMetrics(selectedGroup, fieldBucket),
|
||||
customMetricStats: (fieldBucket: RawBucket) =>
|
||||
getSelectedGroupCustomMetrics(selectedGroup, fieldBucket),
|
||||
data: alertsGroupsData?.aggregations,
|
||||
groupPanelRenderer: (fieldBucket: RawBucket) =>
|
||||
getSelectedGroupButtonContent(selectedGroup, fieldBucket),
|
||||
groupsSelector,
|
||||
inspectButton: inspect,
|
||||
isLoading: loading || isLoadingGroups,
|
||||
pagination,
|
||||
renderChildComponent,
|
||||
selectedGroup,
|
||||
takeActionItems: getTakeActionItems,
|
||||
unit: defaultUnit,
|
||||
}),
|
||||
[
|
||||
alertsGroupsData?.aggregations,
|
||||
getTakeActionItems,
|
||||
|
@ -282,21 +272,4 @@ export const GroupedAlertsTableComponent: React.FC<AlertsTableComponentProps> =
|
|||
return groupedAlerts;
|
||||
};
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getGlobalInputs = inputsSelectors.globalSelector();
|
||||
const mapStateToProps = (state: State) => {
|
||||
const globalInputs: inputsModel.InputsRange = getGlobalInputs(state);
|
||||
const { query, filters } = globalInputs;
|
||||
return {
|
||||
globalQuery: query,
|
||||
globalFilters: filters,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const connector = connect(makeMapStateToProps);
|
||||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const GroupedAlertsTable = connector(React.memo(GroupedAlertsTableComponent));
|
||||
export const GroupedAlertsTable = React.memo(GroupedAlertsTableComponent);
|
|
@ -18,9 +18,9 @@ import {
|
|||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { isArray } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import type { RawBucket } from '@kbn/securitysolution-grouping';
|
||||
import { firstNonNullValue } from '../../../../../common/endpoint/models/ecs_safety_helpers';
|
||||
import type { GenericBuckets } from '../../../../../common/search_strategy';
|
||||
import type { RawBucket } from '../../../../common/components/grouping';
|
||||
import { PopoverItems } from '../../../../common/components/popover_items';
|
||||
import { COLUMN_TAGS } from '../../../pages/detection_engine/rules/translations';
|
||||
|
||||
|
|
|
@ -7,11 +7,7 @@
|
|||
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import type {
|
||||
BadgeMetric,
|
||||
CustomMetric,
|
||||
} from '../../../../common/components/grouping/accordion_panel';
|
||||
import type { RawBucket } from '../../../../common/components/grouping';
|
||||
import type { RawBucket } from '@kbn/securitysolution-grouping';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const getSingleGroupSeverity = (severity?: string) => {
|
||||
|
@ -67,10 +63,7 @@ const multiSeverity = (
|
|||
</>
|
||||
);
|
||||
|
||||
export const getSelectedGroupBadgeMetrics = (
|
||||
selectedGroup: string,
|
||||
bucket: RawBucket
|
||||
): BadgeMetric[] => {
|
||||
export const getSelectedGroupBadgeMetrics = (selectedGroup: string, bucket: RawBucket) => {
|
||||
const defaultBadges = [
|
||||
{
|
||||
title: i18n.STATS_GROUP_ALERTS,
|
||||
|
@ -138,10 +131,7 @@ export const getSelectedGroupBadgeMetrics = (
|
|||
];
|
||||
};
|
||||
|
||||
export const getSelectedGroupCustomMetrics = (
|
||||
selectedGroup: string,
|
||||
bucket: RawBucket
|
||||
): CustomMetric[] => {
|
||||
export const getSelectedGroupCustomMetrics = (selectedGroup: string, bucket: RawBucket) => {
|
||||
const singleSeverityComponent =
|
||||
bucket.severitiesSubAggregation?.buckets && bucket.severitiesSubAggregation?.buckets?.length
|
||||
? getSingleGroupSeverity(bucket.severitiesSubAggregation?.buckets[0].key.toString())
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MAX_QUERY_SIZE } from '../../../../common/components/grouping';
|
||||
import { getAlertsGroupingQuery } from '.';
|
||||
|
||||
describe('getAlertsGroupingQuery', () => {
|
||||
|
@ -95,7 +94,7 @@ describe('getAlertsGroupingQuery', () => {
|
|||
},
|
||||
},
|
||||
multi_terms: {
|
||||
size: MAX_QUERY_SIZE,
|
||||
size: 10000,
|
||||
terms: [
|
||||
{
|
||||
field: 'kibana.alert.rule.name',
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import type { NamedAggregation } from '../../../../common/components/grouping';
|
||||
import { getGroupingQuery } from '../../../../common/components/grouping';
|
||||
import type { NamedAggregation } from '@kbn/securitysolution-grouping';
|
||||
import { getGroupingQuery } from '@kbn/securitysolution-grouping';
|
||||
|
||||
const getGroupFields = (groupValue: string) => {
|
||||
if (groupValue === 'kibana.alert.rule.name') {
|
||||
|
|
|
@ -18,8 +18,8 @@ import {
|
|||
SUB_PLUGINS_REDUCER,
|
||||
TestProviders,
|
||||
} from '../../../common/mock';
|
||||
import type { AlertsTableComponentProps } from './grouped_alerts';
|
||||
import { GroupedAlertsTableComponent } from './grouped_alerts';
|
||||
import type { AlertsTableComponentProps } from './alerts_grouping';
|
||||
import { GroupedAlertsTableComponent } from './alerts_grouping';
|
||||
import { TableId } from '../../../../common/types';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser';
|
||||
|
@ -181,7 +181,6 @@ const renderChildComponent = (groupingFilters: Filter[]) => <p data-test-subj="a
|
|||
|
||||
const testProps: AlertsTableComponentProps = {
|
||||
defaultFilters: [],
|
||||
dispatch: jest.fn(),
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
globalFilters: [],
|
||||
globalQuery: {
|
||||
|
@ -255,7 +254,6 @@ describe('GroupedAlertsTable', () => {
|
|||
language: 'language',
|
||||
}}
|
||||
globalFilters={[]}
|
||||
dispatch={jest.fn()}
|
||||
runtimeMappings={{}}
|
||||
signalIndexName={'test'}
|
||||
renderChildComponent={() => <></>}
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useGetGroupingSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
|
||||
import { isNoneGroup } from '@kbn/securitysolution-grouping';
|
||||
import { useGetGroupSelector } from '../../../common/containers/grouping/hooks/use_get_group_selector';
|
||||
import { defaultGroup } from '../../../common/store/grouping/defaults';
|
||||
import { isNoneGroup } from '../../../common/components/grouping';
|
||||
import type { State } from '../../../common/store';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
|
@ -33,7 +33,7 @@ export const getPersistentControlsHook = (tableId: TableId) => {
|
|||
|
||||
const { indexPattern: indexPatterns } = useSourcererDataView(SourcererScopeName.detections);
|
||||
|
||||
const groupsSelector = useGetGroupingSelector({
|
||||
const groupsSelector = useGetGroupSelector({
|
||||
fields: indexPatterns.fields,
|
||||
groupingId: tableId,
|
||||
tableId,
|
||||
|
|
|
@ -78,7 +78,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 { GroupedAlertsTable } from '../../components/alerts_table/grouped_alerts';
|
||||
import { GroupedAlertsTable } from '../../components/alerts_table/alerts_grouping';
|
||||
import { AlertsTableComponent } from '../../components/alerts_table';
|
||||
/**
|
||||
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
|
||||
|
@ -358,7 +358,7 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
]
|
||||
);
|
||||
|
||||
const renderGroupedAlertTable = useCallback(
|
||||
const renderAlertTable = useCallback(
|
||||
(groupingFilters: Filter[]) => {
|
||||
return (
|
||||
<AlertsTableComponent
|
||||
|
@ -455,17 +455,19 @@ const DetectionEnginePageComponent: React.FC<DetectionEngineComponentProps> = ({
|
|||
<EuiSpacer size="l" />
|
||||
</Display>
|
||||
<GroupedAlertsTable
|
||||
tableId={TableId.alertsOnAlertsPage}
|
||||
loading={isAlertTableLoading}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
from={from}
|
||||
defaultFilters={alertsTableDefaultFilters}
|
||||
signalIndexName={signalIndexName}
|
||||
runtimeMappings={runtimeMappings}
|
||||
to={to}
|
||||
currentAlertStatusFilterValue={filterGroup}
|
||||
renderChildComponent={renderGroupedAlertTable}
|
||||
defaultFilters={alertsTableDefaultFilters}
|
||||
from={from}
|
||||
globalFilters={filters}
|
||||
globalQuery={query}
|
||||
hasIndexMaintenance={hasIndexMaintenance ?? false}
|
||||
hasIndexWrite={hasIndexWrite ?? false}
|
||||
loading={isAlertTableLoading}
|
||||
renderChildComponent={renderAlertTable}
|
||||
runtimeMappings={runtimeMappings}
|
||||
signalIndexName={signalIndexName}
|
||||
tableId={TableId.alertsOnAlertsPage}
|
||||
to={to}
|
||||
/>
|
||||
</SecuritySolutionPageWrapper>
|
||||
</StyledFullHeightContainer>
|
||||
|
|
|
@ -144,5 +144,6 @@
|
|||
"@kbn/shared-ux-router",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/expandable-flyout",
|
||||
"@kbn/securitysolution-grouping",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4841,6 +4841,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/securitysolution-grouping@link:packages/kbn-securitysolution-grouping":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/securitysolution-autocomplete@link:packages/kbn-securitysolution-autocomplete":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue