Sourcerer UI (#117601)
This commit is contained in:
parent
9f02de6ed2
commit
6d951fee69
|
@ -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 = ({
|
|||
</EuiButtonEmpty>
|
||||
 
|
||||
<EuiBadgeGroup gutterSize="s">
|
||||
{index.id && index.id === 'security-solution' && (
|
||||
<EuiBadge key="security-solution">{securityDataView}</EuiBadge>
|
||||
)}
|
||||
|
||||
{index.tags &&
|
||||
index.tags.map(({ key: tagKey, name: tagName }) => (
|
||||
<EuiBadge key={tagKey}>{tagName}</EuiBadge>
|
||||
|
|
|
@ -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(
|
||||
<TestProviders store={store}>
|
||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByText('Add integrations')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(sourcererPaths)('shows sourcerer on %s page', (pathname) => {
|
||||
(useLocation as jest.Mock).mockReturnValue({ pathname });
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders store={store}>
|
||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('sourcerer-trigger')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows sourcerer on rule details page', () => {
|
||||
(useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] });
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestProviders store={store}>
|
||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={mockStore}>
|
||||
<GlobalHeader setHeaderActionMenu={mockSetHeaderActionMenu} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(queryByTestId('sourcerer-trigger')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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(<OutPortal node={portalNode} />);
|
||||
|
@ -65,11 +81,14 @@ export const GlobalHeader = React.memo(
|
|||
<EuiHeaderLink
|
||||
color="primary"
|
||||
data-test-subj="add-data"
|
||||
href={prepend(ADD_DATA_PATH)}
|
||||
href={href}
|
||||
iconType="indexOpen"
|
||||
>
|
||||
{BUTTON_ADD_DATA}
|
||||
</EuiHeaderLink>
|
||||
{showSourcerer && !showTimeline && (
|
||||
<Sourcerer scope={sourcererScope} data-test-subj="sourcerer" />
|
||||
)}
|
||||
</EuiHeaderLinks>
|
||||
</EuiHeaderSectionItem>
|
||||
</EuiHeaderSection>
|
||||
|
|
|
@ -9,8 +9,6 @@ import React from 'react';
|
|||
|
||||
import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page';
|
||||
|
||||
const CaseHeaderPageComponent: React.FC<HeaderPageProps> = (props) => (
|
||||
<HeaderPage hideSourcerer={true} {...props} />
|
||||
);
|
||||
const CaseHeaderPageComponent: React.FC<HeaderPageProps> = (props) => <HeaderPage {...props} />;
|
||||
|
||||
export const CaseHeaderPage = React.memo(CaseHeaderPageComponent);
|
||||
|
|
|
@ -33,9 +33,6 @@ exports[`HeaderPage it renders 1`] = `
|
|||
Test supplement
|
||||
</p>
|
||||
</EuiPageHeaderSection>
|
||||
<Sourcerer
|
||||
scope="default"
|
||||
/>
|
||||
</EuiPageHeader>
|
||||
<EuiSpacer
|
||||
size="l"
|
||||
|
|
|
@ -21,8 +21,6 @@ import { Title } from './title';
|
|||
import { DraggableArguments, BadgeOptions, TitleProp } from './types';
|
||||
import { useFormatUrl } from '../link_to';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import { Sourcerer } from '../sourcerer';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
interface HeaderProps {
|
||||
border?: boolean;
|
||||
|
@ -76,8 +74,6 @@ export interface HeaderPageProps extends HeaderProps {
|
|||
badgeOptions?: BadgeOptions;
|
||||
children?: React.ReactNode;
|
||||
draggableArguments?: DraggableArguments;
|
||||
hideSourcerer?: boolean;
|
||||
sourcererScope?: SourcererScopeName;
|
||||
subtitle?: SubtitleProps['items'];
|
||||
subtitle2?: SubtitleProps['items'];
|
||||
title: TitleProp;
|
||||
|
@ -116,9 +112,7 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
|
|||
border,
|
||||
children,
|
||||
draggableArguments,
|
||||
hideSourcerer = false,
|
||||
isLoading,
|
||||
sourcererScope = SourcererScopeName.default,
|
||||
subtitle,
|
||||
subtitle2,
|
||||
title,
|
||||
|
@ -149,7 +143,6 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
|
|||
{children}
|
||||
</EuiPageHeaderSection>
|
||||
)}
|
||||
{!hideSourcerer && <Sourcerer scope={sourcererScope} />}
|
||||
</EuiPageHeader>
|
||||
{/* Manually add a 'padding-bottom' to header */}
|
||||
<EuiSpacer size="l" />
|
||||
|
|
|
@ -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)<EuiFormRowProps & { $expandAdvancedOptions: boolean }>`
|
||||
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<EuiSuperSelectOption<string>> =>
|
||||
isOnlyDetectionAlerts
|
||||
? [
|
||||
{
|
||||
inputDisplay: (
|
||||
<span data-test-subj="security-alerts-option-super">
|
||||
<EuiIcon type="logoSecurity" size="s" /> {i18n.SIEM_SECURITY_DATA_VIEW_LABEL}
|
||||
<StyledBadge data-test-subj="security-alerts-option-badge">
|
||||
{i18n.ALERTS_BADGE_TITLE}
|
||||
</StyledBadge>
|
||||
</span>
|
||||
),
|
||||
value: defaultDataView.id,
|
||||
},
|
||||
]
|
||||
: kibanaDataViews.map(({ title, id }) => ({
|
||||
inputDisplay:
|
||||
id === defaultDataView.id ? (
|
||||
<span data-test-subj="security-option-super">
|
||||
<EuiIcon type="logoSecurity" size="s" /> {i18n.SECURITY_DEFAULT_DATA_VIEW_LABEL}
|
||||
{isModified && id === dataViewId && (
|
||||
<StyledBadge>{i18n.MODIFIED_BADGE_TITLE}</StyledBadge>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span data-test-subj="dataView-option-super">
|
||||
<EuiIcon type="logoKibana" size="s" /> {title}
|
||||
{isModified && id === dataViewId && (
|
||||
<StyledBadge>{i18n.MODIFIED_BADGE_TITLE}</StyledBadge>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
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);
|
|
@ -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<typeof createStore>;
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
|
@ -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', () => {
|
|||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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', () => {
|
|||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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-*'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<SourcererComponentProps>(({ 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<SourcererComponentProps>(({ scope: scopeId }
|
|||
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
|
||||
|
||||
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
|
||||
|
||||
const [dataViewId, setDataViewId] = useState<string>(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<Array<EuiComboBoxOptionOption<string>>>(
|
||||
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<SourcererComponentProps>(({ scope: scopeId }
|
|||
[dispatch, scopeId]
|
||||
);
|
||||
|
||||
const renderOption = useCallback(
|
||||
({ value }) => <span data-test-subj="sourcerer-combo-option">{value}</span>,
|
||||
[]
|
||||
);
|
||||
|
||||
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(
|
||||
() => (
|
||||
<EuiButtonEmpty
|
||||
aria-label={i18n.SOURCERER}
|
||||
data-test-subj="sourcerer-trigger"
|
||||
<StyledButton
|
||||
aria-label={i18n.DATA_VIEW}
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-trigger' : 'sourcerer-trigger'}
|
||||
flush="left"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
isLoading={loading}
|
||||
onClick={setPopoverIsOpenCb}
|
||||
title={i18n.SOURCERER}
|
||||
title={i18n.DATA_VIEW}
|
||||
>
|
||||
{i18n.SOURCERER}
|
||||
</EuiButtonEmpty>
|
||||
{i18n.DATA_VIEW}
|
||||
{isModified === 'modified' && <StyledBadge>{i18n.MODIFIED_BADGE_TITLE}</StyledBadge>}
|
||||
{isModified === 'alerts' && (
|
||||
<StyledBadge data-test-subj="sourcerer-alerts-badge">
|
||||
{i18n.ALERTS_BADGE_TITLE}
|
||||
</StyledBadge>
|
||||
)}
|
||||
</StyledButton>
|
||||
),
|
||||
[setPopoverIsOpenCb, loading]
|
||||
[isTimelineSourcerer, loading, setPopoverIsOpenCb, isModified]
|
||||
);
|
||||
|
||||
const dataViewSelectOptions = useMemo(
|
||||
() =>
|
||||
kibanaDataViews.map(({ title, id }) => ({
|
||||
inputDisplay:
|
||||
id === defaultDataView.id ? (
|
||||
<span data-test-subj="security-option-super">
|
||||
<EuiIcon type="logoSecurity" size="s" /> {i18n.SIEM_DATA_VIEW_LABEL}
|
||||
</span>
|
||||
) : (
|
||||
<span data-test-subj="dataView-option-super">
|
||||
<EuiIcon type="logoKibana" size="s" /> {title}
|
||||
</span>
|
||||
),
|
||||
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<SourcererComponentProps>(({ 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<SourcererComponentProps>(({ scope: scopeId }
|
|||
);
|
||||
}, [trigger, tooltipContent]);
|
||||
|
||||
const onExpandAdvancedOptionsClicked = useCallback(() => {
|
||||
setExpandAdvancedOptions((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
data-test-subj="sourcerer-popover"
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'}
|
||||
button={buttonWithTooptip}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={handleClosePopOver}
|
||||
panelPaddingSize="s"
|
||||
display="block"
|
||||
repositionOnScroll
|
||||
ownFocus
|
||||
>
|
||||
<PopoverContent>
|
||||
<EuiPopoverTitle>
|
||||
<>{i18n.SELECT_INDEX_PATTERNS}</>
|
||||
<EuiPopoverTitle data-test-subj="sourcerer-title">
|
||||
<>{i18n.SELECT_DATA_VIEW}</>
|
||||
</EuiPopoverTitle>
|
||||
{isOnlyDetectionAlerts && (
|
||||
<EuiCallOut
|
||||
data-test-subj="sourcerer-callout"
|
||||
size="s"
|
||||
iconType="iInCircle"
|
||||
title={isTimelineSourcerer ? i18n.CALL_OUT_TIMELINE_TITLE : i18n.CALL_OUT_TITLE}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="default">{i18n.INDEX_PATTERNS_SELECTION_LABEL}</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
fullWidth
|
||||
options={dataViewSelectOptions}
|
||||
valueOfSelected={dataViewId}
|
||||
onChange={onChangeSuper}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiComboBox
|
||||
data-test-subj="sourcerer-combo-box"
|
||||
fullWidth
|
||||
onChange={onChangeCombo}
|
||||
options={selectableOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
renderOption={renderOption}
|
||||
selectedOptions={selectedOptions}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<ResetButton
|
||||
aria-label={i18n.INDEX_PATTERNS_RESET}
|
||||
data-test-subj="sourcerer-reset"
|
||||
flush="left"
|
||||
onClick={resetDataSources}
|
||||
title={i18n.INDEX_PATTERNS_RESET}
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_RESET}
|
||||
</ResetButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={handleSaveIndices}
|
||||
disabled={isSavingDisabled}
|
||||
data-test-subj="sourcerer-save"
|
||||
fill
|
||||
<EuiForm component="form">
|
||||
{isTimelineSourcerer && (
|
||||
<StyledFormRow>
|
||||
<EuiCheckbox
|
||||
id="sourcerer-alert-only-checkbox"
|
||||
data-test-subj="sourcerer-alert-only-checkbox"
|
||||
label={i18n.ALERTS_CHECKBOX_LABEL}
|
||||
checked={isOnlyDetectionAlertsChecked}
|
||||
onChange={onCheckboxChanged}
|
||||
/>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
|
||||
<StyledFormRow label={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
disabled={isOnlyDetectionAlerts}
|
||||
fullWidth
|
||||
size="s"
|
||||
>
|
||||
{i18n.SAVE_INDEX_PATTERNS}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
onChange={onChangeSuper}
|
||||
options={dataViewSelectOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
valueOfSelected={dataViewId}
|
||||
/>
|
||||
</StyledFormRow>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<StyledButton
|
||||
color="text"
|
||||
onClick={onExpandAdvancedOptionsClicked}
|
||||
iconType={expandAdvancedOptions ? 'arrowDown' : 'arrowRight'}
|
||||
data-test-subj="sourcerer-advanced-options-toggle"
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE}
|
||||
</StyledButton>
|
||||
{expandAdvancedOptions && <EuiSpacer size="m" />}
|
||||
<FormRow
|
||||
label={i18n.INDEX_PATTERNS_LABEL}
|
||||
$expandAdvancedOptions={expandAdvancedOptions}
|
||||
helpText={isOnlyDetectionAlerts ? undefined : i18n.INDEX_PATTERNS_DESCRIPTIONS}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="sourcerer-combo-box"
|
||||
fullWidth
|
||||
isDisabled={isOnlyDetectionAlerts}
|
||||
onChange={onChangeCombo}
|
||||
options={selectableOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
renderOption={renderOption}
|
||||
selectedOptions={selectedOptions}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
{!isDetectionsSourcerer && (
|
||||
<StyledFormRow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ResetButton
|
||||
aria-label={i18n.INDEX_PATTERNS_RESET}
|
||||
data-test-subj="sourcerer-reset"
|
||||
flush="left"
|
||||
onClick={resetDataSources}
|
||||
title={i18n.INDEX_PATTERNS_RESET}
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_RESET}
|
||||
</ResetButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={handleSaveIndices}
|
||||
disabled={isSavingDisabled}
|
||||
data-test-subj="sourcerer-save"
|
||||
fill
|
||||
fullWidth
|
||||
size="s"
|
||||
>
|
||||
{i18n.SAVE_INDEX_PATTERNS}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
</EuiForm>
|
||||
</PopoverContent>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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<EuiComboBoxOptionOption<string>>) => void;
|
||||
renderOption: ({ value }: EuiComboBoxOptionOption<string>) => React.ReactElement;
|
||||
selectableOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
selectedOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
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<Array<EuiComboBoxOptionOption<string>>>(
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns)
|
||||
);
|
||||
|
||||
const getDefaultSelectedOptionsByDataView = useCallback(
|
||||
(id: string, isAlerts: boolean = false): Array<EuiComboBoxOptionOption<string>> =>
|
||||
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 }) => <span data-test-subj="sourcerer-combo-option">{value}</span>,
|
||||
[]
|
||||
);
|
||||
|
||||
const setIndexPatternsByDataView = (newSelectedDataViewId: string, isAlerts?: boolean) => {
|
||||
setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts));
|
||||
};
|
||||
|
||||
return {
|
||||
isModified,
|
||||
onChangeCombo,
|
||||
renderOption,
|
||||
selectableOptions,
|
||||
selectedOptions,
|
||||
setIndexPatternsByDataView,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page';
|
||||
|
||||
const DetectionEngineHeaderPageComponent: React.FC<HeaderPageProps> = (props) => (
|
||||
<HeaderPage hideSourcerer={true} {...props} />
|
||||
<HeaderPage {...props} />
|
||||
);
|
||||
|
||||
export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent);
|
||||
|
|
|
@ -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 = () => {
|
|||
<EuiSpacer size="l" />
|
||||
</>
|
||||
)}
|
||||
<Sourcerer scope={SourcererScopeName.default} />
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
|
||||
<SidebarFlexItem grow={false}>
|
||||
<StatefulSidebar />
|
||||
|
|
|
@ -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<Props> = ({
|
|||
<TimelineDatePickerLock />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PickEventType
|
||||
eventType={eventType}
|
||||
onChangeEventTypeAndIndexesName={updateEventTypeAndIndexesName}
|
||||
/>
|
||||
<Sourcerer scope={SourcererScopeName.timeline} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<TimelineHeaderContainer data-test-subj="timelineHeader">
|
||||
|
|
|
@ -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<Props> = ({
|
|||
<TimelineDatePickerLock />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PickEventType
|
||||
eventType={eventType}
|
||||
onChangeEventTypeAndIndexesName={updateEventTypeAndIndexesName}
|
||||
/>
|
||||
<Sourcerer scope={SourcererScopeName.timeline} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<TimelineHeaderContainer data-test-subj="timelineHeader">
|
||||
|
|
|
@ -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;
|
||||
}) => (
|
||||
<div data-test-subj="timeline-sourcerer-tooltip">
|
||||
<span>{tooltipContent}</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
(EuiToolTip as unknown as jest.Mock).mockImplementation(mockTooltip);
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
it('renders', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders>
|
||||
<PickEventType {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store2}>
|
||||
<PickEventType {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders>
|
||||
<PickEventType {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
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(
|
||||
<TestProviders>
|
||||
<PickEventType {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
const tooltip = wrapper.getByTestId('timeline-sourcerer-tooltip');
|
||||
expect(within(tooltip).getByTestId('sourcerer-timeline-trigger')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('correctly filters options', () => {
|
||||
const wrapper = render(
|
||||
<TestProviders store={store}>
|
||||
<PickEventType {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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(
|
||||
<TestProviders store={store}>
|
||||
<PickEventType {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
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('')
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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: (
|
||||
<AllEuiHealth color="subdued">
|
||||
<WarningEuiHealth color="warning">{i18n.ALL_EVENT}</WarningEuiHealth>
|
||||
</AllEuiHealth>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'raw',
|
||||
label: <EuiHealth color="subdued"> {i18n.RAW_EVENT}</EuiHealth>,
|
||||
disabled: !isDefaultPattern,
|
||||
},
|
||||
{
|
||||
id: 'alert',
|
||||
label: <EuiHealth color="warning"> {i18n.DETECTION_ALERTS_EVENT}</EuiHealth>,
|
||||
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<PickEventTypeProps> = ({
|
||||
eventType = 'all',
|
||||
onChangeEventTypeAndIndexesName,
|
||||
}) => {
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
const [showAdvanceSettings, setAdvanceSettings] = useState(eventType === 'custom');
|
||||
const [filterEventType, setFilterEventType] = useState<TimelineEventsType>(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<string>(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<Array<EuiComboBoxOptionOption<string>>>(
|
||||
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<EuiComboBoxOptionOption<string>>) => {
|
||||
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 ? (
|
||||
<span data-test-subj="dataView-option-super">
|
||||
<EuiIcon type="logoSecurity" size="s" /> {SIEM_DATA_VIEW_LABEL}
|
||||
</span>
|
||||
) : (
|
||||
<span data-test-subj="dataView-option-super">
|
||||
<EuiIcon type="logoKibana" size="s" /> {title}
|
||||
</span>
|
||||
),
|
||||
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 (
|
||||
<MyEuiButton
|
||||
data-test-subj="sourcerer-timeline-trigger"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
isLoading={loading}
|
||||
onClick={togglePopover}
|
||||
>
|
||||
{options.find((opt) => opt.id === eventType)?.label}
|
||||
</MyEuiButton>
|
||||
);
|
||||
}, [defaultDataView.id, eventType, dataViewId, loading, togglePopover]);
|
||||
|
||||
const tooltipContent = useMemo(
|
||||
() => (isPopoverOpen ? null : selectedPatterns.sort().join(', ')),
|
||||
[isPopoverOpen, selectedPatterns]
|
||||
);
|
||||
|
||||
const buttonWithTooptip = useMemo(() => {
|
||||
return tooltipContent ? (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={tooltipContent}
|
||||
data-test-subj="timeline-sourcerer-tooltip"
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
}, [button, tooltipContent]);
|
||||
|
||||
const ButtonContent = useMemo(
|
||||
() => (
|
||||
<AdvancedSettings data-test-subj="advanced-settings">
|
||||
{showAdvanceSettings
|
||||
? i18n.HIDE_INDEX_PATTERNS_ADVANCED_SETTINGS
|
||||
: i18n.SHOW_INDEX_PATTERNS_ADVANCED_SETTINGS}
|
||||
</AdvancedSettings>
|
||||
),
|
||||
[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 (
|
||||
<PickEventContainer>
|
||||
<EuiPopover
|
||||
button={buttonWithTooptip}
|
||||
closePopover={closePopover}
|
||||
id="popover"
|
||||
isOpen={isPopoverOpen}
|
||||
repositionOnScroll
|
||||
>
|
||||
<PopoverContent>
|
||||
<EuiPopoverTitle>
|
||||
<>{i18n.SELECT_INDEX_PATTERNS}</>
|
||||
</EuiPopoverTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<Filter
|
||||
data-test-subj="timeline-sourcerer-radio"
|
||||
options={filterOptions}
|
||||
idSelected={filterEventType}
|
||||
onChange={onChangeFilter}
|
||||
name={i18n.SELECT_INDEX_PATTERNS}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiAccordion
|
||||
data-test-subj="sourcerer-accordion"
|
||||
id="accordion1"
|
||||
forceState={showAdvanceSettings ? 'open' : 'closed'}
|
||||
buttonContent={ButtonContent}
|
||||
onToggle={setAdvanceSettings}
|
||||
>
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
fullWidth
|
||||
options={dataViewSelectOptions}
|
||||
valueOfSelected={dataViewId}
|
||||
onChange={onChangeSuper}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiComboBox
|
||||
data-test-subj="timeline-sourcerer"
|
||||
fullWidth
|
||||
onChange={onChangeCombo}
|
||||
options={selectableOptions}
|
||||
placeholder={i18n.PICK_INDEX_PATTERNS}
|
||||
selectedOptions={selectedOptions}
|
||||
/>
|
||||
</>
|
||||
</EuiAccordion>
|
||||
{!showAdvanceSettings && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<ConfigHelper size="s" color="subdued">
|
||||
{i18n.CONFIGURE_INDEX_PATTERNS}
|
||||
</ConfigHelper>
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<ResetButton
|
||||
aria-label={i18n.DATA_SOURCES_RESET}
|
||||
data-test-subj="sourcerer-reset"
|
||||
flush="left"
|
||||
onClick={resetDataSources}
|
||||
title={i18n.DATA_SOURCES_RESET}
|
||||
>
|
||||
{i18n.DATA_SOURCES_RESET}
|
||||
</ResetButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={handleSaveIndices}
|
||||
data-test-subj="sourcerer-save"
|
||||
disabled={isSavingDisabled}
|
||||
fill
|
||||
fullWidth
|
||||
size="s"
|
||||
>
|
||||
{i18n.SAVE_INDEX_PATTERNS}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</PopoverContent>
|
||||
</EuiPopover>
|
||||
</PickEventContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const PickEventType = memo(PickEventTypeComponents);
|
|
@ -46,7 +46,7 @@ export const TimelinesPageComponent: React.FC = () => {
|
|||
{indicesExist ? (
|
||||
<>
|
||||
<SecuritySolutionPageWrapper>
|
||||
<HeaderPage hideSourcerer={true} title={i18n.PAGE_TITLE}>
|
||||
<HeaderPage title={i18n.PAGE_TITLE}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
{capabilitiesCanUserCRUD && (
|
||||
|
@ -93,6 +93,7 @@ export const TimelinesPageComponent: React.FC = () => {
|
|||
</>
|
||||
) : (
|
||||
<SecuritySolutionPageWrapper>
|
||||
<HeaderPage border title={i18n.PAGE_TITLE} />
|
||||
<OverviewEmpty />
|
||||
</SecuritySolutionPageWrapper>
|
||||
)}
|
||||
|
|
|
@ -99,7 +99,6 @@ const UebaDetailsComponent: React.FC<UebaDetailsProps> = ({ detailName, uebaDeta
|
|||
<Display show={!globalFullScreen}>
|
||||
<HeaderPage
|
||||
border
|
||||
sourcererScope={SourcererScopeName.detections}
|
||||
subtitle={
|
||||
<LastEventTime
|
||||
docValueFields={docValueFields}
|
||||
|
|
|
@ -22076,13 +22076,10 @@
|
|||
"xpack.securitySolution.hostsTable.osTitle": "オペレーティングシステム",
|
||||
"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 > 高度な設定で構成できます。",
|
||||
|
|
|
@ -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”>“高级设置”中配置这些索引。",
|
||||
|
|
Loading…
Reference in New Issue