diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
index 0ad71d9a23cc..89230ae03a92 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
@@ -50,6 +50,13 @@ const title = i18n.translate('indexPatternManagement.dataViewTable.title', {
defaultMessage: 'Data views',
});
+const securityDataView = i18n.translate(
+ 'indexPatternManagement.indexPatternTable.badge.securityDataViewTitle',
+ {
+ defaultMessage: 'Security Data View',
+ }
+);
+
interface Props extends RouteComponentProps {
canSave: boolean;
showCreateDialog?: boolean;
@@ -116,6 +123,10 @@ export const IndexPatternTable = ({
+ {index.id && index.id === 'security-solution' && (
+ {securityDataView}
+ )}
+
{index.tags &&
index.tags.map(({ key: tagKey, name: tagName }) => (
{tagName}
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
new file mode 100644
index 000000000000..c16e77e9182f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx
@@ -0,0 +1,121 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import { useLocation } from 'react-router-dom';
+import { GlobalHeader } from '.';
+import { SecurityPageName } from '../../../../common/constants';
+import {
+ createSecuritySolutionStorageMock,
+ mockGlobalState,
+ SUB_PLUGINS_REDUCER,
+ TestProviders,
+} from '../../../common/mock';
+import { TimelineId } from '../../../../common/types/timeline';
+import { createStore } from '../../../common/store';
+import { kibanaObservable } from '../../../../../timelines/public/mock';
+import { sourcererPaths } from '../../../common/containers/sourcerer';
+
+jest.mock('react-router-dom', () => {
+ const actual = jest.requireActual('react-router-dom');
+ return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
+});
+
+jest.mock('../../../common/lib/kibana', () => {
+ const originalModule = jest.requireActual('../../../common/lib/kibana');
+ return {
+ ...originalModule,
+ useKibana: jest
+ .fn()
+ .mockReturnValue({ services: { http: { basePath: { prepend: jest.fn() } } } }),
+ useUiSetting$: jest.fn().mockReturnValue([]),
+ };
+});
+
+jest.mock('react-reverse-portal', () => ({
+ InPortal: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ OutPortal: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ createPortalNode: () => ({ unmount: jest.fn() }),
+}));
+
+describe('global header', () => {
+ const mockSetHeaderActionMenu = jest.fn();
+ const state = {
+ ...mockGlobalState,
+ timeline: {
+ ...mockGlobalState.timeline,
+ timelineById: {
+ [TimelineId.active]: {
+ ...mockGlobalState.timeline.timelineById.test,
+ show: false,
+ },
+ },
+ },
+ };
+ const { storage } = createSecuritySolutionStorageMock();
+ const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+
+ it('has add data link', () => {
+ (useLocation as jest.Mock).mockReturnValue([
+ { pageName: SecurityPageName.overview, detailName: undefined },
+ ]);
+ const { getByText } = render(
+
+
+
+ );
+ expect(getByText('Add integrations')).toBeInTheDocument();
+ });
+
+ it.each(sourcererPaths)('shows sourcerer on %s page', (pathname) => {
+ (useLocation as jest.Mock).mockReturnValue({ pathname });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
+ });
+
+ it('shows sourcerer on rule details page', () => {
+ (useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] });
+
+ const { getByTestId } = render(
+
+
+
+ );
+ expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
+ });
+
+ it('shows no sourcerer if timeline is open', () => {
+ const mockstate = {
+ ...mockGlobalState,
+ timeline: {
+ ...mockGlobalState.timeline,
+ timelineById: {
+ [TimelineId.active]: {
+ ...mockGlobalState.timeline.timelineById.test,
+ show: true,
+ },
+ },
+ },
+ };
+ const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+
+ (useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] });
+
+ const { queryByTestId } = render(
+
+
+
+ );
+
+ expect(queryByTestId('sourcerer-trigger')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
index 41e441fd4110..6afcc649da5f 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
@@ -5,14 +5,14 @@
* 2.0.
*/
import {
- EuiHeaderSection,
- EuiHeaderLinks,
EuiHeaderLink,
+ EuiHeaderLinks,
+ EuiHeaderSection,
EuiHeaderSectionItem,
} from '@elastic/eui';
import React, { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
-import { createPortalNode, OutPortal, InPortal } from 'react-reverse-portal';
+import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { i18n } from '@kbn/i18n';
import { AppMountParameters } from '../../../../../../../src/core/public';
@@ -21,6 +21,12 @@ import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
import { useKibana } from '../../../common/lib/kibana';
import { ADD_DATA_PATH } from '../../../../common/constants';
import { isDetectionsPath } from '../../../../public/helpers';
+import { Sourcerer } from '../../../common/components/sourcerer';
+import { TimelineId } from '../../../../common/types/timeline';
+import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
+import { timelineSelectors } from '../../../timelines/store/timeline';
+import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
+import { getScopeFromPath, showSourcererByPath } from '../../../common/containers/sourcerer';
const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', {
defaultMessage: 'Add integrations',
@@ -40,6 +46,16 @@ export const GlobalHeader = React.memo(
} = useKibana().services;
const { pathname } = useLocation();
+ const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
+ const showTimeline = useShallowEqualSelector(
+ (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show
+ );
+
+ const sourcererScope = getScopeFromPath(pathname);
+ const showSourcerer = showSourcererByPath(pathname);
+
+ const href = useMemo(() => prepend(ADD_DATA_PATH), [prepend]);
+
useEffect(() => {
setHeaderActionMenu((element) => {
const mount = toMountPoint();
@@ -65,11 +81,14 @@ export const GlobalHeader = React.memo(
{BUTTON_ADD_DATA}
+ {showSourcerer && !showTimeline && (
+
+ )}
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx
index 53bc20af5e49..c1eb11ea5182 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx
@@ -9,8 +9,6 @@ import React from 'react';
import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page';
-const CaseHeaderPageComponent: React.FC = (props) => (
-
-);
+const CaseHeaderPageComponent: React.FC = (props) => ;
export const CaseHeaderPage = React.memo(CaseHeaderPageComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
index d00bd7040c16..9e5b265c187c 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
@@ -33,9 +33,6 @@ exports[`HeaderPage it renders 1`] = `
Test supplement
-
= ({
border,
children,
draggableArguments,
- hideSourcerer = false,
isLoading,
- sourcererScope = SourcererScopeName.default,
subtitle,
subtitle2,
title,
@@ -149,7 +143,6 @@ const HeaderPageComponent: React.FC = ({
{children}
)}
- {!hideSourcerer && }
{/* Manually add a 'padding-bottom' to header */}
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx
new file mode 100644
index 000000000000..af21a018ee47
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx
@@ -0,0 +1,126 @@
+/*
+ * 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 {
+ EuiSuperSelectOption,
+ EuiIcon,
+ EuiBadge,
+ EuiButtonEmpty,
+ EuiFormRow,
+ EuiFormRowProps,
+} from '@elastic/eui';
+import styled from 'styled-components';
+
+import { sourcererModel } from '../../store/sourcerer';
+
+import * as i18n from './translations';
+
+export const FormRow = styled(EuiFormRow)`
+ display: ${({ $expandAdvancedOptions }) => ($expandAdvancedOptions ? 'flex' : 'none')};
+ max-width: none;
+`;
+
+export const StyledFormRow = styled(EuiFormRow)`
+ max-width: none;
+`;
+
+export const StyledButton = styled(EuiButtonEmpty)`
+ &:enabled:focus,
+ &:focus {
+ background-color: transparent;
+ }
+`;
+
+export const ResetButton = styled(EuiButtonEmpty)`
+ width: fit-content;
+ &:enabled:focus,
+ &:focus {
+ background-color: transparent;
+ }
+`;
+
+export const PopoverContent = styled.div`
+ width: 600px;
+`;
+
+export const StyledBadge = styled(EuiBadge)`
+ margin-left: 8px;
+`;
+
+interface GetDataViewSelectOptionsProps {
+ dataViewId: string;
+ defaultDataView: sourcererModel.KibanaDataView;
+ isModified: boolean;
+ isOnlyDetectionAlerts: boolean;
+ kibanaDataViews: sourcererModel.KibanaDataView[];
+}
+
+export const getDataViewSelectOptions = ({
+ dataViewId,
+ defaultDataView,
+ isModified,
+ isOnlyDetectionAlerts,
+ kibanaDataViews,
+}: GetDataViewSelectOptionsProps): Array> =>
+ isOnlyDetectionAlerts
+ ? [
+ {
+ inputDisplay: (
+
+ {i18n.SIEM_SECURITY_DATA_VIEW_LABEL}
+
+ {i18n.ALERTS_BADGE_TITLE}
+
+
+ ),
+ value: defaultDataView.id,
+ },
+ ]
+ : kibanaDataViews.map(({ title, id }) => ({
+ inputDisplay:
+ id === defaultDataView.id ? (
+
+ {i18n.SECURITY_DEFAULT_DATA_VIEW_LABEL}
+ {isModified && id === dataViewId && (
+ {i18n.MODIFIED_BADGE_TITLE}
+ )}
+
+ ) : (
+
+ {title}
+ {isModified && id === dataViewId && (
+ {i18n.MODIFIED_BADGE_TITLE}
+ )}
+
+ ),
+ value: id,
+ }));
+
+interface GetTooltipContent {
+ isOnlyDetectionAlerts: boolean;
+ isPopoverOpen: boolean;
+ selectedPatterns: string[];
+ signalIndexName: string | null;
+}
+
+export const getTooltipContent = ({
+ isOnlyDetectionAlerts,
+ isPopoverOpen,
+ selectedPatterns,
+ signalIndexName,
+}: GetTooltipContent): string | null => {
+ if (isPopoverOpen || (isOnlyDetectionAlerts && !signalIndexName)) {
+ return null;
+ }
+ return (isOnlyDetectionAlerts ? [signalIndexName] : selectedPatterns).join(', ');
+};
+
+export const getPatternListWithoutSignals = (
+ patternList: string[],
+ signalIndexName: string | null
+): string[] => patternList.filter((p) => p !== signalIndexName);
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx
index 1b23d23c5eb6..c2da7e78d64e 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx
@@ -17,7 +17,7 @@ import {
SUB_PLUGINS_REDUCER,
TestProviders,
} from '../../mock';
-import { createStore, State } from '../../store';
+import { createStore } from '../../store';
import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control';
const mockDispatch = jest.fn();
@@ -45,31 +45,55 @@ const defaultProps = {
scope: sourcererModel.SourcererScopeName.default,
};
-describe('Sourcerer component', () => {
- const state: State = mockGlobalState;
- const { id, patternList, title } = state.sourcerer.defaultDataView;
- const patternListNoSignals = patternList
- .filter((p) => p !== state.sourcerer.signalIndexName)
- .sort();
- const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({
- availableOptionCount: wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).length,
- optionsSelected: patterns.every((pattern) =>
- wrapper
- .find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`)
- .first()
- .exists()
- ),
- });
+const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({
+ availableOptionCount: wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).length,
+ optionsSelected: patterns.every((pattern) =>
+ wrapper.find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`).first().exists()
+ ),
+});
+const { id, patternList, title } = mockGlobalState.sourcerer.defaultDataView;
+const patternListNoSignals = patternList
+ .filter((p) => p !== mockGlobalState.sourcerer.signalIndexName)
+ .sort();
+let store: ReturnType;
+describe('Sourcerer component', () => {
const { storage } = createSecuritySolutionStorageMock();
- let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
beforeEach(() => {
- store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+ store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
jest.clearAllMocks();
jest.restoreAllMocks();
});
+ it('renders data view title', () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ expect(wrapper.find(`[data-test-subj="sourcerer-title"]`).first().text()).toEqual(
+ 'Data view selection'
+ );
+ });
+
+ it('renders a toggle for advanced options', () => {
+ const testProps = {
+ ...defaultProps,
+ showAlertsOnlyCheckbox: true,
+ };
+ const wrapper = mount(
+
+
+
+ );
+ wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ expect(
+ wrapper.find(`[data-test-subj="sourcerer-advanced-options-toggle"]`).first().text()
+ ).toEqual('Advanced options');
+ });
+
it('renders tooltip', () => {
const wrapper = mount(
@@ -119,25 +143,25 @@ describe('Sourcerer component', () => {
it('Removes duplicate options from title', () => {
store = createStore(
{
- ...state,
+ ...mockGlobalState,
sourcerer: {
- ...state.sourcerer,
+ ...mockGlobalState.sourcerer,
defaultDataView: {
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
kibanaDataViews: [
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
],
sourcererScopes: {
- ...state.sourcerer.sourcererScopes,
+ ...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
@@ -170,25 +194,25 @@ describe('Sourcerer component', () => {
it('Disables options with no data', () => {
store = createStore(
{
- ...state,
+ ...mockGlobalState,
sourcerer: {
- ...state.sourcerer,
+ ...mockGlobalState.sourcerer,
defaultDataView: {
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,fakebeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
kibanaDataViews: [
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,fakebeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
],
sourcererScopes: {
- ...state.sourcerer.sourcererScopes,
+ ...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
@@ -225,15 +249,15 @@ describe('Sourcerer component', () => {
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
- state.sourcerer.defaultDataView,
+ mockGlobalState.sourcerer.defaultDataView,
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
@@ -244,9 +268,8 @@ describe('Sourcerer component', () => {
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
- patternList,
selectedDataViewId: id,
- selectedPatterns: patternList.slice(0, 2),
+ selectedPatterns: patternListNoSignals.slice(0, 2),
},
},
},
@@ -260,7 +283,7 @@ describe('Sourcerer component', () => {
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
- expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({
+ expect(checkOptionsAndSelections(wrapper, patternListNoSignals.slice(0, 2))).toEqual({
// should hide signal index
availableOptionCount: title.split(',').length - 3,
optionsSelected: true,
@@ -272,15 +295,15 @@ describe('Sourcerer component', () => {
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
- state.sourcerer.defaultDataView,
+ mockGlobalState.sourcerer.defaultDataView,
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
@@ -305,7 +328,7 @@ describe('Sourcerer component', () => {
);
- wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({
// should show every option except fakebeat-*
@@ -316,25 +339,25 @@ describe('Sourcerer component', () => {
it('onSave dispatches setSelectedDataView', async () => {
store = createStore(
{
- ...state,
+ ...mockGlobalState,
sourcerer: {
- ...state.sourcerer,
+ ...mockGlobalState.sourcerer,
kibanaDataViews: [
- state.sourcerer.defaultDataView,
+ mockGlobalState.sourcerer.defaultDataView,
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*',
patternList: ['filebeat-*'],
},
],
sourcererScopes: {
- ...state.sourcerer.sourcererScopes,
+ ...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
selectedDataViewId: id,
- selectedPatterns: patternList.slice(0, 2),
+ selectedPatterns: patternListNoSignals.slice(0, 2),
},
},
},
@@ -350,13 +373,12 @@ describe('Sourcerer component', () => {
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
- expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({
+ expect(checkOptionsAndSelections(wrapper, patternListNoSignals.slice(0, 2))).toEqual({
availableOptionCount: title.split(',').length - 3,
optionsSelected: true,
});
-
wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).first().simulate('click');
- expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 3))).toEqual({
+ expect(checkOptionsAndSelections(wrapper, patternListNoSignals.slice(0, 3))).toEqual({
availableOptionCount: title.split(',').length - 4,
optionsSelected: true,
});
@@ -367,7 +389,7 @@ describe('Sourcerer component', () => {
sourcererActions.setSelectedDataView({
id: SourcererScopeName.default,
selectedDataViewId: id,
- selectedPatterns: patternList.slice(0, 3),
+ selectedPatterns: patternListNoSignals.slice(0, 3),
})
);
});
@@ -387,7 +409,7 @@ describe('Sourcerer component', () => {
wrapper
.find(
- `[data-test-subj="sourcerer-combo-box"] [title="${patternList[0]}"] button.euiBadge__iconButton`
+ `[data-test-subj="sourcerer-combo-box"] [title="${patternListNoSignals[0]}"] button.euiBadge__iconButton`
)
.first()
.simulate('click');
@@ -407,13 +429,13 @@ describe('Sourcerer component', () => {
it('disables saving when no index patterns are selected', () => {
store = createStore(
{
- ...state,
+ ...mockGlobalState,
sourcerer: {
- ...state.sourcerer,
+ ...mockGlobalState.sourcerer,
kibanaDataViews: [
- state.sourcerer.defaultDataView,
+ mockGlobalState.sourcerer.defaultDataView,
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
@@ -434,72 +456,21 @@ describe('Sourcerer component', () => {
wrapper.find('[data-test-subj="comboBoxClearButton"]').first().simulate('click');
expect(wrapper.find('[data-test-subj="sourcerer-save"]').first().prop('disabled')).toBeTruthy();
});
- it('Selects a different index pattern', async () => {
- const state2 = {
- ...mockGlobalState,
- sourcerer: {
- ...mockGlobalState.sourcerer,
- kibanaDataViews: [
- state.sourcerer.defaultDataView,
- {
- ...state.sourcerer.defaultDataView,
- id: '1234',
- title: 'fakebeat-*,neatbeat-*',
- patternList: ['fakebeat-*'],
- },
- ],
- sourcererScopes: {
- ...mockGlobalState.sourcerer.sourcererScopes,
- [SourcererScopeName.default]: {
- ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
- loading: false,
- patternList,
- selectedDataViewId: id,
- selectedPatterns: patternList.slice(0, 2),
- },
- },
- },
- };
-
- store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
- const wrapper = mount(
-
-
-
- );
- wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
- wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click');
-
- wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click');
- expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({
- availableOptionCount: 0,
- optionsSelected: true,
- });
- wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click');
-
- expect(mockDispatch).toHaveBeenCalledWith(
- sourcererActions.setSelectedDataView({
- id: SourcererScopeName.default,
- selectedDataViewId: '1234',
- selectedPatterns: ['fakebeat-*'],
- })
- );
- });
it('Does display signals index on timeline sourcerer', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
- state.sourcerer.defaultDataView,
+ mockGlobalState.sourcerer.defaultDataView,
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
@@ -510,9 +481,8 @@ describe('Sourcerer component', () => {
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
loading: false,
- patternList,
selectedDataViewId: id,
- selectedPatterns: patternList.slice(0, 2),
+ selectedPatterns: patternListNoSignals.slice(0, 2),
},
},
},
@@ -524,9 +494,9 @@ describe('Sourcerer component', () => {
);
- wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxToggleListButton"]`).first().simulate('click');
- expect(wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).at(6).text()).toEqual(
+ expect(wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).at(0).text()).toEqual(
mockGlobalState.sourcerer.signalIndexName
);
});
@@ -536,15 +506,15 @@ describe('Sourcerer component', () => {
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
- state.sourcerer.defaultDataView,
+ mockGlobalState.sourcerer.defaultDataView,
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
- ...state.sourcerer.defaultDataView,
+ ...mockGlobalState.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
@@ -555,9 +525,8 @@ describe('Sourcerer component', () => {
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
- patternList,
selectedDataViewId: id,
- selectedPatterns: patternList.slice(0, 2),
+ selectedPatterns: patternListNoSignals.slice(0, 2),
},
},
},
@@ -581,3 +550,204 @@ describe('Sourcerer component', () => {
).toBeFalsy();
});
});
+
+describe('sourcerer on alerts page or rules details page', () => {
+ let wrapper: ReactWrapper;
+ const { storage } = createSecuritySolutionStorageMock();
+ store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+ const testProps = {
+ scope: sourcererModel.SourcererScopeName.detections,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(
+
+
+
+ );
+ wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ wrapper.find(`[data-test-subj="sourcerer-advanced-options-toggle"]`).first().simulate('click');
+ });
+
+ it('renders an alerts badge in sourcerer button', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-alerts-badge"]`).first().text()).toEqual(
+ 'Alerts'
+ );
+ });
+
+ it('renders a callout', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-callout"]`).first().text()).toEqual(
+ 'Data view cannot be modified on this page'
+ );
+ });
+
+ it('disable data view selector', () => {
+ expect(
+ wrapper.find(`[data-test-subj="sourcerer-select"]`).first().prop('disabled')
+ ).toBeTruthy();
+ });
+
+ it('data view selector is default to Security Data View', () => {
+ expect(
+ wrapper.find(`[data-test-subj="sourcerer-select"]`).first().prop('valueOfSelected')
+ ).toEqual('security-solution');
+ });
+
+ it('renders an alert badge in data view selector', () => {
+ expect(wrapper.find(`[data-test-subj="security-alerts-option-badge"]`).first().text()).toEqual(
+ 'Alerts'
+ );
+ });
+
+ it('disable index pattern selector', () => {
+ expect(
+ wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('disabled')
+ ).toBeTruthy();
+ });
+
+ it('shows signal index as index pattern option', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('options')).toEqual([
+ { disabled: false, label: '.siem-signals-spacename', value: '.siem-signals-spacename' },
+ ]);
+ });
+
+ it('does not render reset button', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-reset"]`).exists()).toBeFalsy();
+ });
+
+ it('does not render save button', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeFalsy();
+ });
+});
+
+describe('timeline sourcerer', () => {
+ let wrapper: ReactWrapper;
+ const { storage } = createSecuritySolutionStorageMock();
+ store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+ const testProps = {
+ scope: sourcererModel.SourcererScopeName.timeline,
+ };
+
+ beforeAll(() => {
+ wrapper = mount(
+
+
+
+ );
+ wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
+ wrapper
+ .find(
+ `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-advanced-options-toggle"]`
+ )
+ .first()
+ .simulate('click');
+ });
+
+ it('renders "alerts only" checkbox', () => {
+ wrapper
+ .find(
+ `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-alert-only-checkbox"]`
+ )
+ .first()
+ .simulate('click');
+ expect(wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"]`).first().text()).toEqual(
+ 'Show only detection alerts'
+ );
+ });
+
+ it('data view selector is enabled', () => {
+ expect(
+ wrapper
+ .find(`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-select"]`)
+ .first()
+ .prop('disabled')
+ ).toBeFalsy();
+ });
+
+ it('data view selector is default to Security Default Data View', () => {
+ expect(
+ wrapper
+ .find(`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-select"]`)
+ .first()
+ .prop('valueOfSelected')
+ ).toEqual('security-solution');
+ });
+
+ it('index pattern selector is enabled', () => {
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-combo-box"]`
+ )
+ .first()
+ .prop('disabled')
+ ).toBeFalsy();
+ });
+
+ it('render reset button', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-reset"]`).exists()).toBeTruthy();
+ });
+
+ it('render save button', () => {
+ expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeTruthy();
+ });
+});
+
+describe('Sourcerer integration tests', () => {
+ const state = {
+ ...mockGlobalState,
+ sourcerer: {
+ ...mockGlobalState.sourcerer,
+ kibanaDataViews: [
+ mockGlobalState.sourcerer.defaultDataView,
+ {
+ ...mockGlobalState.sourcerer.defaultDataView,
+ id: '1234',
+ title: 'fakebeat-*,neatbeat-*',
+ patternList: ['fakebeat-*'],
+ },
+ ],
+ sourcererScopes: {
+ ...mockGlobalState.sourcerer.sourcererScopes,
+ [SourcererScopeName.default]: {
+ ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
+ loading: false,
+ selectedDataViewId: id,
+ selectedPatterns: patternListNoSignals.slice(0, 2),
+ },
+ },
+ },
+ };
+
+ const { storage } = createSecuritySolutionStorageMock();
+
+ beforeEach(() => {
+ store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+ it('Selects a different index pattern', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click');
+
+ wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click');
+ expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({
+ availableOptionCount: 0,
+ optionsSelected: true,
+ });
+ wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click');
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ sourcererActions.setSelectedDataView({
+ id: SourcererScopeName.default,
+ selectedDataViewId: '1234',
+ selectedPatterns: ['fakebeat-*'],
+ })
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
index 6f32282c5304..6f223cbb4aa3 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
@@ -7,48 +7,52 @@
import {
EuiButton,
- EuiButtonEmpty,
+ EuiCallOut,
+ EuiCheckbox,
EuiComboBox,
- EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
- EuiIcon,
+ EuiForm,
EuiPopover,
EuiPopoverTitle,
EuiSpacer,
EuiSuperSelect,
- EuiText,
EuiToolTip,
} from '@elastic/eui';
import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
-import styled from 'styled-components';
import * as i18n from './translations';
import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer';
-import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { SourcererScopeName } from '../../store/sourcerer/model';
+import { usePickIndexPatterns } from './use_pick_index_patterns';
+import {
+ FormRow,
+ getDataViewSelectOptions,
+ getTooltipContent,
+ PopoverContent,
+ ResetButton,
+ StyledBadge,
+ StyledButton,
+ StyledFormRow,
+} from './helpers';
-const PopoverContent = styled.div`
- width: 600px;
-`;
-
-const ResetButton = styled(EuiButtonEmpty)`
- width: fit-content;
-`;
interface SourcererComponentProps {
scope: sourcererModel.SourcererScopeName;
}
-const getPatternListWithoutSignals = (
- patternList: string[],
- signalIndexName: string | null
-): string[] => patternList.filter((p) => p !== signalIndexName);
-
export const Sourcerer = React.memo(({ scope: scopeId }) => {
const dispatch = useDispatch();
+ const isDetectionsSourcerer = scopeId === SourcererScopeName.detections;
+ const isTimelineSourcerer = scopeId === SourcererScopeName.timeline;
+
+ const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState(false);
+
+ const isOnlyDetectionAlerts: boolean =
+ isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked);
+
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
const {
defaultDataView,
@@ -58,55 +62,39 @@ export const Sourcerer = React.memo(({ scope: scopeId }
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
-
const [dataViewId, setDataViewId] = useState(selectedDataViewId ?? defaultDataView.id);
- const { patternList, selectablePatterns } = useMemo(() => {
- const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId);
- return theDataView != null
- ? scopeId === SourcererScopeName.default
- ? {
- patternList: getPatternListWithoutSignals(
- theDataView.title
- .split(',')
- // remove duplicates patterns from selector
- .filter((pattern, i, self) => self.indexOf(pattern) === i),
- signalIndexName
- ),
- selectablePatterns: getPatternListWithoutSignals(
- theDataView.patternList,
- signalIndexName
- ),
- }
- : {
- patternList: theDataView.title
- .split(',')
- // remove duplicates patterns from selector
- .filter((pattern, i, self) => self.indexOf(pattern) === i),
- selectablePatterns: theDataView.patternList,
- }
- : { patternList: [], selectablePatterns: [] };
- }, [kibanaDataViews, scopeId, signalIndexName, dataViewId]);
-
- const selectableOptions = useMemo(
- () =>
- patternList.map((indexName) => ({
- label: indexName,
- value: indexName,
- disabled: !selectablePatterns.includes(indexName),
- })),
- [selectablePatterns, patternList]
- );
-
- const [selectedOptions, setSelectedOptions] = useState>>(
- selectedPatterns.map((indexName) => ({
- label: indexName,
- value: indexName,
- }))
+ const {
+ isModified,
+ onChangeCombo,
+ renderOption,
+ selectableOptions,
+ selectedOptions,
+ setIndexPatternsByDataView,
+ } = usePickIndexPatterns({
+ dataViewId,
+ defaultDataViewId: defaultDataView.id,
+ isOnlyDetectionAlerts,
+ kibanaDataViews,
+ scopeId,
+ selectedPatterns,
+ signalIndexName,
+ });
+ const onCheckboxChanged = useCallback(
+ (e) => {
+ setIsOnlyDetectionAlertsChecked(e.target.checked);
+ setDataViewId(defaultDataView.id);
+ setIndexPatternsByDataView(defaultDataView.id, e.target.checked);
+ },
+ [defaultDataView.id, setIndexPatternsByDataView]
);
const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]);
+ const [expandAdvancedOptions, setExpandAdvancedOptions] = useState(false);
- const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []);
+ const setPopoverIsOpenCb = useCallback(() => {
+ setPopoverIsOpen((prevState) => !prevState);
+ setExpandAdvancedOptions(false); // we always want setExpandAdvancedOptions collapsed by default when popover opened
+ }, []);
const onChangeDataView = useCallback(
(newSelectedDataView: string, newSelectedPatterns: string[]) => {
dispatch(
@@ -120,90 +108,64 @@ export const Sourcerer = React.memo(({ scope: scopeId }
[dispatch, scopeId]
);
- const renderOption = useCallback(
- ({ value }) => {value},
- []
- );
-
- const onChangeCombo = useCallback((newSelectedOptions) => {
- setSelectedOptions(newSelectedOptions);
- }, []);
-
const onChangeSuper = useCallback(
(newSelectedOption) => {
setDataViewId(newSelectedOption);
- setSelectedOptions(
- getScopePatternListSelection(
- kibanaDataViews.find((dataView) => dataView.id === newSelectedOption),
- scopeId,
- signalIndexName,
- newSelectedOption === defaultDataView.id
- ).map((indexSelected: string) => ({
- label: indexSelected,
- value: indexSelected,
- }))
- );
+ setIndexPatternsByDataView(newSelectedOption);
},
- [defaultDataView.id, kibanaDataViews, scopeId, signalIndexName]
+ [setIndexPatternsByDataView]
);
const resetDataSources = useCallback(() => {
setDataViewId(defaultDataView.id);
- setSelectedOptions(
- getScopePatternListSelection(defaultDataView, scopeId, signalIndexName, true).map(
- (indexSelected: string) => ({
- label: indexSelected,
- value: indexSelected,
- })
- )
- );
- }, [defaultDataView, scopeId, signalIndexName]);
+ setIndexPatternsByDataView(defaultDataView.id);
+ setIsOnlyDetectionAlertsChecked(false);
+ }, [defaultDataView.id, setIndexPatternsByDataView]);
const handleSaveIndices = useCallback(() => {
- onChangeDataView(
- dataViewId,
- selectedOptions.map((so) => so.label)
- );
+ const patterns = selectedOptions.map((so) => so.label);
+ onChangeDataView(dataViewId, patterns);
setPopoverIsOpen(false);
}, [onChangeDataView, dataViewId, selectedOptions]);
const handleClosePopOver = useCallback(() => {
setPopoverIsOpen(false);
+ setExpandAdvancedOptions(false);
}, []);
const trigger = useMemo(
() => (
-
- {i18n.SOURCERER}
-
+ {i18n.DATA_VIEW}
+ {isModified === 'modified' && {i18n.MODIFIED_BADGE_TITLE}}
+ {isModified === 'alerts' && (
+
+ {i18n.ALERTS_BADGE_TITLE}
+
+ )}
+
),
- [setPopoverIsOpenCb, loading]
+ [isTimelineSourcerer, loading, setPopoverIsOpenCb, isModified]
);
const dataViewSelectOptions = useMemo(
() =>
- kibanaDataViews.map(({ title, id }) => ({
- inputDisplay:
- id === defaultDataView.id ? (
-
- {i18n.SIEM_DATA_VIEW_LABEL}
-
- ) : (
-
- {title}
-
- ),
- value: id,
- })),
- [defaultDataView.id, kibanaDataViews]
+ getDataViewSelectOptions({
+ dataViewId,
+ defaultDataView,
+ isModified: isModified === 'modified',
+ isOnlyDetectionAlerts,
+ kibanaDataViews,
+ }),
+ [dataViewId, defaultDataView, isModified, isOnlyDetectionAlerts, kibanaDataViews]
);
useEffect(() => {
@@ -213,18 +175,16 @@ export const Sourcerer = React.memo(({ scope: scopeId }
: prevSelectedOption
);
}, [selectedDataViewId]);
- useEffect(() => {
- setSelectedOptions(
- selectedPatterns.map((indexName) => ({
- label: indexName,
- value: indexName,
- }))
- );
- }, [selectedPatterns]);
const tooltipContent = useMemo(
- () => (isPopoverOpen ? null : selectedPatterns.join(', ')),
- [selectedPatterns, isPopoverOpen]
+ () =>
+ getTooltipContent({
+ isOnlyDetectionAlerts,
+ isPopoverOpen,
+ selectedPatterns,
+ signalIndexName,
+ }),
+ [isPopoverOpen, isOnlyDetectionAlerts, signalIndexName, selectedPatterns]
);
const buttonWithTooptip = useMemo(() => {
@@ -237,67 +197,117 @@ export const Sourcerer = React.memo(({ scope: scopeId }
);
}, [trigger, tooltipContent]);
+ const onExpandAdvancedOptionsClicked = useCallback(() => {
+ setExpandAdvancedOptions((prevState) => !prevState);
+ }, []);
+
return (
-
- <>{i18n.SELECT_INDEX_PATTERNS}>
+
+ <>{i18n.SELECT_DATA_VIEW}>
+ {isOnlyDetectionAlerts && (
+
+ )}
- {i18n.INDEX_PATTERNS_SELECTION_LABEL}
-
-
-
-
-
-
-
-
- {i18n.INDEX_PATTERNS_RESET}
-
-
-
-
+ {isTimelineSourcerer && (
+
+
+
+ )}
+
+
+
- {i18n.SAVE_INDEX_PATTERNS}
-
-
-
+ onChange={onChangeSuper}
+ options={dataViewSelectOptions}
+ placeholder={i18n.PICK_INDEX_PATTERNS}
+ valueOfSelected={dataViewId}
+ />
+
+
+
+
+
+ {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE}
+
+ {expandAdvancedOptions && }
+
+
+
+
+ {!isDetectionsSourcerer && (
+
+
+
+
+ {i18n.INDEX_PATTERNS_RESET}
+
+
+
+
+ {i18n.SAVE_INDEX_PATTERNS}
+
+
+
+
+ )}
+
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts
index 03fdc5d19171..fcf465ebfc9e 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts
@@ -7,29 +7,85 @@
import { i18n } from '@kbn/i18n';
-export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.dataSourcesLabel', {
- defaultMessage: 'Data sources',
+export const CALL_OUT_TITLE = i18n.translate('xpack.securitySolution.indexPatterns.callOutTitle', {
+ defaultMessage: 'Data view cannot be modified on this page',
});
-export const SIEM_DATA_VIEW_LABEL = i18n.translate(
- 'xpack.securitySolution.indexPatterns.kipLabel',
+export const CALL_OUT_TIMELINE_TITLE = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.callOutTimelineTitle',
{
- defaultMessage: 'Default Security Data View',
+ defaultMessage: 'Data view cannot be modified when show only detection alerts is selected',
}
);
-export const SELECT_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', {
- defaultMessage: 'Data sources selection',
+export const DATA_VIEW = i18n.translate('xpack.securitySolution.indexPatterns.dataViewLabel', {
+ defaultMessage: 'Data view',
});
+export const MODIFIED_BADGE_TITLE = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.modifiedBadgeTitle',
+ {
+ defaultMessage: 'Modified',
+ }
+);
+
+export const ALERTS_BADGE_TITLE = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.alertsBadgeTitle',
+ {
+ defaultMessage: 'Alerts',
+ }
+);
+
+export const SECURITY_DEFAULT_DATA_VIEW_LABEL = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.securityDefaultDataViewLabel',
+ {
+ defaultMessage: 'Security Default Data View',
+ }
+);
+
+export const SIEM_SECURITY_DATA_VIEW_LABEL = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.securityDataViewLabel',
+ {
+ defaultMessage: 'Security Data View',
+ }
+);
+
+export const SELECT_DATA_VIEW = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.selectDataView',
+ {
+ defaultMessage: 'Data view selection',
+ }
+);
export const SAVE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.save', {
defaultMessage: 'Save',
});
-export const INDEX_PATTERNS_SELECTION_LABEL = i18n.translate(
- 'xpack.securitySolution.indexPatterns.selectionLabel',
+export const INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.chooseDataViewLabel',
{
- defaultMessage: 'Choose the source of the data on this page',
+ defaultMessage: 'Choose data view',
+ }
+);
+
+export const INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.advancedOptionsTitle',
+ {
+ defaultMessage: 'Advanced options',
+ }
+);
+
+export const INDEX_PATTERNS_LABEL = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.indexPatternsLabel',
+ {
+ defaultMessage: 'Index patterns',
+ }
+);
+
+export const INDEX_PATTERNS_DESCRIPTIONS = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.descriptionsLabel',
+ {
+ defaultMessage:
+ 'These are the index patterns currently selected. Filtering out index patterns from your data view can help improve overall performance.',
}
);
@@ -54,3 +110,10 @@ export const PICK_INDEX_PATTERNS = i18n.translate(
defaultMessage: 'Pick index patterns',
}
);
+
+export const ALERTS_CHECKBOX_LABEL = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.onlyDetectionAlertsLabel',
+ {
+ defaultMessage: 'Show only detection alerts',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx
new file mode 100644
index 000000000000..2ed231949939
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx
@@ -0,0 +1,167 @@
+/*
+ * 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, { useCallback, useEffect, useMemo, useState } from 'react';
+import { EuiComboBoxOptionOption } from '@elastic/eui';
+import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
+import { sourcererModel } from '../../store/sourcerer';
+import { getPatternListWithoutSignals } from './helpers';
+import { SourcererScopeName } from '../../store/sourcerer/model';
+
+interface UsePickIndexPatternsProps {
+ dataViewId: string;
+ defaultDataViewId: string;
+ isOnlyDetectionAlerts: boolean;
+ kibanaDataViews: sourcererModel.SourcererModel['kibanaDataViews'];
+ scopeId: sourcererModel.SourcererScopeName;
+ selectedPatterns: string[];
+ signalIndexName: string | null;
+}
+
+export type ModifiedTypes = 'modified' | 'alerts' | '';
+
+interface UsePickIndexPatterns {
+ isModified: ModifiedTypes;
+ onChangeCombo: (newSelectedDataViewId: Array>) => void;
+ renderOption: ({ value }: EuiComboBoxOptionOption) => React.ReactElement;
+ selectableOptions: Array>;
+ selectedOptions: Array>;
+ setIndexPatternsByDataView: (newSelectedDataViewId: string, isAlerts?: boolean) => void;
+}
+
+const patternListToOptions = (patternList: string[], selectablePatterns?: string[]) =>
+ patternList.sort().map((s) => ({
+ label: s,
+ value: s,
+ ...(selectablePatterns != null ? { disabled: !selectablePatterns.includes(s) } : {}),
+ }));
+
+export const usePickIndexPatterns = ({
+ dataViewId,
+ defaultDataViewId,
+ isOnlyDetectionAlerts,
+ kibanaDataViews,
+ scopeId,
+ selectedPatterns,
+ signalIndexName,
+}: UsePickIndexPatternsProps): UsePickIndexPatterns => {
+ const alertsOptions = useMemo(
+ () => (signalIndexName ? patternListToOptions([signalIndexName]) : []),
+ [signalIndexName]
+ );
+
+ const { patternList, selectablePatterns } = useMemo(() => {
+ if (isOnlyDetectionAlerts && signalIndexName) {
+ return {
+ patternList: [signalIndexName],
+ selectablePatterns: [signalIndexName],
+ };
+ }
+ const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId);
+ return theDataView != null
+ ? scopeId === sourcererModel.SourcererScopeName.default
+ ? {
+ patternList: getPatternListWithoutSignals(
+ theDataView.title
+ .split(',')
+ // remove duplicates patterns from selector
+ .filter((pattern, i, self) => self.indexOf(pattern) === i),
+ signalIndexName
+ ),
+ selectablePatterns: getPatternListWithoutSignals(
+ theDataView.patternList,
+ signalIndexName
+ ),
+ }
+ : {
+ patternList: theDataView.title
+ .split(',')
+ // remove duplicates patterns from selector
+ .filter((pattern, i, self) => self.indexOf(pattern) === i),
+ selectablePatterns: theDataView.patternList,
+ }
+ : { patternList: [], selectablePatterns: [] };
+ }, [dataViewId, isOnlyDetectionAlerts, kibanaDataViews, scopeId, signalIndexName]);
+
+ const selectableOptions = useMemo(
+ () => patternListToOptions(patternList, selectablePatterns),
+ [patternList, selectablePatterns]
+ );
+ const [selectedOptions, setSelectedOptions] = useState>>(
+ isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns)
+ );
+
+ const getDefaultSelectedOptionsByDataView = useCallback(
+ (id: string, isAlerts: boolean = false): Array> =>
+ scopeId === SourcererScopeName.detections || isAlerts
+ ? alertsOptions
+ : patternListToOptions(
+ getScopePatternListSelection(
+ kibanaDataViews.find((dataView) => dataView.id === id),
+ scopeId,
+ signalIndexName,
+ id === defaultDataViewId
+ )
+ ),
+ [alertsOptions, kibanaDataViews, scopeId, signalIndexName, defaultDataViewId]
+ );
+
+ const defaultSelectedPatternsAsOptions = useMemo(
+ () => getDefaultSelectedOptionsByDataView(dataViewId),
+ [dataViewId, getDefaultSelectedOptionsByDataView]
+ );
+
+ const [isModified, setIsModified] = useState<'modified' | 'alerts' | ''>('');
+ const onSetIsModified = useCallback(
+ (patterns?: string[]) => {
+ if (isOnlyDetectionAlerts) {
+ return setIsModified('alerts');
+ }
+ const modifiedPatterns = patterns != null ? patterns : selectedPatterns;
+ const isPatternsModified =
+ defaultSelectedPatternsAsOptions.length !== modifiedPatterns.length ||
+ !defaultSelectedPatternsAsOptions.every((option) =>
+ modifiedPatterns.find((pattern) => option.value === pattern)
+ );
+ return setIsModified(isPatternsModified ? 'modified' : '');
+ },
+ [defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, selectedPatterns]
+ );
+
+ // when scope updates, check modified to set/remove alerts label
+ useEffect(() => {
+ setSelectedOptions(
+ scopeId === SourcererScopeName.detections
+ ? alertsOptions
+ : patternListToOptions(selectedPatterns)
+ );
+ onSetIsModified(selectedPatterns);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [scopeId, selectedPatterns]);
+
+ const onChangeCombo = useCallback((newSelectedOptions) => {
+ setSelectedOptions(newSelectedOptions);
+ }, []);
+
+ const renderOption = useCallback(
+ ({ value }) => {value},
+ []
+ );
+
+ const setIndexPatternsByDataView = (newSelectedDataViewId: string, isAlerts?: boolean) => {
+ setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts));
+ };
+
+ return {
+ isModified,
+ onChangeCombo,
+ renderOption,
+ selectableOptions,
+ selectedOptions,
+ setIndexPatternsByDataView,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx
index 4ca8bf037261..2edfc1336269 100644
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx
@@ -13,7 +13,15 @@ import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model';
import { useUserInfo } from '../../../detections/components/user_info';
import { timelineSelectors } from '../../../timelines/store/timeline';
-import { ALERTS_PATH, CASES_PATH, RULES_PATH, UEBA_PATH } from '../../../../common/constants';
+import {
+ ALERTS_PATH,
+ CASES_PATH,
+ HOSTS_PATH,
+ NETWORK_PATH,
+ OVERVIEW_PATH,
+ RULES_PATH,
+ UEBA_PATH,
+} from '../../../../common/constants';
import { TimelineId } from '../../../../common';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
@@ -300,3 +308,24 @@ export const getScopeFromPath = (
}) == null
? SourcererScopeName.default
: SourcererScopeName.detections;
+
+export const sourcererPaths = [
+ ALERTS_PATH,
+ `${RULES_PATH}/id/:id`,
+ HOSTS_PATH,
+ NETWORK_PATH,
+ OVERVIEW_PATH,
+ UEBA_PATH,
+];
+
+export const showSourcererByPath = (pathname: string): boolean =>
+ matchPath(pathname, {
+ path: sourcererPaths,
+ strict: false,
+ }) != null;
+
+export const isAlertsOrRulesDetailsPage = (pathname: string): boolean =>
+ matchPath(pathname, {
+ path: [ALERTS_PATH, `${RULES_PATH}/id/:id`],
+ strict: false,
+ }) != null;
diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts
index 1b4efa72127f..c99ed720c7f0 100644
--- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts
@@ -37,15 +37,7 @@ export const getScopePatternListSelection = (
// set to signalIndexName whether or not it exists yet in the patternList
return (signalIndexName != null ? [signalIndexName] : []).sort();
case SourcererScopeName.timeline:
- return (
- signalIndexName != null
- ? [
- // remove signalIndexName in case its already in there and add it whether or not it exists yet in the patternList
- ...patternList.filter((index) => index !== signalIndexName),
- signalIndexName,
- ]
- : patternList
- ).sort();
+ return patternList.sort();
}
};
@@ -96,16 +88,14 @@ export const validateSelectedPatterns = (
selectedDataViewId: dataView?.id ?? null,
selectedPatterns,
...(isEmpty(selectedPatterns)
- ? id === SourcererScopeName.timeline
- ? defaultDataViewByEventType({ state, eventType })
- : {
- selectedPatterns: getScopePatternListSelection(
- dataView ?? state.defaultDataView,
- id,
- state.signalIndexName,
- (dataView ?? state.defaultDataView).id === state.defaultDataView.id
- ),
- }
+ ? {
+ selectedPatterns: getScopePatternListSelection(
+ dataView ?? state.defaultDataView,
+ id,
+ state.signalIndexName,
+ (dataView ?? state.defaultDataView).id === state.defaultDataView.id
+ ),
+ }
: {}),
loading: false,
},
diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx
index 92911ab28537..44f27b690fbc 100644
--- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page';
const DetectionEngineHeaderPageComponent: React.FC = (props) => (
-
+
);
export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent);
diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx
index 3a98f062db65..67ee6c55ac06 100644
--- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx
+++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx
@@ -28,8 +28,6 @@ import { EndpointNotice } from '../components/endpoint_notice';
import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage';
import { ENDPOINT_METADATA_INDEX } from '../../../common/constants';
import { useSourcererDataView } from '../../common/containers/sourcerer';
-import { Sourcerer } from '../../common/components/sourcerer';
-import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useDeepEqualSelector } from '../../common/hooks/use_selector';
import { ThreatIntelLinkPanel } from '../components/overview_cti_links';
import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled';
@@ -96,7 +94,6 @@ const OverviewComponent = () => {
>
)}
-
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
index 6cb0e6f2e798..4bd963b21a7f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx
@@ -42,7 +42,6 @@ import { requiredFieldsForActions } from '../../../../detections/components/aler
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
-import { PickEventType } from '../search_or_filter/pick_events';
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
@@ -57,6 +56,7 @@ import { DetailsPanel } from '../../side_panel';
import { EqlQueryBarTimeline } from '../query_bar/eql';
import { defaultControlColumn } from '../body/control_columns';
import { Sort } from '../body/sort';
+import { Sourcerer } from '../../../../common/components/sourcerer';
const TimelineHeaderContainer = styled.div`
margin-top: 6px;
@@ -283,10 +283,7 @@ export const EqlTabContentComponent: React.FC = ({
-
+
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
index 5f6f2796d4ba..6d53e7194306 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx
@@ -46,7 +46,6 @@ import {
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
-import { PickEventType } from '../search_or_filter/pick_events';
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { sourcererActions } from '../../../../common/store/sourcerer';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
@@ -61,6 +60,7 @@ import { DetailsPanel } from '../../side_panel';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { defaultControlColumn } from '../body/control_columns';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
+import { Sourcerer } from '../../../../common/components/sourcerer';
const TimelineHeaderContainer = styled.div`
margin-top: 6px;
@@ -358,10 +358,7 @@ export const QueryTabContentComponent: React.FC = ({
-
+
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx
deleted file mode 100644
index 47ea0f781f7c..000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx
+++ /dev/null
@@ -1,220 +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 { fireEvent, render, within } from '@testing-library/react';
-import { EuiToolTip } from '@elastic/eui';
-
-import React from 'react';
-import { PickEventType } from './pick_events';
-import {
- createSecuritySolutionStorageMock,
- kibanaObservable,
- mockGlobalState,
- mockSourcererState,
- SUB_PLUGINS_REDUCER,
- TestProviders,
-} from '../../../../common/mock';
-import { TimelineEventsType } from '../../../../../common';
-import { createStore } from '../../../../common/store';
-import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
-
-jest.mock('@elastic/eui', () => {
- const actual = jest.requireActual('@elastic/eui');
- return {
- ...actual,
- EuiToolTip: jest.fn(),
- };
-});
-
-describe('Pick Events/Timeline Sourcerer', () => {
- const defaultProps = {
- eventType: 'all' as TimelineEventsType,
- onChangeEventTypeAndIndexesName: jest.fn(),
- };
- const initialPatterns = [
- ...mockSourcererState.defaultDataView.patternList.filter(
- (p) => p !== mockSourcererState.signalIndexName
- ),
- mockSourcererState.signalIndexName,
- ];
- const { storage } = createSecuritySolutionStorageMock();
-
- // const state = {
- // ...mockGlobalState,
- // sourcerer: {
- // ...mockGlobalState.sourcerer,
- // kibanaIndexPatterns: [
- // { id: '1234', title: 'auditbeat-*' },
- // { id: '9100', title: 'filebeat-*' },
- // { id: '9100', title: 'auditbeat-*,filebeat-*' },
- // { id: '5678', title: 'auditbeat-*,.siem-signals-default' },
- // ],
- // configIndexPatterns:
- // mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns,
- // signalIndexName: mockGlobalState.sourcerer.signalIndexName,
- // sourcererScopes: {
- // ...mockGlobalState.sourcerer.sourcererScopes,
- // [SourcererScopeName.timeline]: {
- // ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
- // loading: false,
- // selectedPatterns: ['filebeat-*'],
- // },
- // },
- // },
- // };
- // const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
- const state = {
- ...mockGlobalState,
- sourcerer: {
- ...mockGlobalState.sourcerer,
- sourcererScopes: {
- ...mockGlobalState.sourcerer.sourcererScopes,
- [SourcererScopeName.timeline]: {
- ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
- loading: false,
- selectedDataViewId: mockGlobalState.sourcerer.defaultDataView.id,
- selectedPatterns: ['filebeat-*'],
- },
- },
- },
- };
- const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
- const mockTooltip = ({
- tooltipContent,
- children,
- }: {
- tooltipContent: string;
- children: React.ReactElement;
- }) => (
-
- {tooltipContent}
- {children}
-
- );
-
- beforeAll(() => {
- (EuiToolTip as unknown as jest.Mock).mockImplementation(mockTooltip);
- });
- beforeEach(() => {
- jest.clearAllMocks();
- jest.restoreAllMocks();
- });
- it('renders', () => {
- const wrapper = render(
-
-
-
- );
- fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger'));
- expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual(
- initialPatterns.sort().join('')
- );
- fireEvent.click(wrapper.getByTestId(`sourcerer-accordion`));
- fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton'));
- const optionNodes = wrapper.getAllByTestId('sourcerer-option');
- expect(optionNodes.length).toBe(1);
- });
- it('Removes duplicate options from options list', () => {
- const store2 = createStore(
- {
- ...mockGlobalState,
- sourcerer: {
- ...mockGlobalState.sourcerer,
- defaultDataView: {
- ...mockGlobalState.sourcerer.defaultDataView,
- id: '1234',
- title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*',
- patternList: ['filebeat-*', 'auditbeat-*'],
- },
- kibanaDataViews: [
- {
- ...mockGlobalState.sourcerer.defaultDataView,
- id: '1234',
- title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*',
- patternList: ['filebeat-*', 'auditbeat-*'],
- },
- ],
- sourcererScopes: {
- ...mockGlobalState.sourcerer.sourcererScopes,
- [SourcererScopeName.timeline]: {
- ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
- loading: false,
- selectedDataViewId: '1234',
- selectedPatterns: ['filebeat-*'],
- },
- },
- },
- },
- SUB_PLUGINS_REDUCER,
- kibanaObservable,
- storage
- );
- const wrapper = render(
-
-
-
- );
- fireEvent.click(wrapper.getByTestId(`sourcerer-timeline-trigger`));
- fireEvent.click(wrapper.getByTestId(`sourcerer-accordion`));
- fireEvent.click(wrapper.getByTestId(`comboBoxToggleListButton`));
- expect(
- wrapper.getByTestId('comboBoxOptionsList timeline-sourcerer-optionsList').textContent
- ).toEqual('auditbeat-*');
- });
-
- it('renders tooltip', () => {
- render(
-
-
-
- );
-
- expect((EuiToolTip as unknown as jest.Mock).mock.calls[0][0].content).toEqual(
- initialPatterns
- .filter((p) => p != null)
- .sort()
- .join(', ')
- );
- });
-
- it('renders popover button inside tooltip', () => {
- const wrapper = render(
-
-
-
- );
- const tooltip = wrapper.getByTestId('timeline-sourcerer-tooltip');
- expect(within(tooltip).getByTestId('sourcerer-timeline-trigger')).toBeTruthy();
- });
-
- it('correctly filters options', () => {
- const wrapper = render(
-
-
-
- );
- fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger'));
- fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton'));
- fireEvent.click(wrapper.getByTestId('sourcerer-accordion'));
- const optionNodes = wrapper.getAllByTestId('sourcerer-option');
- expect(optionNodes.length).toBe(9);
- });
- it('reset button works', () => {
- const wrapper = render(
-
-
-
- );
- fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger'));
- expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual('filebeat-*');
-
- fireEvent.click(wrapper.getByTestId('sourcerer-reset'));
- expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual(
- initialPatterns.sort().join('')
- );
- });
-});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx
deleted file mode 100644
index 791993d67135..000000000000
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx
+++ /dev/null
@@ -1,465 +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 {
- EuiAccordion,
- EuiButton,
- EuiButtonEmpty,
- EuiRadioGroup,
- EuiComboBox,
- EuiComboBoxOptionOption,
- EuiHealth,
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
- EuiPopover,
- EuiPopoverTitle,
- EuiSpacer,
- EuiText,
- EuiToolTip,
- EuiSuperSelect,
-} from '@elastic/eui';
-import deepEqual from 'fast-deep-equal';
-import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
-import styled from 'styled-components';
-
-import { sourcererSelectors } from '../../../../common/store';
-import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
-import { TimelineEventsType } from '../../../../../common';
-import * as i18n from './translations';
-import { getScopePatternListSelection } from '../../../../common/store/sourcerer/helpers';
-import { SIEM_DATA_VIEW_LABEL } from '../../../../common/components/sourcerer/translations';
-import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
-
-const PopoverContent = styled.div`
- width: 600px;
-`;
-
-const ResetButton = styled(EuiButtonEmpty)`
- width: fit-content;
-`;
-
-const MyEuiButton = styled(EuiButton)`
- .euiHealth {
- vertical-align: middle;
- }
-`;
-
-const AllEuiHealth = styled(EuiHealth)`
- margin-left: -2px;
- svg {
- stroke: #fff;
- stroke-width: 1px;
- stroke-linejoin: round;
- width: 19px;
- height: 19px;
- margin-top: 1px;
- z-index: 1;
- }
-`;
-
-const WarningEuiHealth = styled(EuiHealth)`
- margin-left: -17px;
- svg {
- z-index: 0;
- }
-`;
-
-const AdvancedSettings = styled(EuiText)`
- color: ${({ theme }) => theme.eui.euiColorPrimary};
-`;
-
-const ConfigHelper = styled(EuiText)`
- margin-left: 4px;
-`;
-
-const Filter = styled(EuiRadioGroup)`
- margin-left: 4px;
-`;
-
-const PickEventContainer = styled.div`
- .euiSuperSelect {
- width: 170px;
- max-width: 170px;
- button.euiSuperSelectControl {
- padding-top: 3px;
- }
- }
-`;
-
-const getEventTypeOptions = (isCustomDisabled: boolean = true, isDefaultPattern: boolean) => [
- {
- id: 'all',
- label: (
-
- {i18n.ALL_EVENT}
-
- ),
- },
- {
- id: 'raw',
- label: {i18n.RAW_EVENT},
- disabled: !isDefaultPattern,
- },
- {
- id: 'alert',
- label: {i18n.DETECTION_ALERTS_EVENT},
- disabled: !isDefaultPattern,
- },
- {
- id: 'custom',
- label: <>{i18n.CUSTOM_INDEX_PATTERNS}>,
- disabled: isCustomDisabled,
- },
-];
-
-interface PickEventTypeProps {
- eventType: TimelineEventsType;
- onChangeEventTypeAndIndexesName: (
- value: TimelineEventsType,
- indexNames: string[],
- dataViewId: string
- ) => void;
-}
-
-// AKA TimelineSourcerer
-const PickEventTypeComponents: React.FC = ({
- eventType = 'all',
- onChangeEventTypeAndIndexesName,
-}) => {
- const [isPopoverOpen, setPopover] = useState(false);
- const [showAdvanceSettings, setAdvanceSettings] = useState(eventType === 'custom');
- const [filterEventType, setFilterEventType] = useState(eventType);
- const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
- const {
- defaultDataView,
- kibanaDataViews,
- signalIndexName,
- sourcererScope: { loading, selectedPatterns, selectedDataViewId },
- }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) =>
- sourcererScopeSelector(state, SourcererScopeName.timeline)
- );
-
- const [dataViewId, setDataViewId] = useState(selectedDataViewId ?? '');
- const { patternList, selectablePatterns } = useMemo(() => {
- const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId);
- return theDataView != null
- ? {
- patternList: theDataView.title
- .split(',')
- // remove duplicates patterns from selector
- .filter((pattern, i, self) => self.indexOf(pattern) === i),
- selectablePatterns: theDataView.patternList,
- }
- : { patternList: [], selectablePatterns: [] };
- }, [kibanaDataViews, dataViewId]);
- const [selectedOptions, setSelectedOptions] = useState>>(
- selectedPatterns.map((indexName) => ({
- label: indexName,
- value: indexName,
- }))
- );
- const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]);
- const selectableOptions = useMemo(
- () =>
- patternList.map((indexName) => ({
- label: indexName,
- value: indexName,
- 'data-test-subj': 'sourcerer-option',
- disabled: !selectablePatterns.includes(indexName),
- })),
- [selectablePatterns, patternList]
- );
-
- const onChangeFilter = useCallback(
- (filter) => {
- setFilterEventType(filter);
- if (filter === 'all' || filter === 'kibana') {
- setSelectedOptions(
- selectablePatterns.map((indexSelected) => ({
- label: indexSelected,
- value: indexSelected,
- }))
- );
- } else if (filter === 'raw') {
- setSelectedOptions(
- (signalIndexName == null
- ? selectablePatterns
- : selectablePatterns.filter((index) => index !== signalIndexName)
- ).map((indexSelected) => ({
- label: indexSelected,
- value: indexSelected,
- }))
- );
- } else if (filter === 'alert') {
- setSelectedOptions([
- {
- label: signalIndexName ?? '',
- value: signalIndexName ?? '',
- },
- ]);
- }
- },
- [selectablePatterns, signalIndexName]
- );
-
- const onChangeCombo = useCallback(
- (newSelectedOptions: Array>) => {
- const localSelectedPatterns = newSelectedOptions
- .map((nso) => nso.label)
- .sort()
- .join();
- if (localSelectedPatterns === selectablePatterns.sort().join()) {
- setFilterEventType('all');
- } else if (
- dataViewId === defaultDataView.id &&
- localSelectedPatterns ===
- selectablePatterns
- .filter((index) => index !== signalIndexName)
- .sort()
- .join()
- ) {
- setFilterEventType('raw');
- } else if (dataViewId === defaultDataView.id && localSelectedPatterns === signalIndexName) {
- setFilterEventType('alert');
- } else {
- setFilterEventType('custom');
- }
-
- setSelectedOptions(newSelectedOptions);
- },
- [defaultDataView.id, dataViewId, selectablePatterns, signalIndexName]
- );
-
- const onChangeSuper = useCallback(
- (newSelectedOption) => {
- setFilterEventType('all');
- setDataViewId(newSelectedOption);
- setSelectedOptions(
- getScopePatternListSelection(
- kibanaDataViews.find((dataView) => dataView.id === newSelectedOption),
- SourcererScopeName.timeline,
- signalIndexName,
- newSelectedOption === defaultDataView.id
- ).map((indexSelected: string) => ({
- label: indexSelected,
- value: indexSelected,
- }))
- );
- },
- [defaultDataView.id, kibanaDataViews, signalIndexName]
- );
-
- const togglePopover = useCallback(
- () => setPopover((prevIsPopoverOpen) => !prevIsPopoverOpen),
- []
- );
-
- const closePopover = useCallback(() => setPopover(false), []);
-
- const handleSaveIndices = useCallback(() => {
- onChangeEventTypeAndIndexesName(
- filterEventType,
- selectedOptions.map((so) => so.label),
- dataViewId
- );
- setPopover(false);
- }, [dataViewId, filterEventType, onChangeEventTypeAndIndexesName, selectedOptions]);
-
- const resetDataSources = useCallback(() => {
- setDataViewId(defaultDataView.id);
- setSelectedOptions(
- getScopePatternListSelection(
- defaultDataView,
- SourcererScopeName.timeline,
- signalIndexName,
- true
- ).map((indexSelected: string) => ({
- label: indexSelected,
- value: indexSelected,
- }))
- );
- setFilterEventType(eventType);
- }, [defaultDataView, eventType, signalIndexName]);
-
- const dataViewSelectOptions = useMemo(
- () =>
- kibanaDataViews.map(({ title, id }) => ({
- inputDisplay:
- id === defaultDataView.id ? (
-
- {SIEM_DATA_VIEW_LABEL}
-
- ) : (
-
- {title}
-
- ),
- value: id,
- })),
- [defaultDataView.id, kibanaDataViews]
- );
-
- const filterOptions = useMemo(
- () => getEventTypeOptions(filterEventType !== 'custom', dataViewId === defaultDataView.id),
- [defaultDataView.id, filterEventType, dataViewId]
- );
-
- const button = useMemo(() => {
- const options = getEventTypeOptions(true, dataViewId === defaultDataView.id);
- return (
-
- {options.find((opt) => opt.id === eventType)?.label}
-
- );
- }, [defaultDataView.id, eventType, dataViewId, loading, togglePopover]);
-
- const tooltipContent = useMemo(
- () => (isPopoverOpen ? null : selectedPatterns.sort().join(', ')),
- [isPopoverOpen, selectedPatterns]
- );
-
- const buttonWithTooptip = useMemo(() => {
- return tooltipContent ? (
-
- {button}
-
- ) : (
- button
- );
- }, [button, tooltipContent]);
-
- const ButtonContent = useMemo(
- () => (
-
- {showAdvanceSettings
- ? i18n.HIDE_INDEX_PATTERNS_ADVANCED_SETTINGS
- : i18n.SHOW_INDEX_PATTERNS_ADVANCED_SETTINGS}
-
- ),
- [showAdvanceSettings]
- );
-
- useEffect(() => {
- const newSelectedOptions = selectedPatterns.map((indexSelected) => ({
- label: indexSelected,
- value: indexSelected,
- }));
- setSelectedOptions((prevSelectedOptions) => {
- if (!deepEqual(newSelectedOptions, prevSelectedOptions)) {
- return newSelectedOptions;
- }
- return prevSelectedOptions;
- });
- }, [selectedPatterns]);
-
- useEffect(() => {
- setFilterEventType((prevFilter) => (prevFilter !== eventType ? eventType : prevFilter));
- setAdvanceSettings(eventType === 'custom');
- }, [eventType]);
-
- return (
-
-
-
-
- <>{i18n.SELECT_INDEX_PATTERNS}>
-
-
-
-
-
- <>
-
-
-
-
- >
-
- {!showAdvanceSettings && (
- <>
-
-
- {i18n.CONFIGURE_INDEX_PATTERNS}
-
- >
- )}
-
-
-
-
- {i18n.DATA_SOURCES_RESET}
-
-
-
-
- {i18n.SAVE_INDEX_PATTERNS}
-
-
-
-
-
-
- );
-};
-
-export const PickEventType = memo(PickEventTypeComponents);
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
index 3a9b9b0d2693..e59af74d9a47 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx
@@ -46,7 +46,7 @@ export const TimelinesPageComponent: React.FC = () => {
{indicesExist ? (
<>
-
+
{capabilitiesCanUserCRUD && (
@@ -93,6 +93,7 @@ export const TimelinesPageComponent: React.FC = () => {
>
) : (
+
)}
diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx
index 72122aba3c4a..51c06fffb7b6 100644
--- a/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx
@@ -99,7 +99,6 @@ const UebaDetailsComponent: React.FC = ({ detailName, uebaDeta
高度な設定で構成できます。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index b6b274416042..9a8338015841 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -22426,13 +22426,10 @@
"xpack.securitySolution.hostsTable.unit": "{totalCount, plural, other {个主机}}",
"xpack.securitySolution.hostsTable.versionTitle": "版本",
"xpack.securitySolution.hoverActions.showTopTooltip": "显示排名靠前的{fieldName}",
- "xpack.securitySolution.indexPatterns.dataSourcesLabel": "数据源",
"xpack.securitySolution.indexPatterns.disabled": "在此页面上建议使用已禁用的索引模式,但是首先需要在 Kibana 索引模式设置中配置这些模式",
- "xpack.securitySolution.indexPatterns.help": "数据源的选择",
"xpack.securitySolution.indexPatterns.pickIndexPatternsCombo": "选取索引模式",
"xpack.securitySolution.indexPatterns.resetButton": "重置",
"xpack.securitySolution.indexPatterns.save": "保存",
- "xpack.securitySolution.indexPatterns.selectionLabel": "在此页面上选择数据源",
"xpack.securitySolution.insert.timeline.insertTimelineButton": "插入时间线链接",
"xpack.securitySolution.inspect.modal.closeTitle": "关闭",
"xpack.securitySolution.inspect.modal.indexPatternDescription": "连接到 Elasticsearch 索引的索引模式。可以在“Kibana”>“高级设置”中配置这些索引。",