mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
resolve conflicts (#120633)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3de8da3964
commit
3ed46c25e1
52 changed files with 1713 additions and 618 deletions
|
@ -39,7 +39,7 @@ export interface SortColumnTimeline {
|
|||
export interface TimelinePersistInput {
|
||||
columns: ColumnHeaderOptions[];
|
||||
dataProviders?: DataProvider[];
|
||||
dataViewId: string;
|
||||
dataViewId: string | null; // null if legacy pre-8.0 timeline
|
||||
dateRange?: {
|
||||
start: string;
|
||||
end: string;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { SecurityPageName } from '../../../../common/constants';
|
|||
import {
|
||||
createSecuritySolutionStorageMock,
|
||||
mockGlobalState,
|
||||
mockIndexPattern,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
TestProviders,
|
||||
} from '../../../common/mock';
|
||||
|
@ -36,6 +37,10 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/containers/source', () => ({
|
||||
useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }],
|
||||
}));
|
||||
|
||||
jest.mock('react-reverse-portal', () => ({
|
||||
InPortal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
OutPortal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
|
|
|
@ -24,10 +24,10 @@ export const SecuritySolutionBottomBar = React.memo(
|
|||
({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => {
|
||||
const [showTimeline] = useShowTimeline();
|
||||
|
||||
const { indicesExist } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
useResolveRedirect();
|
||||
const { indicesExist, dataViewId } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
return indicesExist && showTimeline ? (
|
||||
useResolveRedirect();
|
||||
return (indicesExist || dataViewId === null) && showTimeline ? (
|
||||
<>
|
||||
<AutoSaveWarningMsg />
|
||||
<Flyout timelineId={TimelineId.active} onAppLeave={onAppLeave} />
|
||||
|
|
|
@ -135,7 +135,7 @@ export const CaseView = React.memo(
|
|||
timelineActions.createTimeline({
|
||||
id: TimelineId.casePage,
|
||||
columns: [],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
indexNames: [],
|
||||
expandedDetail: {},
|
||||
show: false,
|
||||
|
|
|
@ -27,6 +27,10 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar
|
|||
z-index: 9900 !important;
|
||||
min-width: 24px;
|
||||
}
|
||||
.euiPopover__panel.euiPopover__panel-isOpen.sourcererPopoverPanel {
|
||||
// needs to appear under modal
|
||||
z-index: 5900 !important;
|
||||
}
|
||||
.euiToolTip {
|
||||
z-index: 9950 !important;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
EuiFormRow,
|
||||
EuiFormRowProps,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { sourcererModel } from '../../store/sourcerer';
|
||||
|
||||
|
@ -50,11 +50,25 @@ export const PopoverContent = styled.div`
|
|||
|
||||
export const StyledBadge = styled(EuiBadge)`
|
||||
margin-left: 8px;
|
||||
&,
|
||||
.euiBadge__text {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Blockquote = styled.span`
|
||||
${({ theme }) => css`
|
||||
display: block;
|
||||
border-color: ${theme.eui.euiColorDarkShade};
|
||||
border-left: ${theme.eui.euiBorderThick};
|
||||
margin: ${theme.eui.euiSizeS} 0 ${theme.eui.euiSizeS} ${theme.eui.euiSizeS};
|
||||
padding: ${theme.eui.euiSizeS};
|
||||
`}
|
||||
`;
|
||||
|
||||
interface GetDataViewSelectOptionsProps {
|
||||
dataViewId: string;
|
||||
defaultDataView: sourcererModel.KibanaDataView;
|
||||
defaultDataViewId: sourcererModel.KibanaDataView['id'];
|
||||
isModified: boolean;
|
||||
isOnlyDetectionAlerts: boolean;
|
||||
kibanaDataViews: sourcererModel.KibanaDataView[];
|
||||
|
@ -62,7 +76,7 @@ interface GetDataViewSelectOptionsProps {
|
|||
|
||||
export const getDataViewSelectOptions = ({
|
||||
dataViewId,
|
||||
defaultDataView,
|
||||
defaultDataViewId,
|
||||
isModified,
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
|
@ -78,12 +92,12 @@ export const getDataViewSelectOptions = ({
|
|||
</StyledBadge>
|
||||
</span>
|
||||
),
|
||||
value: defaultDataView.id,
|
||||
value: defaultDataViewId,
|
||||
},
|
||||
]
|
||||
: kibanaDataViews.map(({ title, id }) => ({
|
||||
inputDisplay:
|
||||
id === defaultDataView.id ? (
|
||||
id === defaultDataViewId ? (
|
||||
<span data-test-subj="security-option-super">
|
||||
<EuiIcon type="logoSecurity" size="s" /> {i18n.SECURITY_DEFAULT_DATA_VIEW_LABEL}
|
||||
{isModified && id === dataViewId && (
|
||||
|
|
|
@ -19,8 +19,16 @@ import {
|
|||
} from '../../mock';
|
||||
import { createStore } from '../../store';
|
||||
import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
import { useSourcererDataView } from '../../containers/sourcerer';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
jest.mock('../../containers/sourcerer');
|
||||
const mockUseUpdateDataView = jest.fn().mockReturnValue(() => true);
|
||||
jest.mock('./use_update_data_view', () => ({
|
||||
useUpdateDataView: () => mockUseUpdateDataView,
|
||||
}));
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
|
@ -30,6 +38,15 @@ jest.mock('react-redux', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../../src/plugins/kibana_react/public', () => {
|
||||
const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public');
|
||||
|
||||
return {
|
||||
...original,
|
||||
toMountPoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockOptions = [
|
||||
{ label: 'apm-*-transaction*', value: 'apm-*-transaction*' },
|
||||
{ label: 'auditbeat-*', value: 'auditbeat-*' },
|
||||
|
@ -57,12 +74,21 @@ const patternListNoSignals = patternList
|
|||
.filter((p) => p !== mockGlobalState.sourcerer.signalIndexName)
|
||||
.sort();
|
||||
let store: ReturnType<typeof createStore>;
|
||||
const sourcererDataView = {
|
||||
indicesExist: true,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
describe('Sourcerer component', () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
@ -215,7 +241,6 @@ describe('Sourcerer component', () => {
|
|||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
loading: false,
|
||||
selectedDataViewId: '1234',
|
||||
selectedPatterns: ['filebeat-*'],
|
||||
},
|
||||
|
@ -267,7 +292,6 @@ describe('Sourcerer component', () => {
|
|||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
loading: false,
|
||||
selectedDataViewId: id,
|
||||
selectedPatterns: patternListNoSignals.slice(0, 2),
|
||||
},
|
||||
|
@ -313,8 +337,6 @@ describe('Sourcerer component', () => {
|
|||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
loading: false,
|
||||
patternList,
|
||||
selectedDataViewId: id,
|
||||
selectedPatterns: patternList.slice(0, 2),
|
||||
},
|
||||
|
@ -355,7 +377,6 @@ describe('Sourcerer component', () => {
|
|||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
loading: false,
|
||||
selectedDataViewId: id,
|
||||
selectedPatterns: patternListNoSignals.slice(0, 2),
|
||||
},
|
||||
|
@ -629,6 +650,7 @@ describe('timeline sourcerer', () => {
|
|||
};
|
||||
|
||||
beforeAll(() => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView);
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...testProps} />
|
||||
|
@ -713,6 +735,7 @@ describe('timeline sourcerer', () => {
|
|||
};
|
||||
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
|
@ -754,6 +777,7 @@ describe('Sourcerer integration tests', () => {
|
|||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
beforeEach(() => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue(sourcererDataView);
|
||||
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
|
@ -795,11 +819,15 @@ describe('No data', () => {
|
|||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
beforeEach(() => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue({
|
||||
...sourcererDataView,
|
||||
indicesExist: false,
|
||||
});
|
||||
store = createStore(mockNoIndicesState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
test('Hide sourcerer', () => {
|
||||
test('Hide sourcerer - default ', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer {...defaultProps} />
|
||||
|
@ -808,4 +836,123 @@ describe('No data', () => {
|
|||
|
||||
expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false);
|
||||
});
|
||||
test('Hide sourcerer - detections ', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.detections} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="sourcerer-trigger"]`).exists()).toEqual(false);
|
||||
});
|
||||
test('Hide sourcerer - timeline ', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update available', () => {
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const state2 = {
|
||||
...mockGlobalState,
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
kibanaDataViews: [
|
||||
mockGlobalState.sourcerer.defaultDataView,
|
||||
{
|
||||
...mockGlobalState.sourcerer.defaultDataView,
|
||||
id: '1234',
|
||||
title: 'auditbeat-*',
|
||||
patternList: ['auditbeat-*'],
|
||||
},
|
||||
{
|
||||
...mockGlobalState.sourcerer.defaultDataView,
|
||||
id: '12347',
|
||||
title: 'packetbeat-*',
|
||||
patternList: ['packetbeat-*'],
|
||||
},
|
||||
],
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
loading: false,
|
||||
patternList,
|
||||
selectedDataViewId: null,
|
||||
selectedPatterns: ['myFakebeat-*'],
|
||||
missingPatterns: ['myFakebeat-*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue({
|
||||
...sourcererDataView,
|
||||
activePatterns: ['myFakebeat-*'],
|
||||
});
|
||||
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
|
||||
</TestProviders>
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Show Update available label', () => {
|
||||
expect(wrapper.find(`[data-test-subj="sourcerer-deprecated-badge"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Show correct tooltip', () => {
|
||||
expect(wrapper.find(`[data-test-subj="sourcerer-tooltip"]`).prop('content')).toEqual(
|
||||
'myFakebeat-*'
|
||||
);
|
||||
});
|
||||
|
||||
test('Show UpdateDefaultDataViewModal', () => {
|
||||
wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
|
||||
|
||||
wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click');
|
||||
|
||||
expect(wrapper.find(`UpdateDefaultDataViewModal`).prop('isShowing')).toEqual(true);
|
||||
});
|
||||
|
||||
test('Show Add index pattern in UpdateDefaultDataViewModal', () => {
|
||||
wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
|
||||
|
||||
wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click');
|
||||
|
||||
expect(wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).text()).toEqual(
|
||||
'Add index pattern'
|
||||
);
|
||||
});
|
||||
|
||||
test('Set all the index patterns from legacy timeline to sourcerer, after clicking on "Add index pattern"', async () => {
|
||||
wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click');
|
||||
|
||||
wrapper.find(`button[data-test-subj="sourcerer-deprecated-update"]`).first().simulate('click');
|
||||
|
||||
wrapper.find(`button[data-test-subj="sourcerer-update-data-view"]`).simulate('click');
|
||||
|
||||
await waitFor(() => wrapper.update());
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedDataViewId: 'security-solution',
|
||||
selectedPatterns: ['myFakebeat-*'],
|
||||
shouldValidateSelectedPatterns: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,13 +13,12 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiOutsideClickDetector,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiSpacer,
|
||||
EuiSuperSelect,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
|
@ -27,18 +26,13 @@ import * as i18n from './translations';
|
|||
import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import { checkIfIndicesExist } from '../../store/sourcerer/helpers';
|
||||
import { usePickIndexPatterns } from './use_pick_index_patterns';
|
||||
import {
|
||||
FormRow,
|
||||
getDataViewSelectOptions,
|
||||
getTooltipContent,
|
||||
PopoverContent,
|
||||
ResetButton,
|
||||
StyledBadge,
|
||||
StyledButton,
|
||||
StyledFormRow,
|
||||
} from './helpers';
|
||||
import { FormRow, PopoverContent, ResetButton, StyledButton, StyledFormRow } from './helpers';
|
||||
import { TemporarySourcerer } from './temporary';
|
||||
import { UpdateDefaultDataViewModal } from './update_default_data_view_modal';
|
||||
import { useSourcererDataView } from '../../containers/sourcerer';
|
||||
import { useUpdateDataView } from './use_update_data_view';
|
||||
import { Trigger } from './trigger';
|
||||
|
||||
interface SourcererComponentProps {
|
||||
scope: sourcererModel.SourcererScopeName;
|
||||
|
@ -54,13 +48,24 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
defaultDataView,
|
||||
kibanaDataViews,
|
||||
signalIndexName,
|
||||
sourcererScope: { selectedDataViewId, selectedPatterns, loading },
|
||||
sourcererDataView,
|
||||
sourcererScope: {
|
||||
selectedDataViewId,
|
||||
selectedPatterns,
|
||||
missingPatterns: sourcererMissingPatterns,
|
||||
},
|
||||
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
|
||||
const indicesExist = useMemo(
|
||||
() => checkIfIndicesExist({ scopeId, signalIndexName, sourcererDataView }),
|
||||
[scopeId, signalIndexName, sourcererDataView]
|
||||
|
||||
const { activePatterns, indicesExist, loading } = useSourcererDataView(scopeId);
|
||||
const [missingPatterns, setMissingPatterns] = useState<string[]>(
|
||||
activePatterns && activePatterns.length > 0
|
||||
? sourcererMissingPatterns.filter((p) => activePatterns.includes(p))
|
||||
: []
|
||||
);
|
||||
useEffect(() => {
|
||||
if (activePatterns && activePatterns.length > 0) {
|
||||
setMissingPatterns(sourcererMissingPatterns.filter((p) => activePatterns.includes(p)));
|
||||
}
|
||||
}, [activePatterns, sourcererMissingPatterns]);
|
||||
|
||||
const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState(
|
||||
isTimelineSourcerer && selectedPatterns.join() === signalIndexName
|
||||
|
@ -68,15 +73,15 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
|
||||
const isOnlyDetectionAlerts: boolean =
|
||||
isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked);
|
||||
|
||||
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
|
||||
const [dataViewId, setDataViewId] = useState<string>(selectedDataViewId ?? defaultDataView.id);
|
||||
const [dataViewId, setDataViewId] = useState<string | null>(selectedDataViewId);
|
||||
|
||||
const {
|
||||
allOptions,
|
||||
dataViewSelectOptions,
|
||||
isModified,
|
||||
onChangeCombo,
|
||||
renderOption,
|
||||
selectableOptions,
|
||||
selectedOptions,
|
||||
setIndexPatternsByDataView,
|
||||
} = usePickIndexPatterns({
|
||||
|
@ -84,10 +89,12 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
defaultDataViewId: defaultDataView.id,
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
missingPatterns,
|
||||
scopeId,
|
||||
selectedPatterns,
|
||||
signalIndexName,
|
||||
});
|
||||
|
||||
const onCheckboxChanged = useCallback(
|
||||
(e) => {
|
||||
setIsOnlyDetectionAlertsChecked(e.target.checked);
|
||||
|
@ -96,20 +103,26 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
},
|
||||
[defaultDataView.id, setIndexPatternsByDataView]
|
||||
);
|
||||
const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]);
|
||||
|
||||
const [expandAdvancedOptions, setExpandAdvancedOptions] = useState(false);
|
||||
const [isShowingUpdateModal, setIsShowingUpdateModal] = useState(false);
|
||||
|
||||
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[]) => {
|
||||
(
|
||||
newSelectedDataView: string,
|
||||
newSelectedPatterns: string[],
|
||||
shouldValidateSelectedPatterns?: boolean
|
||||
) => {
|
||||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: scopeId,
|
||||
selectedDataViewId: newSelectedDataView,
|
||||
selectedPatterns: newSelectedPatterns,
|
||||
shouldValidateSelectedPatterns,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
@ -128,11 +141,14 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
setDataViewId(defaultDataView.id);
|
||||
setIndexPatternsByDataView(defaultDataView.id);
|
||||
setIsOnlyDetectionAlertsChecked(false);
|
||||
setMissingPatterns([]);
|
||||
}, [defaultDataView.id, setIndexPatternsByDataView]);
|
||||
|
||||
const handleSaveIndices = useCallback(() => {
|
||||
const patterns = selectedOptions.map((so) => so.label);
|
||||
onChangeDataView(dataViewId, patterns);
|
||||
if (dataViewId != null) {
|
||||
onChangeDataView(dataViewId, patterns);
|
||||
}
|
||||
setPopoverIsOpen(false);
|
||||
}, [onChangeDataView, dataViewId, selectedOptions]);
|
||||
|
||||
|
@ -140,183 +156,220 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
|
|||
setPopoverIsOpen(false);
|
||||
setExpandAdvancedOptions(false);
|
||||
}, []);
|
||||
const trigger = useMemo(
|
||||
() => (
|
||||
<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.DATA_VIEW}
|
||||
>
|
||||
{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>
|
||||
),
|
||||
[isTimelineSourcerer, loading, setPopoverIsOpenCb, isModified]
|
||||
);
|
||||
|
||||
const dataViewSelectOptions = useMemo(
|
||||
() =>
|
||||
getDataViewSelectOptions({
|
||||
dataViewId,
|
||||
defaultDataView,
|
||||
isModified: isModified === 'modified',
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
}),
|
||||
[dataViewId, defaultDataView, isModified, isOnlyDetectionAlerts, kibanaDataViews]
|
||||
);
|
||||
// deprecated timeline index pattern handlers
|
||||
const onContinueUpdateDeprecated = useCallback(() => {
|
||||
setIsShowingUpdateModal(false);
|
||||
const patterns = selectedPatterns.filter((pattern) =>
|
||||
defaultDataView.patternList.includes(pattern)
|
||||
);
|
||||
onChangeDataView(defaultDataView.id, patterns);
|
||||
setPopoverIsOpen(false);
|
||||
}, [defaultDataView.id, defaultDataView.patternList, onChangeDataView, selectedPatterns]);
|
||||
|
||||
const onUpdateDeprecated = useCallback(() => {
|
||||
// are all the patterns in the default?
|
||||
if (missingPatterns.length === 0) {
|
||||
onContinueUpdateDeprecated();
|
||||
} else {
|
||||
// open modal
|
||||
setIsShowingUpdateModal(true);
|
||||
}
|
||||
}, [missingPatterns, onContinueUpdateDeprecated]);
|
||||
|
||||
const [isTriggerDisabled, setIsTriggerDisabled] = useState(false);
|
||||
|
||||
const onOpenAndReset = useCallback(() => {
|
||||
setPopoverIsOpen(true);
|
||||
resetDataSources();
|
||||
}, [resetDataSources]);
|
||||
|
||||
const updateDataView = useUpdateDataView(onOpenAndReset);
|
||||
const onUpdateDataView = useCallback(async () => {
|
||||
const isUiSettingsSuccess = await updateDataView(missingPatterns);
|
||||
setIsShowingUpdateModal(false);
|
||||
setPopoverIsOpen(false);
|
||||
|
||||
if (isUiSettingsSuccess) {
|
||||
onChangeDataView(
|
||||
defaultDataView.id,
|
||||
// to be at this stage, activePatterns is defined, the ?? selectedPatterns is to make TS happy
|
||||
activePatterns ?? selectedPatterns,
|
||||
false
|
||||
);
|
||||
setIsTriggerDisabled(true);
|
||||
}
|
||||
}, [
|
||||
activePatterns,
|
||||
defaultDataView.id,
|
||||
missingPatterns,
|
||||
onChangeDataView,
|
||||
selectedPatterns,
|
||||
updateDataView,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataViewId((prevSelectedOption) =>
|
||||
selectedDataViewId != null && !deepEqual(selectedDataViewId, prevSelectedOption)
|
||||
? selectedDataViewId
|
||||
: prevSelectedOption
|
||||
);
|
||||
setDataViewId(selectedDataViewId);
|
||||
}, [selectedDataViewId]);
|
||||
|
||||
const tooltipContent = useMemo(
|
||||
() =>
|
||||
getTooltipContent({
|
||||
isOnlyDetectionAlerts,
|
||||
isPopoverOpen,
|
||||
selectedPatterns,
|
||||
signalIndexName,
|
||||
}),
|
||||
[isPopoverOpen, isOnlyDetectionAlerts, signalIndexName, selectedPatterns]
|
||||
);
|
||||
|
||||
const buttonWithTooptip = useMemo(() => {
|
||||
return tooltipContent ? (
|
||||
<EuiToolTip position="top" content={tooltipContent} data-test-subj="sourcerer-tooltip">
|
||||
{trigger}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
trigger
|
||||
);
|
||||
}, [trigger, tooltipContent]);
|
||||
const onOutsideClick = useCallback(() => {
|
||||
setDataViewId(selectedDataViewId);
|
||||
setMissingPatterns(sourcererMissingPatterns);
|
||||
}, [selectedDataViewId, sourcererMissingPatterns]);
|
||||
|
||||
const onExpandAdvancedOptionsClicked = useCallback(() => {
|
||||
setExpandAdvancedOptions((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
return indicesExist ? (
|
||||
// always show sourcerer in timeline
|
||||
return indicesExist || scopeId === SourcererScopeName.timeline ? (
|
||||
<EuiPopover
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'}
|
||||
button={buttonWithTooptip}
|
||||
isOpen={isPopoverOpen}
|
||||
panelClassName="sourcererPopoverPanel"
|
||||
button={
|
||||
<Trigger
|
||||
activePatterns={activePatterns}
|
||||
disabled={isTriggerDisabled}
|
||||
isModified={isModified}
|
||||
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
|
||||
isPopoverOpen={isPopoverOpen}
|
||||
isTimelineSourcerer={isTimelineSourcerer}
|
||||
loading={loading}
|
||||
onClick={setPopoverIsOpenCb}
|
||||
selectedPatterns={selectedPatterns}
|
||||
signalIndexName={signalIndexName}
|
||||
/>
|
||||
}
|
||||
closePopover={handleClosePopOver}
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-popover' : 'sourcerer-popover'}
|
||||
display="block"
|
||||
repositionOnScroll
|
||||
isOpen={isPopoverOpen}
|
||||
ownFocus
|
||||
repositionOnScroll
|
||||
>
|
||||
<PopoverContent>
|
||||
<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" />
|
||||
<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
|
||||
onChange={onChangeSuper}
|
||||
options={dataViewSelectOptions}
|
||||
placeholder={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}
|
||||
valueOfSelected={dataViewId}
|
||||
<EuiOutsideClickDetector onOutsideClick={onOutsideClick}>
|
||||
<PopoverContent>
|
||||
<EuiPopoverTitle data-test-subj="sourcerer-title">
|
||||
<>{i18n.SELECT_DATA_VIEW}</>
|
||||
</EuiPopoverTitle>
|
||||
{isOnlyDetectionAlerts && (
|
||||
<EuiCallOut
|
||||
data-test-subj="sourcerer-callout"
|
||||
iconType="iInCircle"
|
||||
size="s"
|
||||
title={isTimelineSourcerer ? i18n.CALL_OUT_TIMELINE_TITLE : i18n.CALL_OUT_TITLE}
|
||||
/>
|
||||
</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>
|
||||
{isModified === 'deprecated' || isModified === 'missingPatterns' ? (
|
||||
<>
|
||||
<TemporarySourcerer
|
||||
activePatterns={activePatterns}
|
||||
indicesExist={indicesExist}
|
||||
isModified={isModified}
|
||||
missingPatterns={missingPatterns}
|
||||
onClick={resetDataSources}
|
||||
onClose={setPopoverIsOpenCb}
|
||||
onUpdate={isModified === 'deprecated' ? onUpdateDeprecated : onUpdateDataView}
|
||||
selectedPatterns={selectedPatterns}
|
||||
/>
|
||||
<UpdateDefaultDataViewModal
|
||||
isShowing={isShowingUpdateModal}
|
||||
missingPatterns={missingPatterns}
|
||||
onClose={() => setIsShowingUpdateModal(false)}
|
||||
onContinue={onContinueUpdateDeprecated}
|
||||
onUpdate={onUpdateDataView}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EuiForm component="form">
|
||||
<>
|
||||
{isTimelineSourcerer && (
|
||||
<StyledFormRow>
|
||||
<EuiCheckbox
|
||||
checked={isOnlyDetectionAlertsChecked}
|
||||
data-test-subj="sourcerer-alert-only-checkbox"
|
||||
id="sourcerer-alert-only-checkbox"
|
||||
label={i18n.ALERTS_CHECKBOX_LABEL}
|
||||
onChange={onCheckboxChanged}
|
||||
/>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
{dataViewId && (
|
||||
<StyledFormRow label={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="sourcerer-select"
|
||||
disabled={isOnlyDetectionAlerts}
|
||||
fullWidth
|
||||
onChange={onChangeSuper}
|
||||
options={dataViewSelectOptions}
|
||||
placeholder={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}
|
||||
valueOfSelected={dataViewId}
|
||||
/>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<StyledButton
|
||||
color="text"
|
||||
data-test-subj="sourcerer-advanced-options-toggle"
|
||||
iconType={expandAdvancedOptions ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={onExpandAdvancedOptionsClicked}
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE}
|
||||
</StyledButton>
|
||||
{expandAdvancedOptions && <EuiSpacer size="m" />}
|
||||
<FormRow
|
||||
$expandAdvancedOptions={expandAdvancedOptions}
|
||||
helpText={isOnlyDetectionAlerts ? undefined : i18n.INDEX_PATTERNS_DESCRIPTIONS}
|
||||
label={i18n.INDEX_PATTERNS_LABEL}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="sourcerer-combo-box"
|
||||
fullWidth
|
||||
isDisabled={isOnlyDetectionAlerts}
|
||||
onChange={onChangeCombo}
|
||||
options={allOptions}
|
||||
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={selectedOptions.length === 0}
|
||||
data-test-subj="sourcerer-save"
|
||||
fill
|
||||
fullWidth
|
||||
size="s"
|
||||
>
|
||||
{i18n.SAVE_INDEX_PATTERNS}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StyledFormRow>
|
||||
)}
|
||||
</>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiForm>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</EuiOutsideClickDetector>
|
||||
</EuiPopover>
|
||||
) : null;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RELOAD_PAGE_TITLE } from './translations';
|
||||
|
||||
const StyledRefreshButton = styled(EuiButton)`
|
||||
float: right;
|
||||
`;
|
||||
|
||||
export const RefreshButton = React.memo(() => {
|
||||
const onPageRefresh = useCallback(() => {
|
||||
document.location.reload();
|
||||
}, []);
|
||||
return (
|
||||
<StyledRefreshButton onClick={onPageRefresh} data-test-subj="page-refresh">
|
||||
{RELOAD_PAGE_TITLE}
|
||||
</StyledRefreshButton>
|
||||
);
|
||||
});
|
||||
|
||||
RefreshButton.displayName = 'RefreshButton';
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import * as i18n from './translations';
|
||||
import { Blockquote, ResetButton } from './helpers';
|
||||
|
||||
interface Props {
|
||||
activePatterns?: string[];
|
||||
indicesExist: boolean;
|
||||
isModified: 'deprecated' | 'missingPatterns';
|
||||
missingPatterns: string[];
|
||||
onClick: () => void;
|
||||
onClose: () => void;
|
||||
onUpdate: () => void;
|
||||
selectedPatterns: string[];
|
||||
}
|
||||
|
||||
const translations = {
|
||||
deprecated: {
|
||||
title: i18n.CALL_OUT_DEPRECATED_TITLE,
|
||||
update: i18n.UPDATE_INDEX_PATTERNS,
|
||||
},
|
||||
missingPatterns: {
|
||||
title: i18n.CALL_OUT_MISSING_PATTERNS_TITLE,
|
||||
update: i18n.ADD_INDEX_PATTERN,
|
||||
},
|
||||
};
|
||||
|
||||
export const TemporarySourcerer = React.memo<Props>(
|
||||
({
|
||||
activePatterns,
|
||||
indicesExist,
|
||||
isModified,
|
||||
onClose,
|
||||
onClick,
|
||||
onUpdate,
|
||||
selectedPatterns,
|
||||
missingPatterns,
|
||||
}) => {
|
||||
const trigger = useMemo(
|
||||
() => (
|
||||
<EuiButton
|
||||
data-test-subj="sourcerer-deprecated-update"
|
||||
fill
|
||||
fullWidth
|
||||
onClick={onUpdate}
|
||||
size="s"
|
||||
disabled={!indicesExist}
|
||||
>
|
||||
{translations[isModified].update}
|
||||
</EuiButton>
|
||||
),
|
||||
[indicesExist, isModified, onUpdate]
|
||||
);
|
||||
const buttonWithTooltip = useMemo(
|
||||
() =>
|
||||
!indicesExist ? (
|
||||
<EuiToolTip position="top" content={i18n.NO_DATA} data-test-subj="sourcerer-tooltip">
|
||||
{trigger}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
trigger
|
||||
),
|
||||
[indicesExist, trigger]
|
||||
);
|
||||
|
||||
const deadPatterns =
|
||||
activePatterns && activePatterns.length > 0
|
||||
? selectedPatterns.filter((p) => !activePatterns.includes(p))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
data-test-subj="sourcerer-deprecated-callout"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
title={translations[isModified].title}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<EuiTextColor color="subdued">
|
||||
<p>
|
||||
{activePatterns && activePatterns.length > 0 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.currentPatterns"
|
||||
defaultMessage="The active index patterns in this timeline are{tooltip}: {callout}"
|
||||
values={{
|
||||
tooltip:
|
||||
deadPatterns.length > 0 ? (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.noMatchData"
|
||||
defaultMessage="The following index patterns are saved to this timeline but do not match any data streams, indices, or index aliases: {aliases}"
|
||||
values={{
|
||||
aliases: selectedPatterns
|
||||
.filter((p) => !activePatterns.includes(p))
|
||||
.join(', '),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiIcon type="questionInCircle" title={i18n.INACTIVE_PATTERNS} />
|
||||
</EuiToolTip>
|
||||
) : null,
|
||||
callout: <Blockquote>{activePatterns.join(', ')}</Blockquote>,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.currentPatternsBad"
|
||||
defaultMessage="The current index patterns in this timeline are: {callout}"
|
||||
values={{
|
||||
callout: <Blockquote>{selectedPatterns.join(', ')}</Blockquote>,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isModified === 'deprecated' && (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.toggleToNewSourcerer"
|
||||
defaultMessage="We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can recreate your temporary data view with the new data view selector. You can also manually select a data view {link}."
|
||||
values={{
|
||||
link: <EuiLink onClick={onClick}>{i18n.TOGGLE_TO_NEW_SOURCERER}</EuiLink>,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isModified === 'missingPatterns' && (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.missingPatterns.callout"
|
||||
defaultMessage="Security Data View is missing the following index patterns: {callout}"
|
||||
values={{
|
||||
callout: <Blockquote>{missingPatterns.join(', ')}</Blockquote>,
|
||||
}}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.missingPatterns.description"
|
||||
defaultMessage="We have preserved your timeline by creating a temporary data view. If you'd like to modify your data, we can add the missing index patterns to the Security Data View. You can also manually select a data view {link}."
|
||||
values={{
|
||||
link: <EuiLink onClick={onClick}>{i18n.TOGGLE_TO_NEW_SOURCERER}</EuiLink>,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ResetButton
|
||||
aria-label={i18n.INDEX_PATTERNS_CLOSE}
|
||||
data-test-subj="sourcerer-deprecated-close"
|
||||
flush="left"
|
||||
onClick={onClose}
|
||||
title={i18n.INDEX_PATTERNS_CLOSE}
|
||||
>
|
||||
{i18n.INDEX_PATTERNS_CLOSE}
|
||||
</ResetButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{buttonWithTooltip}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TemporarySourcerer.displayName = 'TemporarySourcerer';
|
|
@ -11,6 +11,20 @@ export const CALL_OUT_TITLE = i18n.translate('xpack.securitySolution.indexPatter
|
|||
defaultMessage: 'Data view cannot be modified on this page',
|
||||
});
|
||||
|
||||
export const CALL_OUT_DEPRECATED_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.callOutDeprecxatedTitle',
|
||||
{
|
||||
defaultMessage: 'This timeline uses a legacy data view selector',
|
||||
}
|
||||
);
|
||||
|
||||
export const CALL_OUT_MISSING_PATTERNS_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.callOutMissingPatternsTitle',
|
||||
{
|
||||
defaultMessage: 'This timeline is out of date with the Security Data View',
|
||||
}
|
||||
);
|
||||
|
||||
export const CALL_OUT_TIMELINE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.callOutTimelineTitle',
|
||||
{
|
||||
|
@ -18,9 +32,42 @@ export const CALL_OUT_TIMELINE_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const TOGGLE_TO_NEW_SOURCERER = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.toggleToNewSourcerer.link',
|
||||
{
|
||||
defaultMessage: 'here',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_VIEW = i18n.translate('xpack.securitySolution.indexPatterns.dataViewLabel', {
|
||||
defaultMessage: 'Data view',
|
||||
});
|
||||
|
||||
export const UPDATE_DATA_VIEW = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.updateDataView',
|
||||
{
|
||||
defaultMessage:
|
||||
'Would you like to add this index pattern to Security Data View? Otherwise, we can recreate the data view without the missing index patterns.',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATE_SECURITY_DATA_VIEW = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.updateSecurityDataView',
|
||||
{
|
||||
defaultMessage: 'Update Security Data View',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONTINUE_WITHOUT_ADDING = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.continue',
|
||||
{
|
||||
defaultMessage: 'Continue without adding',
|
||||
}
|
||||
);
|
||||
export const ADD_INDEX_PATTERN = i18n.translate('xpack.securitySolution.indexPatterns.add', {
|
||||
defaultMessage: 'Add index pattern',
|
||||
});
|
||||
|
||||
export const MODIFIED_BADGE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.modifiedBadgeTitle',
|
||||
{
|
||||
|
@ -35,6 +82,13 @@ export const ALERTS_BADGE_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const DEPRECATED_BADGE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.updateAvailableBadgeTitle',
|
||||
{
|
||||
defaultMessage: 'Update available',
|
||||
}
|
||||
);
|
||||
|
||||
export const SECURITY_DEFAULT_DATA_VIEW_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.securityDefaultDataViewLabel',
|
||||
{
|
||||
|
@ -97,6 +151,14 @@ export const DISABLED_INDEX_PATTERNS = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const DISABLED_SOURCERER = i18n.translate('xpack.securitySolution.sourcerer.disabled', {
|
||||
defaultMessage: 'The updates to the Data view require a page reload to take effect.',
|
||||
});
|
||||
|
||||
export const UPDATE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.update', {
|
||||
defaultMessage: 'Update and recreate data view',
|
||||
});
|
||||
|
||||
export const INDEX_PATTERNS_RESET = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.resetButton',
|
||||
{
|
||||
|
@ -104,6 +166,22 @@ export const INDEX_PATTERNS_RESET = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const INDEX_PATTERNS_CLOSE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.closeButton',
|
||||
{
|
||||
defaultMessage: 'Close',
|
||||
}
|
||||
);
|
||||
|
||||
export const INACTIVE_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.inactive', {
|
||||
defaultMessage: 'Inactive index patterns',
|
||||
});
|
||||
|
||||
export const NO_DATA = i18n.translate('xpack.securitySolution.indexPatterns.noData', {
|
||||
defaultMessage:
|
||||
"The index pattern on this timeline doesn't match any data streams, indices, or index aliases.",
|
||||
});
|
||||
|
||||
export const PICK_INDEX_PATTERNS = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.pickIndexPatternsCombo',
|
||||
{
|
||||
|
@ -117,3 +195,24 @@ export const ALERTS_CHECKBOX_LABEL = i18n.translate(
|
|||
defaultMessage: 'Show only detection alerts',
|
||||
}
|
||||
);
|
||||
|
||||
export const SUCCESS_TOAST_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.successToastTitle',
|
||||
{
|
||||
defaultMessage: 'One or more settings require you to reload the page to take effect',
|
||||
}
|
||||
);
|
||||
|
||||
export const RELOAD_PAGE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.reloadPageTitle',
|
||||
{
|
||||
defaultMessage: 'Reload page',
|
||||
}
|
||||
);
|
||||
|
||||
export const FAILURE_TOAST_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.indexPatterns.failureToastTitle',
|
||||
{
|
||||
defaultMessage: 'Unable to update data view',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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, { FC, memo, useMemo } from 'react';
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
import { getTooltipContent, StyledBadge, StyledButton } from './helpers';
|
||||
import { ModifiedTypes } from './use_pick_index_patterns';
|
||||
|
||||
interface Props {
|
||||
activePatterns?: string[];
|
||||
disabled: boolean;
|
||||
isModified: ModifiedTypes;
|
||||
isOnlyDetectionAlerts: boolean;
|
||||
isPopoverOpen: boolean;
|
||||
isTimelineSourcerer: boolean;
|
||||
loading: boolean;
|
||||
onClick: () => void;
|
||||
selectedPatterns: string[];
|
||||
signalIndexName: string | null;
|
||||
}
|
||||
export const TriggerComponent: FC<Props> = ({
|
||||
activePatterns,
|
||||
disabled,
|
||||
isModified,
|
||||
isOnlyDetectionAlerts,
|
||||
isPopoverOpen,
|
||||
isTimelineSourcerer,
|
||||
loading,
|
||||
onClick,
|
||||
selectedPatterns,
|
||||
signalIndexName,
|
||||
}) => {
|
||||
const badge = useMemo(() => {
|
||||
switch (isModified) {
|
||||
case 'modified':
|
||||
return <StyledBadge>{i18n.MODIFIED_BADGE_TITLE}</StyledBadge>;
|
||||
case 'alerts':
|
||||
return (
|
||||
<StyledBadge data-test-subj="sourcerer-alerts-badge">
|
||||
{i18n.ALERTS_BADGE_TITLE}
|
||||
</StyledBadge>
|
||||
);
|
||||
case 'deprecated':
|
||||
return (
|
||||
<StyledBadge color="warning" data-test-subj="sourcerer-deprecated-badge">
|
||||
{i18n.DEPRECATED_BADGE_TITLE}
|
||||
</StyledBadge>
|
||||
);
|
||||
case 'missingPatterns':
|
||||
return (
|
||||
<StyledBadge color="warning" data-test-subj="sourcerer-missingPatterns-badge">
|
||||
{i18n.DEPRECATED_BADGE_TITLE}
|
||||
</StyledBadge>
|
||||
);
|
||||
case '':
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [isModified]);
|
||||
|
||||
const trigger = useMemo(
|
||||
() => (
|
||||
<StyledButton
|
||||
aria-label={i18n.DATA_VIEW}
|
||||
data-test-subj={isTimelineSourcerer ? 'timeline-sourcerer-trigger' : 'sourcerer-trigger'}
|
||||
flush="left"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
disabled={disabled}
|
||||
isLoading={loading}
|
||||
onClick={onClick}
|
||||
title={i18n.DATA_VIEW}
|
||||
>
|
||||
{i18n.DATA_VIEW}
|
||||
{!disabled && badge}
|
||||
</StyledButton>
|
||||
),
|
||||
[disabled, badge, isTimelineSourcerer, loading, onClick]
|
||||
);
|
||||
|
||||
const tooltipContent = useMemo(
|
||||
() =>
|
||||
disabled
|
||||
? i18n.DISABLED_SOURCERER
|
||||
: getTooltipContent({
|
||||
isOnlyDetectionAlerts,
|
||||
isPopoverOpen,
|
||||
// if activePatterns, use because we are in the temporary sourcerer state
|
||||
selectedPatterns: activePatterns ?? selectedPatterns,
|
||||
signalIndexName,
|
||||
}),
|
||||
[
|
||||
activePatterns,
|
||||
disabled,
|
||||
isOnlyDetectionAlerts,
|
||||
isPopoverOpen,
|
||||
selectedPatterns,
|
||||
signalIndexName,
|
||||
]
|
||||
);
|
||||
|
||||
return tooltipContent ? (
|
||||
<EuiToolTip position="top" content={tooltipContent} data-test-subj="sourcerer-tooltip">
|
||||
{trigger}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
trigger
|
||||
);
|
||||
};
|
||||
|
||||
export const Trigger = memo(TriggerComponent);
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from './translations';
|
||||
import { Blockquote, ResetButton } from './helpers';
|
||||
|
||||
interface Props {
|
||||
isShowing: boolean;
|
||||
missingPatterns: string[];
|
||||
onClose: () => void;
|
||||
onContinue: () => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
const MyEuiModal = styled(EuiModal)`
|
||||
.euiModal__flex {
|
||||
width: 60vw;
|
||||
}
|
||||
.euiCodeBlock {
|
||||
height: auto !important;
|
||||
max-width: 718px;
|
||||
}
|
||||
z-index: 99999999;
|
||||
`;
|
||||
|
||||
export const UpdateDefaultDataViewModal = React.memo<Props>(
|
||||
({ isShowing, onClose, onContinue, onUpdate, missingPatterns }) =>
|
||||
isShowing ? (
|
||||
<MyEuiModal onClose={onClose} data-test-subj="sourcerer-update-data-view-modal">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<h1>{i18n.UPDATE_SECURITY_DATA_VIEW}</h1>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiText size="s">
|
||||
<EuiTextColor color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.missingPatterns"
|
||||
defaultMessage="Security Data View is missing the following index patterns in order to recreate the previous timeline's data view: {callout}"
|
||||
values={{
|
||||
callout: <Blockquote>{missingPatterns.join(', ')}</Blockquote>,
|
||||
}}
|
||||
/>
|
||||
{i18n.UPDATE_DATA_VIEW}
|
||||
</p>
|
||||
</EuiTextColor>
|
||||
</EuiText>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ResetButton
|
||||
aria-label={i18n.CONTINUE_WITHOUT_ADDING}
|
||||
data-test-subj="sourcerer-continue-close"
|
||||
flush="left"
|
||||
onClick={onContinue}
|
||||
title={i18n.CONTINUE_WITHOUT_ADDING}
|
||||
>
|
||||
{i18n.CONTINUE_WITHOUT_ADDING}
|
||||
</ResetButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="sourcerer-update-data-view"
|
||||
fill
|
||||
fullWidth
|
||||
onClick={onUpdate}
|
||||
size="s"
|
||||
>
|
||||
{i18n.ADD_INDEX_PATTERN}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
</MyEuiModal>
|
||||
) : null
|
||||
);
|
||||
|
||||
UpdateDefaultDataViewModal.displayName = 'UpdateDefaultDataViewModal';
|
|
@ -6,29 +6,31 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiComboBoxOptionOption, EuiSuperSelectOption } from '@elastic/eui';
|
||||
import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
|
||||
import { sourcererModel } from '../../store/sourcerer';
|
||||
import { getPatternListWithoutSignals } from './helpers';
|
||||
import { getDataViewSelectOptions, getPatternListWithoutSignals } from './helpers';
|
||||
import { SourcererScopeName } from '../../store/sourcerer/model';
|
||||
|
||||
interface UsePickIndexPatternsProps {
|
||||
dataViewId: string;
|
||||
dataViewId: string | null;
|
||||
defaultDataViewId: string;
|
||||
isOnlyDetectionAlerts: boolean;
|
||||
kibanaDataViews: sourcererModel.SourcererModel['kibanaDataViews'];
|
||||
missingPatterns: string[];
|
||||
scopeId: sourcererModel.SourcererScopeName;
|
||||
selectedPatterns: string[];
|
||||
signalIndexName: string | null;
|
||||
}
|
||||
|
||||
export type ModifiedTypes = 'modified' | 'alerts' | '';
|
||||
export type ModifiedTypes = 'modified' | 'alerts' | 'deprecated' | 'missingPatterns' | '';
|
||||
|
||||
interface UsePickIndexPatterns {
|
||||
allOptions: Array<EuiComboBoxOptionOption<string>>;
|
||||
dataViewSelectOptions: Array<EuiSuperSelectOption<string>>;
|
||||
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;
|
||||
}
|
||||
|
@ -45,6 +47,7 @@ export const usePickIndexPatterns = ({
|
|||
defaultDataViewId,
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
missingPatterns,
|
||||
scopeId,
|
||||
selectedPatterns,
|
||||
signalIndexName,
|
||||
|
@ -54,42 +57,44 @@ export const usePickIndexPatterns = ({
|
|||
[signalIndexName]
|
||||
);
|
||||
|
||||
const { patternList, selectablePatterns } = useMemo(() => {
|
||||
const { allPatterns, selectablePatterns } = useMemo<{
|
||||
allPatterns: string[];
|
||||
selectablePatterns: string[];
|
||||
}>(() => {
|
||||
if (isOnlyDetectionAlerts && signalIndexName) {
|
||||
return {
|
||||
patternList: [signalIndexName],
|
||||
allPatterns: [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: [] };
|
||||
|
||||
if (theDataView == null) {
|
||||
return {
|
||||
allPatterns: [],
|
||||
selectablePatterns: [],
|
||||
};
|
||||
}
|
||||
|
||||
const titleAsList = [...new Set(theDataView.title.split(','))];
|
||||
|
||||
return scopeId === sourcererModel.SourcererScopeName.default
|
||||
? {
|
||||
allPatterns: getPatternListWithoutSignals(titleAsList, signalIndexName),
|
||||
selectablePatterns: getPatternListWithoutSignals(
|
||||
theDataView.patternList,
|
||||
signalIndexName
|
||||
),
|
||||
}
|
||||
: {
|
||||
allPatterns: titleAsList,
|
||||
selectablePatterns: theDataView.patternList,
|
||||
};
|
||||
}, [dataViewId, isOnlyDetectionAlerts, kibanaDataViews, scopeId, signalIndexName]);
|
||||
|
||||
const selectableOptions = useMemo(
|
||||
() => patternListToOptions(patternList, selectablePatterns),
|
||||
[patternList, selectablePatterns]
|
||||
const allOptions = useMemo(
|
||||
() => patternListToOptions(allPatterns, selectablePatterns),
|
||||
[allPatterns, selectablePatterns]
|
||||
);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
|
||||
isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns)
|
||||
|
@ -111,37 +116,50 @@ export const usePickIndexPatterns = ({
|
|||
);
|
||||
|
||||
const defaultSelectedPatternsAsOptions = useMemo(
|
||||
() => getDefaultSelectedOptionsByDataView(dataViewId),
|
||||
() => (dataViewId != null ? getDefaultSelectedOptionsByDataView(dataViewId) : []),
|
||||
[dataViewId, getDefaultSelectedOptionsByDataView]
|
||||
);
|
||||
|
||||
const [isModified, setIsModified] = useState<'modified' | 'alerts' | ''>('');
|
||||
const [isModified, setIsModified] = useState<ModifiedTypes>(
|
||||
dataViewId == null ? 'deprecated' : missingPatterns.length > 0 ? 'missingPatterns' : ''
|
||||
);
|
||||
const onSetIsModified = useCallback(
|
||||
(patterns?: string[]) => {
|
||||
(patterns: string[], id: string | null) => {
|
||||
if (id == null) {
|
||||
return setIsModified('deprecated');
|
||||
}
|
||||
if (missingPatterns.length > 0) {
|
||||
return setIsModified('missingPatterns');
|
||||
}
|
||||
if (isOnlyDetectionAlerts) {
|
||||
return setIsModified('alerts');
|
||||
}
|
||||
const modifiedPatterns = patterns != null ? patterns : selectedPatterns;
|
||||
const isPatternsModified =
|
||||
defaultSelectedPatternsAsOptions.length !== modifiedPatterns.length ||
|
||||
defaultSelectedPatternsAsOptions.length !== patterns.length ||
|
||||
!defaultSelectedPatternsAsOptions.every((option) =>
|
||||
modifiedPatterns.find((pattern) => option.value === pattern)
|
||||
patterns.find((pattern) => option.value === pattern)
|
||||
);
|
||||
return setIsModified(isPatternsModified ? 'modified' : '');
|
||||
},
|
||||
[defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, selectedPatterns]
|
||||
[defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, missingPatterns.length]
|
||||
);
|
||||
|
||||
// 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]);
|
||||
}, [selectedPatterns, scopeId]);
|
||||
// when scope updates, check modified to set/remove alerts label
|
||||
useEffect(() => {
|
||||
onSetIsModified(
|
||||
selectedOptions.map(({ label }) => label),
|
||||
dataViewId
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataViewId, missingPatterns, scopeId, selectedOptions]);
|
||||
|
||||
const onChangeCombo = useCallback((newSelectedOptions) => {
|
||||
setSelectedOptions(newSelectedOptions);
|
||||
|
@ -156,11 +174,26 @@ export const usePickIndexPatterns = ({
|
|||
setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts));
|
||||
};
|
||||
|
||||
const dataViewSelectOptions = useMemo(
|
||||
() =>
|
||||
dataViewId != null
|
||||
? getDataViewSelectOptions({
|
||||
dataViewId,
|
||||
defaultDataViewId,
|
||||
isModified: isModified === 'modified',
|
||||
isOnlyDetectionAlerts,
|
||||
kibanaDataViews,
|
||||
})
|
||||
: [],
|
||||
[dataViewId, defaultDataViewId, isModified, isOnlyDetectionAlerts, kibanaDataViews]
|
||||
);
|
||||
|
||||
return {
|
||||
allOptions,
|
||||
dataViewSelectOptions,
|
||||
isModified,
|
||||
onChangeCombo,
|
||||
renderOption,
|
||||
selectableOptions,
|
||||
selectedOptions,
|
||||
setIndexPatternsByDataView,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { useUpdateDataView } from './use_update_data_view';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import * as i18n from './translations';
|
||||
const mockAddSuccess = jest.fn();
|
||||
const mockAddError = jest.fn();
|
||||
const mockSet = jest.fn();
|
||||
const mockPatterns = ['packetbeat-*', 'winlogbeat-*'];
|
||||
jest.mock('../../hooks/use_app_toasts', () => {
|
||||
const original = jest.requireActual('../../hooks/use_app_toasts');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useAppToasts: () => ({
|
||||
addSuccess: mockAddSuccess,
|
||||
addError: mockAddError,
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../lib/kibana');
|
||||
jest.mock('../../../../../../../src/plugins/kibana_react/public', () => {
|
||||
const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public');
|
||||
|
||||
return {
|
||||
...original,
|
||||
toMountPoint: jest.fn(),
|
||||
};
|
||||
});
|
||||
describe('use_update_data_view', () => {
|
||||
const mockError = jest.fn();
|
||||
beforeEach(() => {
|
||||
(useKibana as jest.Mock).mockImplementation(() => ({
|
||||
services: {
|
||||
uiSettings: {
|
||||
get: () => mockPatterns,
|
||||
set: mockSet.mockResolvedValue(true),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Successful uiSettings updates with correct index pattern, and shows success toast', async () => {
|
||||
const { result } = renderHook(() => useUpdateDataView(mockError));
|
||||
const updateDataView = result.current;
|
||||
const isUiSettingsSuccess = await updateDataView(['missing-*']);
|
||||
expect(mockSet.mock.calls[0][1]).toEqual([...mockPatterns, 'missing-*'].sort());
|
||||
expect(isUiSettingsSuccess).toEqual(true);
|
||||
expect(mockAddSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Failed uiSettings update returns false and shows error toast', async () => {
|
||||
(useKibana as jest.Mock).mockImplementation(() => ({
|
||||
services: {
|
||||
uiSettings: {
|
||||
get: () => mockPatterns,
|
||||
set: mockSet.mockResolvedValue(false),
|
||||
},
|
||||
},
|
||||
}));
|
||||
const { result } = renderHook(() => useUpdateDataView(mockError));
|
||||
const updateDataView = result.current;
|
||||
const isUiSettingsSuccess = await updateDataView(['missing-*']);
|
||||
expect(mockSet.mock.calls[0][1]).toEqual([...mockPatterns, 'missing-*'].sort());
|
||||
expect(isUiSettingsSuccess).toEqual(false);
|
||||
expect(mockAddError).toHaveBeenCalled();
|
||||
expect(mockAddError.mock.calls[0][0]).toEqual(new Error(i18n.FAILURE_TOAST_TITLE));
|
||||
});
|
||||
|
||||
test('Failed uiSettings throws error and shows error toast', async () => {
|
||||
(useKibana as jest.Mock).mockImplementation(() => ({
|
||||
services: {
|
||||
uiSettings: {
|
||||
get: jest.fn().mockImplementation(() => {
|
||||
throw new Error('Uh oh bad times over here');
|
||||
}),
|
||||
set: mockSet.mockResolvedValue(true),
|
||||
},
|
||||
},
|
||||
}));
|
||||
const { result } = renderHook(() => useUpdateDataView(mockError));
|
||||
const updateDataView = result.current;
|
||||
const isUiSettingsSuccess = await updateDataView(['missing-*']);
|
||||
expect(isUiSettingsSuccess).toEqual(false);
|
||||
expect(mockAddError).toHaveBeenCalled();
|
||||
expect(mockAddError.mock.calls[0][0]).toEqual(new Error('Uh oh bad times over here'));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
|
||||
import { ensurePatternFormat } from '../../store/sourcerer/helpers';
|
||||
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import * as i18n from './translations';
|
||||
import { RefreshButton } from './refresh_button';
|
||||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
|
||||
export const useUpdateDataView = (
|
||||
onOpenAndReset: () => void
|
||||
): ((missingPatterns: string[]) => Promise<boolean>) => {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const { addSuccess, addError } = useAppToasts();
|
||||
return useCallback(
|
||||
async (missingPatterns: string[]): Promise<boolean> => {
|
||||
const asyncSearch = async (): Promise<[boolean, Error | null]> => {
|
||||
try {
|
||||
const defaultPatterns = uiSettings.get<string[]>(DEFAULT_INDEX_KEY);
|
||||
const uiSettingsIndexPattern = [...defaultPatterns, ...missingPatterns];
|
||||
const isSuccess = await uiSettings.set(
|
||||
DEFAULT_INDEX_KEY,
|
||||
ensurePatternFormat(uiSettingsIndexPattern)
|
||||
);
|
||||
return [isSuccess, null];
|
||||
} catch (e) {
|
||||
return [false, e];
|
||||
}
|
||||
};
|
||||
const [isUiSettingsSuccess, possibleError] = await asyncSearch();
|
||||
if (isUiSettingsSuccess) {
|
||||
addSuccess({
|
||||
color: 'success',
|
||||
title: toMountPoint(i18n.SUCCESS_TOAST_TITLE),
|
||||
text: toMountPoint(<RefreshButton />),
|
||||
iconType: undefined,
|
||||
toastLifeTimeMs: 600000,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
addError(possibleError !== null ? possibleError : new Error(i18n.FAILURE_TOAST_TITLE), {
|
||||
title: i18n.FAILURE_TOAST_TITLE,
|
||||
toastMessage: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.indexPatterns.failureToastText"
|
||||
defaultMessage="Unexpected error occurred on update. If you would like to modify your data, you can manually select a data view {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={onOpenAndReset} data-test-subj="failureToastLink">
|
||||
{i18n.TOGGLE_TO_NEW_SOURCERER}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) as unknown as string,
|
||||
});
|
||||
return false;
|
||||
},
|
||||
[addError, addSuccess, onOpenAndReset, uiSettings]
|
||||
);
|
||||
};
|
|
@ -64,7 +64,7 @@ export const useSetInitialStateFromUrl = () => {
|
|||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: scope,
|
||||
selectedDataViewId: sourcererState[scope]?.id ?? '',
|
||||
selectedDataViewId: sourcererState[scope]?.id ?? null,
|
||||
selectedPatterns: sourcererState[scope]?.selectedPatterns ?? [],
|
||||
})
|
||||
)
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
|
||||
import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model';
|
||||
import {
|
||||
SelectedDataView,
|
||||
SourcererDataView,
|
||||
SourcererScopeName,
|
||||
} from '../../store/sourcerer/model';
|
||||
import { useUserInfo } from '../../../detections/components/user_info';
|
||||
import { timelineSelectors } from '../../../timelines/store/timeline';
|
||||
import {
|
||||
|
@ -28,6 +32,7 @@ import { checkIfIndicesExist, getScopePatternListSelection } from '../../store/s
|
|||
import { useAppToasts } from '../../hooks/use_app_toasts';
|
||||
import { postSourcererDataView } from './api';
|
||||
import { useDataView } from '../source/use_data_view';
|
||||
import { useFetchIndex } from '../source';
|
||||
|
||||
export const useInitSourcerer = (
|
||||
scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default
|
||||
|
@ -37,11 +42,14 @@ export const useInitSourcerer = (
|
|||
const initialTimelineSourcerer = useRef(true);
|
||||
const initialDetectionSourcerer = useRef(true);
|
||||
const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo();
|
||||
const getDefaultDataViewSelector = useMemo(
|
||||
() => sourcererSelectors.defaultDataViewSelector(),
|
||||
|
||||
const getDataViewsSelector = useMemo(
|
||||
() => sourcererSelectors.getSourcererDataViewsSelector(),
|
||||
[]
|
||||
);
|
||||
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
|
||||
const { defaultDataView, signalIndexName: signalIndexNameSourcerer } = useDeepEqualSelector(
|
||||
(state) => getDataViewsSelector(state)
|
||||
);
|
||||
|
||||
const { addError } = useAppToasts();
|
||||
|
||||
|
@ -59,12 +67,6 @@ export const useInitSourcerer = (
|
|||
}
|
||||
}, [addError, defaultDataView.error]);
|
||||
|
||||
const getSignalIndexNameSelector = useMemo(
|
||||
() => sourcererSelectors.signalIndexNameSelector(),
|
||||
[]
|
||||
);
|
||||
const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector);
|
||||
|
||||
const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const activeTimeline = useDeepEqualSelector((state) =>
|
||||
getTimelineSelector(state, TimelineId.active)
|
||||
|
@ -256,14 +258,26 @@ export const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*';
|
|||
export const useSourcererDataView = (
|
||||
scopeId: SourcererScopeName = SourcererScopeName.default
|
||||
): SelectedDataView => {
|
||||
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
|
||||
const { getDataViewsSelector, getSourcererDataViewSelector, getScopeSelector } = useMemo(
|
||||
() => ({
|
||||
getDataViewsSelector: sourcererSelectors.getSourcererDataViewsSelector(),
|
||||
getSourcererDataViewSelector: sourcererSelectors.sourcererDataViewSelector(),
|
||||
getScopeSelector: sourcererSelectors.scopeIdSelector(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const {
|
||||
signalIndexName,
|
||||
sourcererDataView: selectedDataView,
|
||||
sourcererScope: { selectedPatterns: scopeSelectedPatterns, loading },
|
||||
}: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) =>
|
||||
sourcererScopeSelector(state, scopeId)
|
||||
);
|
||||
selectedDataView,
|
||||
sourcererScope: { missingPatterns, selectedPatterns: scopeSelectedPatterns, loading },
|
||||
}: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => {
|
||||
const sourcererScope = getScopeSelector(state, scopeId);
|
||||
return {
|
||||
...getDataViewsSelector(state),
|
||||
selectedDataView: getSourcererDataViewSelector(state, sourcererScope.selectedDataViewId),
|
||||
sourcererScope,
|
||||
};
|
||||
});
|
||||
|
||||
const selectedPatterns = useMemo(
|
||||
() =>
|
||||
|
@ -273,40 +287,69 @@ export const useSourcererDataView = (
|
|||
[scopeSelectedPatterns]
|
||||
);
|
||||
|
||||
const [legacyPatterns, setLegacyPatterns] = useState<string[]>([]);
|
||||
|
||||
const [indexPatternsLoading, fetchIndexReturn] = useFetchIndex(legacyPatterns);
|
||||
|
||||
const legacyDataView: Omit<SourcererDataView, 'id'> & { id: string | null } = useMemo(
|
||||
() => ({
|
||||
...fetchIndexReturn,
|
||||
runtimeMappings: {},
|
||||
title: '',
|
||||
id: selectedDataView?.id ?? null,
|
||||
loading: indexPatternsLoading,
|
||||
patternList: fetchIndexReturn.indexes,
|
||||
indexFields: fetchIndexReturn.indexPatterns
|
||||
.fields as SelectedDataView['indexPattern']['fields'],
|
||||
}),
|
||||
[fetchIndexReturn, indexPatternsLoading, selectedDataView]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDataView == null || missingPatterns.length > 0) {
|
||||
// old way of fetching indices, legacy timeline
|
||||
setLegacyPatterns(selectedPatterns);
|
||||
} else {
|
||||
setLegacyPatterns([]);
|
||||
}
|
||||
}, [missingPatterns, selectedDataView, selectedPatterns]);
|
||||
|
||||
const sourcererDataView = useMemo(
|
||||
() =>
|
||||
selectedDataView == null || missingPatterns.length > 0 ? legacyDataView : selectedDataView,
|
||||
[legacyDataView, missingPatterns.length, selectedDataView]
|
||||
);
|
||||
|
||||
const indicesExist = useMemo(
|
||||
() => checkIfIndicesExist({ scopeId, signalIndexName, sourcererDataView: selectedDataView }),
|
||||
[scopeId, signalIndexName, selectedDataView]
|
||||
() =>
|
||||
checkIfIndicesExist({
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
patternList: sourcererDataView.patternList,
|
||||
}),
|
||||
[scopeId, signalIndexName, sourcererDataView]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
browserFields: selectedDataView.browserFields,
|
||||
dataViewId: selectedDataView.id,
|
||||
docValueFields: selectedDataView.docValueFields,
|
||||
browserFields: sourcererDataView.browserFields,
|
||||
dataViewId: sourcererDataView.id,
|
||||
docValueFields: sourcererDataView.docValueFields,
|
||||
indexPattern: {
|
||||
fields: selectedDataView.indexFields,
|
||||
fields: sourcererDataView.indexFields,
|
||||
title: selectedPatterns.join(','),
|
||||
},
|
||||
indicesExist,
|
||||
loading: loading || selectedDataView.loading,
|
||||
runtimeMappings: selectedDataView.runtimeMappings,
|
||||
loading: loading || sourcererDataView.loading,
|
||||
runtimeMappings: sourcererDataView.runtimeMappings,
|
||||
// all active & inactive patterns in DATA_VIEW
|
||||
patternList: selectedDataView.title.split(','),
|
||||
// selected patterns in DATA_VIEW
|
||||
patternList: sourcererDataView.title.split(','),
|
||||
// selected patterns in DATA_VIEW including filter
|
||||
selectedPatterns: selectedPatterns.sort(),
|
||||
// if we have to do an update to data view, tell us which patterns are active
|
||||
...(legacyPatterns.length > 0 ? { activePatterns: sourcererDataView.patternList } : {}),
|
||||
}),
|
||||
[
|
||||
selectedDataView.browserFields,
|
||||
selectedDataView.id,
|
||||
selectedDataView.docValueFields,
|
||||
selectedDataView.indexFields,
|
||||
selectedDataView.loading,
|
||||
selectedDataView.runtimeMappings,
|
||||
selectedDataView.title,
|
||||
selectedPatterns,
|
||||
indicesExist,
|
||||
loading,
|
||||
]
|
||||
[sourcererDataView, selectedPatterns, indicesExist, loading, legacyPatterns.length]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1955,7 +1955,7 @@ export const mockTimelineModel: TimelineModel = {
|
|||
columns: mockTimelineModelColumns,
|
||||
defaultColumns: mockTimelineModelColumns,
|
||||
dataProviders: [],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: {
|
||||
end: '2020-03-18T13:52:38.929Z',
|
||||
start: '2020-03-18T13:46:38.929Z',
|
||||
|
@ -2092,7 +2092,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
|
|||
queryMatch: { field: '_id', operator: ':', value: '1' },
|
||||
},
|
||||
],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' },
|
||||
deletedEventIds: [],
|
||||
description: '',
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { parseExperimentalConfigValue } from '../../../common/experimental_features';
|
||||
import { SecuritySubPlugins } from '../../app/types';
|
||||
import { createInitialState } from './reducer';
|
||||
import { mockSourcererState } from '../mock';
|
||||
import { mockIndexPattern, mockSourcererState } from '../mock';
|
||||
import { useSourcererDataView } from '../containers/sourcerer';
|
||||
import { useDeepEqualSelector } from '../hooks/use_selector';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
@ -19,6 +19,12 @@ jest.mock('../lib/kibana', () => ({
|
|||
get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })),
|
||||
},
|
||||
}));
|
||||
jest.mock('../containers/source', () => ({
|
||||
useFetchIndex: () => [
|
||||
false,
|
||||
{ indexes: [], indicesExist: true, indexPatterns: mockIndexPattern },
|
||||
],
|
||||
}));
|
||||
|
||||
describe('createInitialState', () => {
|
||||
describe('sourcerer -> default -> indicesExist', () => {
|
||||
|
@ -40,20 +46,24 @@ describe('createInitialState', () => {
|
|||
(useDeepEqualSelector as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
test('indicesExist should be TRUE if configIndexPatterns is NOT empty', async () => {
|
||||
test('indicesExist should be TRUE if patternList is NOT empty', async () => {
|
||||
const { result } = renderHook(() => useSourcererDataView());
|
||||
expect(result.current.indicesExist).toEqual(true);
|
||||
});
|
||||
|
||||
test('indicesExist should be FALSE if configIndexPatterns is empty', () => {
|
||||
test('indicesExist should be FALSE if patternList is empty', () => {
|
||||
const state = createInitialState(mockPluginState, {
|
||||
...defaultState,
|
||||
defaultDataView: {
|
||||
...defaultState.defaultDataView,
|
||||
id: '',
|
||||
title: '',
|
||||
patternList: [],
|
||||
},
|
||||
kibanaDataViews: [
|
||||
{
|
||||
...defaultState.defaultDataView,
|
||||
patternList: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state));
|
||||
const { result } = renderHook(() => useSourcererDataView());
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
*/
|
||||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
import { TimelineEventsType } from '../../../../common/types/timeline';
|
||||
|
||||
import { SourcererDataView, SourcererScopeName } from './model';
|
||||
import { SelectedDataView, SourcererDataView, SourcererScopeName } from './model';
|
||||
import { SecurityDataView } from '../../containers/sourcerer/api';
|
||||
|
||||
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
|
||||
|
@ -39,8 +38,8 @@ export const setSourcererScopeLoading = actionCreator<{
|
|||
|
||||
export interface SelectedDataViewPayload {
|
||||
id: SourcererScopeName;
|
||||
selectedDataViewId: string;
|
||||
selectedPatterns: string[];
|
||||
eventType?: TimelineEventsType;
|
||||
selectedDataViewId: SelectedDataView['dataViewId'];
|
||||
selectedPatterns: SelectedDataView['selectedPatterns'];
|
||||
shouldValidateSelectedPatterns?: boolean;
|
||||
}
|
||||
export const setSelectedDataView = actionCreator<SelectedDataViewPayload>('SET_SELECTED_DATA_VIEW');
|
||||
|
|
|
@ -69,7 +69,7 @@ describe('sourcerer store helpers', () => {
|
|||
selectedPatterns: ['auditbeat-*'],
|
||||
};
|
||||
it('sets selectedPattern', () => {
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload);
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload, true);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
|
@ -78,10 +78,14 @@ describe('sourcerer store helpers', () => {
|
|||
});
|
||||
});
|
||||
it('sets to default when empty array is passed and scope is default', () => {
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
|
||||
...payload,
|
||||
selectedPatterns: [],
|
||||
});
|
||||
const result = validateSelectedPatterns(
|
||||
mockGlobalState.sourcerer,
|
||||
{
|
||||
...payload,
|
||||
selectedPatterns: [],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.default]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
|
||||
|
@ -90,11 +94,15 @@ describe('sourcerer store helpers', () => {
|
|||
});
|
||||
});
|
||||
it('sets to default when empty array is passed and scope is detections', () => {
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
|
||||
...payload,
|
||||
id: SourcererScopeName.detections,
|
||||
selectedPatterns: [],
|
||||
});
|
||||
const result = validateSelectedPatterns(
|
||||
mockGlobalState.sourcerer,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.detections,
|
||||
selectedPatterns: [],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.detections]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.detections],
|
||||
|
@ -103,22 +111,21 @@ describe('sourcerer store helpers', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
it('sets to default when empty array is passed and scope is timeline', () => {
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedPatterns: [],
|
||||
});
|
||||
it('sets to empty when empty array is passed and scope is timeline', () => {
|
||||
const result = validateSelectedPatterns(
|
||||
mockGlobalState.sourcerer,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedPatterns: [],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedDataViewId: dataView.id,
|
||||
selectedPatterns: [
|
||||
signalIndexName,
|
||||
...mockGlobalState.sourcerer.defaultDataView.patternList.filter(
|
||||
(p) => p !== signalIndexName
|
||||
),
|
||||
].sort(),
|
||||
selectedPatterns: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -132,11 +139,15 @@ describe('sourcerer store helpers', () => {
|
|||
defaultDataView: dataViewNoSignals,
|
||||
kibanaDataViews: [dataViewNoSignals],
|
||||
};
|
||||
const result = validateSelectedPatterns(stateNoSignals, {
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`],
|
||||
});
|
||||
const result = validateSelectedPatterns(
|
||||
stateNoSignals,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedPatterns: [`${mockGlobalState.sourcerer.signalIndexName}`],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
|
@ -147,19 +158,23 @@ describe('sourcerer store helpers', () => {
|
|||
});
|
||||
describe('handles missing dataViewId, 7.16 -> 8.0', () => {
|
||||
it('selectedPatterns.length > 0 & all selectedPatterns exist in defaultDataView, set dataViewId to defaultDataView.id', () => {
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedDataViewId: '',
|
||||
selectedPatterns: [
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[3],
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[4],
|
||||
],
|
||||
});
|
||||
const result = validateSelectedPatterns(
|
||||
mockGlobalState.sourcerer,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedDataViewId: null,
|
||||
selectedPatterns: [
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[3],
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[4],
|
||||
],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedDataViewId: dataView.id,
|
||||
selectedDataViewId: null,
|
||||
selectedPatterns: [
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[3],
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[4],
|
||||
|
@ -167,16 +182,20 @@ describe('sourcerer store helpers', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
it('selectedPatterns.length > 0 & a pattern in selectedPatterns does not exist in defaultDataView, set dataViewId to null', () => {
|
||||
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedDataViewId: '',
|
||||
selectedPatterns: [
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[3],
|
||||
'journalbeat-*',
|
||||
],
|
||||
});
|
||||
it('selectedPatterns.length > 0 & some selectedPatterns do not exist in defaultDataView, set dataViewId to null', () => {
|
||||
const result = validateSelectedPatterns(
|
||||
mockGlobalState.sourcerer,
|
||||
{
|
||||
...payload,
|
||||
id: SourcererScopeName.timeline,
|
||||
selectedDataViewId: null,
|
||||
selectedPatterns: [
|
||||
mockGlobalState.sourcerer.defaultDataView.patternList[3],
|
||||
'journalbeat-*',
|
||||
],
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(result).toEqual({
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
|
@ -185,6 +204,7 @@ describe('sourcerer store helpers', () => {
|
|||
mockGlobalState.sourcerer.defaultDataView.patternList[3],
|
||||
'journalbeat-*',
|
||||
],
|
||||
missingPatterns: ['journalbeat-*'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,54 +34,58 @@ export const getScopePatternListSelection = (
|
|||
}
|
||||
};
|
||||
|
||||
export const ensurePatternFormat = (patternList: string[]): string[] =>
|
||||
[
|
||||
...new Set(
|
||||
patternList.reduce((acc: string[], pattern: string) => [...pattern.split(','), ...acc], [])
|
||||
),
|
||||
].sort();
|
||||
|
||||
export const validateSelectedPatterns = (
|
||||
state: SourcererModel,
|
||||
payload: SelectedDataViewPayload
|
||||
payload: SelectedDataViewPayload,
|
||||
shouldValidateSelectedPatterns: boolean
|
||||
): Partial<SourcererScopeById> => {
|
||||
const { id, ...rest } = payload;
|
||||
let dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId);
|
||||
const dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId);
|
||||
// dedupe because these could come from a silly url or pre 8.0 timeline
|
||||
const dedupePatterns = [...new Set(rest.selectedPatterns)];
|
||||
let selectedPatterns =
|
||||
dataView != null
|
||||
const dedupePatterns = ensurePatternFormat(rest.selectedPatterns);
|
||||
let missingPatterns: string[] = [];
|
||||
// check for missing patterns against default data view only
|
||||
if (dataView == null || dataView.id === state.defaultDataView.id) {
|
||||
const dedupeAllDefaultPatterns = ensurePatternFormat(
|
||||
(dataView ?? state.defaultDataView).title.split(',')
|
||||
);
|
||||
missingPatterns = dedupePatterns.filter(
|
||||
(pattern) => !dedupeAllDefaultPatterns.includes(pattern)
|
||||
);
|
||||
}
|
||||
const selectedPatterns =
|
||||
// shouldValidateSelectedPatterns is false when upgrading from
|
||||
// legacy pre-8.0 timeline index patterns to data view.
|
||||
shouldValidateSelectedPatterns && dataView != null && missingPatterns.length === 0
|
||||
? dedupePatterns.filter(
|
||||
(pattern) =>
|
||||
// Typescript is being mean and telling me dataView could be undefined here
|
||||
// so redoing the dataView != null check
|
||||
(dataView != null && dataView.patternList.includes(pattern)) ||
|
||||
// this is a hack, but sometimes signal index is deleted and is getting regenerated. it gets set before it is put in the dataView
|
||||
state.signalIndexName == null ||
|
||||
state.signalIndexName === pattern
|
||||
)
|
||||
: // 7.16 -> 8.0 this will get hit because dataView == null
|
||||
: // don't remove non-existing patterns, they were saved in the first place in timeline
|
||||
// but removed from the security data view
|
||||
// or its a legacy pre-8.0 timeline
|
||||
dedupePatterns;
|
||||
|
||||
if (selectedPatterns.length > 0 && dataView == null) {
|
||||
// we have index patterns, but not a data view id
|
||||
// find out if we have these index patterns in the defaultDataView
|
||||
const areAllPatternsInDefault = selectedPatterns.every(
|
||||
(pattern) => state.defaultDataView.title.indexOf(pattern) > -1
|
||||
);
|
||||
if (areAllPatternsInDefault) {
|
||||
dataView = state.defaultDataView;
|
||||
selectedPatterns = selectedPatterns.filter(
|
||||
(pattern) => dataView != null && dataView.patternList.includes(pattern)
|
||||
);
|
||||
}
|
||||
}
|
||||
// TO DO: Steph/sourcerer If dataView is still undefined here, create temporary dataView
|
||||
// and prompt user to go create this dataView
|
||||
// currently UI will take the undefined dataView and default to defaultDataView anyways
|
||||
// this is a "strategically merged" bug ;)
|
||||
// https://github.com/elastic/security-team/issues/1921
|
||||
|
||||
return {
|
||||
[id]: {
|
||||
...state.sourcererScopes[id],
|
||||
...rest,
|
||||
selectedDataViewId: dataView?.id ?? null,
|
||||
selectedPatterns,
|
||||
...(isEmpty(selectedPatterns)
|
||||
missingPatterns,
|
||||
// if in timeline, allow for empty in case pattern was deleted
|
||||
// need flow for this
|
||||
...(isEmpty(selectedPatterns) && id !== SourcererScopeName.timeline
|
||||
? {
|
||||
selectedPatterns: getScopePatternListSelection(
|
||||
dataView ?? state.defaultDataView,
|
||||
|
@ -97,17 +101,17 @@ export const validateSelectedPatterns = (
|
|||
};
|
||||
|
||||
interface CheckIfIndicesExistParams {
|
||||
patternList: sourcererModel.SourcererDataView['patternList'];
|
||||
scopeId: sourcererModel.SourcererScopeName;
|
||||
signalIndexName: string | null;
|
||||
sourcererDataView: sourcererModel.SourcererDataView;
|
||||
}
|
||||
export const checkIfIndicesExist = ({
|
||||
patternList,
|
||||
scopeId,
|
||||
signalIndexName,
|
||||
sourcererDataView,
|
||||
}: CheckIfIndicesExistParams) =>
|
||||
scopeId === SourcererScopeName.detections
|
||||
? sourcererDataView.patternList.includes(`${signalIndexName}`)
|
||||
? patternList.includes(`${signalIndexName}`)
|
||||
: scopeId === SourcererScopeName.default
|
||||
? sourcererDataView.patternList.filter((i) => i !== signalIndexName).length > 0
|
||||
: sourcererDataView.patternList.length > 0;
|
||||
? patternList.filter((i) => i !== signalIndexName).length > 0
|
||||
: patternList.length > 0;
|
||||
|
|
|
@ -29,10 +29,15 @@ export interface SourcererScope {
|
|||
id: SourcererScopeName;
|
||||
/** is an update being made to the sourcerer data view */
|
||||
loading: boolean;
|
||||
/** selected data view id */
|
||||
selectedDataViewId: string;
|
||||
/** selected data view id, null if it is legacy index patterns*/
|
||||
selectedDataViewId: string | null;
|
||||
/** selected patterns within the data view */
|
||||
selectedPatterns: string[];
|
||||
/** if has length,
|
||||
* id === SourcererScopeName.timeline
|
||||
* selectedDataViewId === null OR defaultDataView.id
|
||||
* saved timeline has pattern that is not in the default */
|
||||
missingPatterns: string[];
|
||||
}
|
||||
|
||||
export type SourcererScopeById = Record<SourcererScopeName, SourcererScope>;
|
||||
|
@ -54,6 +59,7 @@ export interface KibanaDataView {
|
|||
* DataView from Kibana + timelines/index_fields enhanced field data
|
||||
*/
|
||||
export interface SourcererDataView extends KibanaDataView {
|
||||
id: string;
|
||||
/** we need this for @timestamp data */
|
||||
browserFields: BrowserFields;
|
||||
/** we need this for @timestamp data */
|
||||
|
@ -75,7 +81,7 @@ export interface SourcererDataView extends KibanaDataView {
|
|||
*/
|
||||
export interface SelectedDataView {
|
||||
browserFields: SourcererDataView['browserFields'];
|
||||
dataViewId: SourcererDataView['id'];
|
||||
dataViewId: string | null; // null if legacy pre-8.0 timeline
|
||||
docValueFields: SourcererDataView['docValueFields'];
|
||||
/**
|
||||
* DataViewBase with enhanced index fields used in timelines
|
||||
|
@ -88,8 +94,10 @@ export interface SelectedDataView {
|
|||
/** all active & inactive patterns from SourcererDataView['title'] */
|
||||
patternList: string[];
|
||||
runtimeMappings: SourcererDataView['runtimeMappings'];
|
||||
/** all selected patterns from SourcererScope['selectedPatterns'] */
|
||||
selectedPatterns: string[];
|
||||
/** all selected patterns from SourcererScope['selectedPatterns'] */
|
||||
selectedPatterns: SourcererScope['selectedPatterns'];
|
||||
// active patterns when dataViewId == null
|
||||
activePatterns?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,7 +105,7 @@ export interface SelectedDataView {
|
|||
*/
|
||||
export interface SourcererModel {
|
||||
/** default security-solution data view */
|
||||
defaultDataView: SourcererDataView & { error?: unknown };
|
||||
defaultDataView: SourcererDataView & { id: string; error?: unknown };
|
||||
/** all Kibana data views, including security-solution */
|
||||
kibanaDataViews: SourcererDataView[];
|
||||
/** security solution signals index name */
|
||||
|
@ -115,8 +123,9 @@ export type SourcererUrlState = Partial<{
|
|||
|
||||
export const initSourcererScope: Omit<SourcererScope, 'id'> = {
|
||||
loading: false,
|
||||
selectedDataViewId: '',
|
||||
selectedDataViewId: null,
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
};
|
||||
export const initDataView = {
|
||||
browserFields: EMPTY_BROWSER_FIELDS,
|
||||
|
|
|
@ -72,13 +72,17 @@ export const sourcererReducer = reducerWithInitialState(initialSourcererState)
|
|||
}),
|
||||
},
|
||||
}))
|
||||
.case(setSelectedDataView, (state, payload) => ({
|
||||
...state,
|
||||
sourcererScopes: {
|
||||
...state.sourcererScopes,
|
||||
...validateSelectedPatterns(state, payload),
|
||||
},
|
||||
}))
|
||||
.case(setSelectedDataView, (state, payload) => {
|
||||
const { shouldValidateSelectedPatterns = true, ...patternsInfo } = payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
sourcererScopes: {
|
||||
...state.sourcererScopes,
|
||||
...validateSelectedPatterns(state, patternsInfo, shouldValidateSelectedPatterns),
|
||||
},
|
||||
};
|
||||
})
|
||||
.case(setDataView, (state, dataView) => ({
|
||||
...state,
|
||||
...(dataView.id === state.defaultDataView.id
|
||||
|
|
|
@ -26,8 +26,11 @@ export const sourcererDefaultDataViewSelector = ({
|
|||
sourcerer,
|
||||
}: State): SourcererModel['defaultDataView'] => sourcerer.defaultDataView;
|
||||
|
||||
export const dataViewSelector = ({ sourcerer }: State, id: string): SourcererDataView =>
|
||||
sourcerer.kibanaDataViews.find((dataView) => dataView.id === id) ?? sourcerer.defaultDataView;
|
||||
export const dataViewSelector = (
|
||||
{ sourcerer }: State,
|
||||
id: string | null
|
||||
): SourcererDataView | undefined =>
|
||||
sourcerer.kibanaDataViews.find((dataView) => dataView.id === id);
|
||||
|
||||
export const sourcererScopeIdSelector = (
|
||||
{ sourcerer }: State,
|
||||
|
@ -54,29 +57,48 @@ export const sourcererDataViewSelector = () =>
|
|||
createSelector(dataViewSelector, (dataView) => dataView);
|
||||
|
||||
export interface SourcererScopeSelector extends Omit<SourcererModel, 'sourcererScopes'> {
|
||||
sourcererDataView: SourcererDataView;
|
||||
selectedDataView: SourcererDataView | undefined;
|
||||
sourcererScope: SourcererScope;
|
||||
}
|
||||
|
||||
export const getSourcererScopeSelector = () => {
|
||||
export const getSourcererDataViewsSelector = () => {
|
||||
const getKibanaDataViewsSelector = kibanaDataViewsSelector();
|
||||
const getDefaultDataViewSelector = defaultDataViewSelector();
|
||||
const getSignalIndexNameSelector = signalIndexNameSelector();
|
||||
const getSourcererDataViewSelector = sourcererDataViewSelector();
|
||||
const getScopeSelector = scopeIdSelector();
|
||||
|
||||
return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => {
|
||||
return (state: State): Omit<SourcererModel, 'sourcererScopes'> => {
|
||||
const kibanaDataViews = getKibanaDataViewsSelector(state);
|
||||
const defaultDataView = getDefaultDataViewSelector(state);
|
||||
const signalIndexName = getSignalIndexNameSelector(state);
|
||||
const scope = getScopeSelector(state, scopeId);
|
||||
const sourcererDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId);
|
||||
|
||||
return {
|
||||
defaultDataView,
|
||||
kibanaDataViews,
|
||||
signalIndexName,
|
||||
sourcererDataView,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Attn Future Developer
|
||||
* Access sourcererScope.selectedPatterns from
|
||||
* hook useSourcererDataView in `common/containers/sourcerer/index`
|
||||
* in order to get exclude patterns for searches
|
||||
* Access sourcererScope.selectedPatterns
|
||||
* from this function for display purposes only
|
||||
* */
|
||||
export const getSourcererScopeSelector = () => {
|
||||
const getDataViewsSelector = getSourcererDataViewsSelector();
|
||||
const getSourcererDataViewSelector = sourcererDataViewSelector();
|
||||
const getScopeSelector = scopeIdSelector();
|
||||
|
||||
return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => {
|
||||
const dataViews = getDataViewsSelector(state);
|
||||
const scope = getScopeSelector(state, scopeId);
|
||||
const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId);
|
||||
|
||||
return {
|
||||
...dataViews,
|
||||
selectedDataView,
|
||||
sourcererScope: scope,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -140,7 +140,7 @@ describe('alert actions', () => {
|
|||
],
|
||||
defaultColumns: defaultHeaders,
|
||||
dataProviders: [],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: {
|
||||
end: '2018-11-05T19:03:25.937Z',
|
||||
start: '2018-11-05T18:58:25.937Z',
|
||||
|
|
|
@ -10,8 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
|
|||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
|
||||
import '../../../common/mock/match_media';
|
||||
import { mockIndexPattern } from '../../../common/mock/index_pattern';
|
||||
import { TestProviders } from '../../../common/mock/test_providers';
|
||||
import { mockIndexPattern, TestProviders } from '../../../common/mock';
|
||||
import { HostDetailsTabs } from './details_tabs';
|
||||
import { HostDetailsTabsProps, SetAbsoluteRangeDatePicker } from './types';
|
||||
import { hostDetailsPagePath } from '../types';
|
||||
|
@ -25,7 +24,7 @@ jest.mock('../../../common/lib/kibana');
|
|||
jest.mock('../../../common/components/url_state/normalize_time_range.ts');
|
||||
|
||||
jest.mock('../../../common/containers/source', () => ({
|
||||
useWithSource: jest.fn().mockReturnValue({ indicesExist: true, indexPattern: mockIndexPattern }),
|
||||
useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }],
|
||||
}));
|
||||
|
||||
jest.mock('../../../common/containers/use_global_time', () => ({
|
||||
|
|
|
@ -42,7 +42,13 @@ jest.mock('../../../common/lib/kibana', () => {
|
|||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../common/containers/sourcerer', () => {
|
||||
return {
|
||||
useSourcererDataView: () => ({
|
||||
selectedPatterns: ['filebeat-*', 'packetbeat-*'],
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('./index_patterns_missing_prompt', () => {
|
||||
return {
|
||||
IndexPatternsMissingPrompt: jest.fn(() => <div data-test-subj="IndexPatternsMissingPrompt" />),
|
||||
|
@ -56,7 +62,6 @@ describe('EmbeddedMapComponent', () => {
|
|||
{ id: '6f1eeb50-023d-11eb-bcb6-6ba0578012a9', title: 'filebeat-*' },
|
||||
{ id: '28995490-023d-11eb-bcb6-6ba0578012a9', title: 'auditbeat-*' },
|
||||
],
|
||||
sourcererScope: { selectedPatterns: ['filebeat-*', 'packetbeat-*'] },
|
||||
};
|
||||
const mockCreateEmbeddable = {
|
||||
destroyed: false,
|
||||
|
|
|
@ -32,6 +32,7 @@ import { getLayerList } from './map_config';
|
|||
import { sourcererSelectors } from '../../../common/store/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
|
||||
interface EmbeddableMapProps {
|
||||
maintainRatio?: boolean;
|
||||
|
@ -97,12 +98,15 @@ export const EmbeddedMapComponent = ({
|
|||
|
||||
const [, dispatchToaster] = useStateToaster();
|
||||
|
||||
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
|
||||
const { kibanaDataViews, sourcererScope }: sourcererSelectors.SourcererScopeSelector =
|
||||
useDeepEqualSelector((state) => sourcererScopeSelector(state, SourcererScopeName.default));
|
||||
const getDataViewsSelector = useMemo(
|
||||
() => sourcererSelectors.getSourcererDataViewsSelector(),
|
||||
[]
|
||||
);
|
||||
const { kibanaDataViews } = useDeepEqualSelector((state) => getDataViewsSelector(state));
|
||||
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.default);
|
||||
|
||||
const [mapIndexPatterns, setMapIndexPatterns] = useState(
|
||||
kibanaDataViews.filter((dataView) => sourcererScope.selectedPatterns.includes(dataView.title))
|
||||
kibanaDataViews.filter((dataView) => selectedPatterns.includes(dataView.title))
|
||||
);
|
||||
|
||||
// This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our
|
||||
|
@ -116,7 +120,7 @@ export const EmbeddedMapComponent = ({
|
|||
useEffect(() => {
|
||||
setMapIndexPatterns((prevMapIndexPatterns) => {
|
||||
const newIndexPatterns = kibanaDataViews.filter((dataView) =>
|
||||
sourcererScope.selectedPatterns.includes(dataView.title)
|
||||
selectedPatterns.includes(dataView.title)
|
||||
);
|
||||
if (!deepEqual(newIndexPatterns, prevMapIndexPatterns)) {
|
||||
if (newIndexPatterns.length === 0) {
|
||||
|
@ -126,7 +130,7 @@ export const EmbeddedMapComponent = ({
|
|||
}
|
||||
return prevMapIndexPatterns;
|
||||
});
|
||||
}, [kibanaDataViews, sourcererScope.selectedPatterns]);
|
||||
}, [kibanaDataViews, selectedPatterns]);
|
||||
|
||||
// Initial Load useEffect
|
||||
useEffect(() => {
|
||||
|
@ -159,7 +163,7 @@ export const EmbeddedMapComponent = ({
|
|||
}
|
||||
}
|
||||
}
|
||||
if (embeddable == null && sourcererScope.selectedPatterns.length > 0) {
|
||||
if (embeddable == null && selectedPatterns.length > 0) {
|
||||
setupEmbeddable();
|
||||
}
|
||||
|
||||
|
@ -175,7 +179,7 @@ export const EmbeddedMapComponent = ({
|
|||
query,
|
||||
portalNode,
|
||||
services.embeddable,
|
||||
sourcererScope.selectedPatterns,
|
||||
selectedPatterns,
|
||||
setQuery,
|
||||
startDate,
|
||||
]);
|
||||
|
|
|
@ -114,11 +114,14 @@ export const useCreateFieldButton = (
|
|||
timelineId: TimelineId
|
||||
) => {
|
||||
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
|
||||
const { selectedDataViewId } = useDeepEqualSelector((state) =>
|
||||
const { missingPatterns, selectedDataViewId } = useDeepEqualSelector((state) =>
|
||||
scopeIdSelector(state, sourcererScope)
|
||||
);
|
||||
|
||||
const createFieldComponent = useMemo(() => {
|
||||
return useMemo(() => {
|
||||
if (selectedDataViewId == null || missingPatterns.length > 0) {
|
||||
return;
|
||||
}
|
||||
// It receives onClick props from field browser in order to close the modal.
|
||||
const CreateFieldButtonComponent: CreateFieldComponentType = ({ onClick }) => (
|
||||
<CreateFieldButton
|
||||
|
@ -129,7 +132,5 @@ export const useCreateFieldButton = (
|
|||
);
|
||||
|
||||
return CreateFieldButtonComponent;
|
||||
}, [selectedDataViewId, timelineId]);
|
||||
|
||||
return createFieldComponent;
|
||||
}, [missingPatterns.length, selectedDataViewId, timelineId]);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,11 @@ import { waitFor } from '@testing-library/react';
|
|||
import { AddTimelineButton } from './';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { TimelineId } from '../../../../../common/types/timeline';
|
||||
import { mockOpenTimelineQueryResults, TestProviders } from '../../../../common/mock';
|
||||
import {
|
||||
mockIndexPattern,
|
||||
mockOpenTimelineQueryResults,
|
||||
TestProviders,
|
||||
} from '../../../../common/mock';
|
||||
import { getAllTimeline, useGetAllTimeline } from '../../../containers/all';
|
||||
import { mockHistory, Router } from '../../../../common/mock/router';
|
||||
|
||||
|
@ -62,6 +66,10 @@ jest.mock('../../../../common/components/inspect', () => ({
|
|||
InspectButtonContainer: jest.fn(({ children }) => <div>{children}</div>),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/containers/source', () => ({
|
||||
useFetchIndex: () => [false, { indicesExist: true, indexPatterns: mockIndexPattern }],
|
||||
}));
|
||||
|
||||
describe('AddTimelineButton', () => {
|
||||
let wrapper: ReactWrapper;
|
||||
const props = {
|
||||
|
|
|
@ -417,7 +417,6 @@ export const dispatchUpdateTimeline =
|
|||
id: SourcererScopeName.timeline,
|
||||
selectedDataViewId: timeline.dataViewId,
|
||||
selectedPatterns: timeline.indexNames,
|
||||
eventType: timeline.eventType,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -112,6 +112,10 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({
|
|||
maxDelay: () => 3000,
|
||||
}));
|
||||
|
||||
jest.mock('../../create_field_button', () => ({
|
||||
useCreateFieldButton: () => <></>,
|
||||
}));
|
||||
|
||||
describe('Body', () => {
|
||||
const mount = useMountAppended();
|
||||
const mockRefetch = jest.fn();
|
||||
|
|
|
@ -180,6 +180,7 @@ describe('Timeline', () => {
|
|||
loading: true,
|
||||
indexPattern: {},
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -14,18 +14,23 @@ import '../../../common/mock/match_media';
|
|||
import { mockBrowserFields, mockDocValueFields } from '../../../common/containers/source/mock';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import {
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
mockGlobalState,
|
||||
mockIndexNames,
|
||||
mockIndexPattern,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
TestProviders,
|
||||
} from '../../../common/mock';
|
||||
|
||||
import { StatefulTimeline, Props as StatefulTimelineOwnProps } from './index';
|
||||
import { useTimelineEvents } from '../../containers/index';
|
||||
import { useTimelineEvents } from '../../containers';
|
||||
import { DefaultCellRenderer } from './cell_rendering/default_cell_renderer';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from './styles';
|
||||
import { defaultRowRenderers } from './body/renderers';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { createStore } from '../../../common/store';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
|
||||
jest.mock('../../containers/index', () => ({
|
||||
useTimelineEvents: jest.fn(),
|
||||
|
@ -91,6 +96,7 @@ describe('StatefulTimeline', () => {
|
|||
rowRenderers: defaultRowRenderers,
|
||||
timelineId: TimelineId.test,
|
||||
};
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -114,25 +120,6 @@ describe('StatefulTimeline', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="timeline"]')).toBeTruthy();
|
||||
expect(mockDispatch).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('data view updates, updates timeline', () => {
|
||||
mockUseSourcererDataView.mockReturnValue({ ...mockDataView, selectedPatterns: mockIndexNames });
|
||||
mount(
|
||||
<TestProviders>
|
||||
<StatefulTimeline {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockDispatch).toBeCalledTimes(2);
|
||||
expect(mockDispatch).toHaveBeenNthCalledWith(2, {
|
||||
payload: {
|
||||
id: 'test',
|
||||
dataViewId: mockDataView.dataViewId,
|
||||
indexNames: mockIndexNames,
|
||||
},
|
||||
type: 'x-pack/security_solution/local/timeline/UPDATE_DATA_VIEW',
|
||||
});
|
||||
});
|
||||
|
||||
test(`it add attribute data-timeline-id in ${SELECTOR_TIMELINE_GLOBAL_CONTAINER}`, () => {
|
||||
|
@ -150,4 +137,90 @@ describe('StatefulTimeline', () => {
|
|||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('on create timeline and timeline savedObjectId: null, sourcerer does not update timeline', () => {
|
||||
mount(
|
||||
<TestProviders>
|
||||
<StatefulTimeline {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockDispatch).toBeCalledTimes(1);
|
||||
expect(mockDispatch.mock.calls[0][0].payload.indexNames).toEqual(
|
||||
mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns
|
||||
);
|
||||
});
|
||||
test('sourcerer data view updates and timeline already matches the data view, no updates', () => {
|
||||
mount(
|
||||
<TestProviders
|
||||
store={createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
timeline: {
|
||||
...mockGlobalState.timeline,
|
||||
timelineById: {
|
||||
test: {
|
||||
...mockGlobalState.timeline.timelineById.test,
|
||||
savedObjectId: 'definitely-not-null',
|
||||
indexNames:
|
||||
mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline]
|
||||
.selectedPatterns,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
)}
|
||||
>
|
||||
<StatefulTimeline {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('sourcerer data view updates, update timeline data view', () => {
|
||||
mount(
|
||||
<TestProviders
|
||||
store={createStore(
|
||||
{
|
||||
...mockGlobalState,
|
||||
timeline: {
|
||||
...mockGlobalState.timeline,
|
||||
timelineById: {
|
||||
test: {
|
||||
...mockGlobalState.timeline.timelineById.test,
|
||||
savedObjectId: 'definitely-not-null',
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcerer: {
|
||||
...mockGlobalState.sourcerer,
|
||||
sourcererScopes: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes,
|
||||
[SourcererScopeName.timeline]: {
|
||||
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
|
||||
selectedPatterns: mockIndexNames,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SUB_PLUGINS_REDUCER,
|
||||
kibanaObservable,
|
||||
storage
|
||||
)}
|
||||
>
|
||||
<StatefulTimeline {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockDispatch).toBeCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenNthCalledWith(1, {
|
||||
payload: {
|
||||
id: 'test',
|
||||
dataViewId: mockDataView.dataViewId,
|
||||
indexNames: mockIndexNames,
|
||||
},
|
||||
type: 'x-pack/security_solution/local/timeline/UPDATE_DATA_VIEW',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ import { timelineActions, timelineSelectors } from '../../store/timeline';
|
|||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
import { defaultHeaders } from './body/column_headers/default_headers';
|
||||
import { CellValueElementProps } from './cell_rendering';
|
||||
import { useSourcererDataView } from '../../../common/containers/sourcerer';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { FlyoutHeader, FlyoutHeaderPanel } from '../flyout/header';
|
||||
import { TimelineType, TimelineId, RowRenderer } from '../../../../common/types/timeline';
|
||||
|
@ -29,6 +28,7 @@ import { HideShowContainer, TimelineContainer } from './styles';
|
|||
import { useTimelineFullScreen } from '../../../common/containers/use_full_screen';
|
||||
import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_full_screen';
|
||||
import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict';
|
||||
import { sourcererSelectors } from '../../../common/store';
|
||||
|
||||
const TimelineTemplateBadge = styled.div`
|
||||
background: ${({ theme }) => theme.eui.euiColorVis3_behindText};
|
||||
|
@ -62,11 +62,14 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
const dispatch = useDispatch();
|
||||
const containerElement = useRef<HTMLDivElement | null>(null);
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const { dataViewId, selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
|
||||
const {
|
||||
dataViewId: dataViewIdCurrent,
|
||||
indexNames: selectedPatternsCurrent,
|
||||
selectedPatterns: selectedPatternsSourcerer,
|
||||
selectedDataViewId: selectedDataViewIdSourcerer,
|
||||
} = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline));
|
||||
const {
|
||||
dataViewId: selectedDataViewIdTimeline,
|
||||
indexNames: selectedPatternsTimeline,
|
||||
graphEventId,
|
||||
savedObjectId,
|
||||
timelineType,
|
||||
|
@ -77,6 +80,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
getTimeline(state, timelineId) ?? timelineDefaults
|
||||
)
|
||||
);
|
||||
|
||||
const { timelineFullScreen } = useTimelineFullScreen();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -85,8 +89,8 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
timelineActions.createTimeline({
|
||||
id: timelineId,
|
||||
columns: defaultHeaders,
|
||||
dataViewId,
|
||||
indexNames: selectedPatterns,
|
||||
dataViewId: selectedDataViewIdSourcerer,
|
||||
indexNames: selectedPatternsSourcerer,
|
||||
expandedDetail: activeTimeline.getExpandedDetail(),
|
||||
show: false,
|
||||
})
|
||||
|
@ -95,37 +99,40 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onDataViewChange = useCallback(() => {
|
||||
const onSourcererChange = useCallback(() => {
|
||||
if (
|
||||
// timeline not initialized, so this must be initial state and not user change
|
||||
!savedObjectId ||
|
||||
selectedDataViewIdSourcerer == null ||
|
||||
// initial state will get set on create
|
||||
(dataViewIdCurrent === '' && selectedPatternsCurrent.length === 0) ||
|
||||
(selectedDataViewIdTimeline === null && selectedPatternsTimeline.length === 0) ||
|
||||
// don't update if no change
|
||||
(dataViewIdCurrent === dataViewId &&
|
||||
selectedPatternsCurrent.sort().join() === selectedPatterns.sort().join())
|
||||
(selectedDataViewIdTimeline === selectedDataViewIdSourcerer &&
|
||||
selectedPatternsTimeline.sort().join() === selectedPatternsSourcerer.sort().join())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
timelineActions.updateDataView({
|
||||
dataViewId: selectedDataViewIdSourcerer,
|
||||
id: timelineId,
|
||||
dataViewId,
|
||||
indexNames: selectedPatterns,
|
||||
indexNames: selectedPatternsSourcerer,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
dataViewId,
|
||||
dataViewIdCurrent,
|
||||
dispatch,
|
||||
selectedPatterns,
|
||||
selectedPatternsCurrent,
|
||||
savedObjectId,
|
||||
selectedDataViewIdSourcerer,
|
||||
selectedDataViewIdTimeline,
|
||||
selectedPatternsSourcerer,
|
||||
selectedPatternsTimeline,
|
||||
timelineId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
onDataViewChange();
|
||||
onSourcererChange();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataViewId, selectedPatterns]);
|
||||
}, [selectedDataViewIdSourcerer, selectedPatternsSourcerer]);
|
||||
|
||||
const onSkipFocusBeforeEventsTable = useCallback(() => {
|
||||
const exitFullScreenButton = containerElement.current?.querySelector<HTMLButtonElement>(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
|
@ -19,10 +19,9 @@ import {
|
|||
} from '../../../../../common/types/timeline';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { inputsActions, inputsSelectors } from '../../../../common/store/inputs';
|
||||
import { sourcererActions } from '../../../../common/store/sourcerer';
|
||||
import { sourcererActions, sourcererSelectors } from '../../../../common/store/sourcerer';
|
||||
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
|
||||
import { appActions } from '../../../../common/store/app';
|
||||
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
|
||||
|
||||
interface Props {
|
||||
timelineId?: string;
|
||||
|
@ -32,7 +31,9 @@ interface Props {
|
|||
|
||||
export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { dataViewId, selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const defaultDataViewSelector = useMemo(() => sourcererSelectors.defaultDataViewSelector(), []);
|
||||
const { id: dataViewId, patternList: selectedPatterns } =
|
||||
useDeepEqualSelector(defaultDataViewSelector);
|
||||
|
||||
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
|
||||
const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector);
|
||||
|
|
|
@ -191,6 +191,7 @@ describe('Timeline', () => {
|
|||
loading: true,
|
||||
indexPattern: {},
|
||||
selectedPatterns: [],
|
||||
missingPatterns: [],
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -190,6 +190,8 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
loading: loadingSourcerer,
|
||||
indexPattern,
|
||||
runtimeMappings,
|
||||
// important to get selectedPatterns from useSourcererDataView
|
||||
// in order to include the exclude filters in the search that are not stored in the timeline
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const { uiSettings } = useKibana().services;
|
||||
|
|
|
@ -22,7 +22,7 @@ export const timelineDefaults: SubsetTimelineModel &
|
|||
documentType: '',
|
||||
defaultColumns: defaultHeaders,
|
||||
dataProviders: [],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: { start, end },
|
||||
deletedEventIds: [],
|
||||
description: '',
|
||||
|
|
|
@ -89,7 +89,7 @@ describe('Epic Timeline', () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
deletedEventIds: [],
|
||||
description: '',
|
||||
documentType: '',
|
||||
|
@ -239,7 +239,7 @@ describe('Epic Timeline', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: {
|
||||
end: '2019-10-31T21:06:27.644Z',
|
||||
start: '2019-10-30T21:06:27.644Z',
|
||||
|
|
|
@ -85,7 +85,7 @@ const basicTimeline: TimelineModel = {
|
|||
columns: [],
|
||||
defaultColumns: [],
|
||||
dataProviders: [{ ...basicDataProvider }],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: {
|
||||
start: '2020-07-07T08:20:18.966Z',
|
||||
end: '2020-07-08T08:20:18.966Z',
|
||||
|
@ -220,7 +220,7 @@ describe('Timeline', () => {
|
|||
const update = addNewTimeline({
|
||||
id: 'bar',
|
||||
columns: defaultHeaders,
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
indexNames: [],
|
||||
timelineById: timelineByIdMock,
|
||||
timelineType: TimelineType.default,
|
||||
|
@ -232,7 +232,7 @@ describe('Timeline', () => {
|
|||
const update = addNewTimeline({
|
||||
id: 'bar',
|
||||
columns: timelineDefaults.columns,
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
indexNames: [],
|
||||
timelineById: timelineByIdMock,
|
||||
timelineType: TimelineType.default,
|
||||
|
@ -250,7 +250,7 @@ describe('Timeline', () => {
|
|||
const update = addNewTimeline({
|
||||
id: 'bar',
|
||||
columns: defaultHeaders,
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
indexNames: [],
|
||||
timelineById: timelineByIdMock,
|
||||
timelineType: TimelineType.default,
|
||||
|
|
|
@ -163,7 +163,7 @@ export const mockTemplate = {
|
|||
and: [],
|
||||
},
|
||||
],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
description: '',
|
||||
eventType: 'all',
|
||||
excludedRowRendererIds: [],
|
||||
|
|
|
@ -28,7 +28,10 @@ export const FieldBrowserWrappedComponent = (props: FieldBrowserWrappedComponent
|
|||
return (
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<StatefulFieldsBrowser {...fieldsBrowseProps} />
|
||||
<StatefulFieldsBrowser
|
||||
data-test-ref="steph-loves-this-fields-browser"
|
||||
{...fieldsBrowseProps}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
|
|
@ -9,10 +9,14 @@ import { mount } from 'enzyme';
|
|||
import React from 'react';
|
||||
|
||||
import { TestProviders, mockBrowserFields, defaultHeaders } from '../../../../mock';
|
||||
import { mockGlobalState } from '../../../../mock/global_state';
|
||||
import { tGridActions } from '../../../../store/t_grid';
|
||||
|
||||
import { FieldsBrowser } from './field_browser';
|
||||
|
||||
import { createStore, State } from '../../../../types';
|
||||
import { createSecuritySolutionStorageMock } from '../../../../mock/mock_local_storage';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
@ -21,26 +25,30 @@ jest.mock('react-redux', () => {
|
|||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
const timelineId = 'test';
|
||||
const onHide = jest.fn();
|
||||
const testProps = {
|
||||
columnHeaders: [],
|
||||
browserFields: mockBrowserFields,
|
||||
filteredBrowserFields: mockBrowserFields,
|
||||
searchInput: '',
|
||||
isSearching: false,
|
||||
onCategorySelected: jest.fn(),
|
||||
onHide,
|
||||
onSearchInputChange: jest.fn(),
|
||||
restoreFocusTo: React.createRef<HTMLButtonElement>(),
|
||||
selectedCategoryId: '',
|
||||
timelineId,
|
||||
};
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
describe('FieldsBrowser', () => {
|
||||
const timelineId = 'test';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
test('it renders the Close button', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -48,22 +56,9 @@ describe('FieldsBrowser', () => {
|
|||
});
|
||||
|
||||
test('it invokes the Close button', () => {
|
||||
const onHide = jest.fn();
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={onHide}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -74,19 +69,7 @@ describe('FieldsBrowser', () => {
|
|||
test('it renders the Reset Fields button', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -123,23 +106,9 @@ describe('FieldsBrowser', () => {
|
|||
});
|
||||
|
||||
test('it invokes onHide when the user clicks the Reset Fields button', () => {
|
||||
const onHide = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={onHide}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -151,19 +120,7 @@ describe('FieldsBrowser', () => {
|
|||
test('it renders the search', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -173,19 +130,7 @@ describe('FieldsBrowser', () => {
|
|||
test('it renders the categories pane', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -195,19 +140,7 @@ describe('FieldsBrowser', () => {
|
|||
test('it renders the fields pane', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -217,19 +150,7 @@ describe('FieldsBrowser', () => {
|
|||
test('focuses the search input when the component mounts', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -245,19 +166,7 @@ describe('FieldsBrowser', () => {
|
|||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={onSearchInputChange}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} onSearchInputChange={onSearchInputChange} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -272,25 +181,36 @@ describe('FieldsBrowser', () => {
|
|||
expect(onSearchInputChange).toBeCalledWith(inputText);
|
||||
});
|
||||
|
||||
test('it renders the CreateField button when createFieldComponent is provided', () => {
|
||||
test('does not render the CreateField button when createFieldComponent is provided without a dataViewId', () => {
|
||||
const MyTestComponent = () => <div>{'test'}</div>;
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FieldsBrowser
|
||||
columnHeaders={[]}
|
||||
browserFields={mockBrowserFields}
|
||||
filteredBrowserFields={mockBrowserFields}
|
||||
searchInput={''}
|
||||
isSearching={false}
|
||||
onCategorySelected={jest.fn()}
|
||||
onHide={jest.fn()}
|
||||
onSearchInputChange={jest.fn()}
|
||||
restoreFocusTo={React.createRef<HTMLButtonElement>()}
|
||||
selectedCategoryId={''}
|
||||
timelineId={timelineId}
|
||||
createFieldComponent={MyTestComponent}
|
||||
/>
|
||||
<FieldsBrowser {...testProps} createFieldComponent={MyTestComponent} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(MyTestComponent).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it renders the CreateField button when createFieldComponent is provided with a dataViewId', () => {
|
||||
const state: State = {
|
||||
...mockGlobalState,
|
||||
timelineById: {
|
||||
...mockGlobalState.timelineById,
|
||||
test: {
|
||||
...mockGlobalState.timelineById.test,
|
||||
dataViewId: 'security-solution-default',
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = createStore(state, storage);
|
||||
|
||||
const MyTestComponent = () => <div>{'test'}</div>;
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders store={store}>
|
||||
<FieldsBrowser {...testProps} createFieldComponent={MyTestComponent} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -136,7 +136,9 @@ const FieldsBrowserComponent: React.FC<Props> = ({
|
|||
}, [onHide, restoreFocusTo]);
|
||||
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
const { defaultColumns } = useDeepEqualSelector((state) => getManageTimeline(state, timelineId));
|
||||
const { dataViewId, defaultColumns } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, timelineId)
|
||||
);
|
||||
|
||||
const onResetColumns = useCallback(() => {
|
||||
onUpdateColumns(defaultColumns);
|
||||
|
@ -209,7 +211,9 @@ const FieldsBrowserComponent: React.FC<Props> = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{CreateField && <CreateField onClick={onHide} />}
|
||||
{CreateField && dataViewId != null && dataViewId.length > 0 && (
|
||||
<CreateField onClick={onHide} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export const mockGlobalState: TimelineState = {
|
|||
end: '2020-07-08T08:20:18.966Z',
|
||||
},
|
||||
dataProviders: [],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
deletedEventIds: [],
|
||||
excludedRowRendererIds: [],
|
||||
expandedDetail: {},
|
||||
|
|
|
@ -1552,7 +1552,7 @@ export const mockTgridModel: TGridModel = {
|
|||
},
|
||||
],
|
||||
dataProviders: [],
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
defaultColumns: [],
|
||||
queryFields: [],
|
||||
dateRange: {
|
||||
|
|
|
@ -63,7 +63,7 @@ export const defaultHeaders: ColumnHeaderOptions[] = [
|
|||
export const tGridDefaults: SubsetTGridModel = {
|
||||
columns: defaultHeaders,
|
||||
defaultColumns: defaultHeaders,
|
||||
dataViewId: '',
|
||||
dataViewId: null,
|
||||
dateRange: { start: '', end: '' },
|
||||
deletedEventIds: [],
|
||||
excludedRowRendererIds: [],
|
||||
|
|
|
@ -50,7 +50,7 @@ export interface TGridModel extends TGridModelSettings {
|
|||
end: string;
|
||||
};
|
||||
/** Kibana data view id **/
|
||||
dataViewId: string;
|
||||
dataViewId: string | null; // null if legacy pre-8.0 timeline
|
||||
/** Events to not be rendered **/
|
||||
deletedEventIds: string[];
|
||||
/** This holds the view information for the flyout when viewing timeline in a consuming view (i.e. hosts page) or the side panel in the primary timeline view */
|
||||
|
|
|
@ -40,6 +40,7 @@ export const getManageTimelineById = () =>
|
|||
createSelector(
|
||||
selectTGridById,
|
||||
({
|
||||
dataViewId,
|
||||
documentType,
|
||||
defaultColumns,
|
||||
isLoading,
|
||||
|
@ -50,6 +51,7 @@ export const getManageTimelineById = () =>
|
|||
selectAll,
|
||||
title,
|
||||
}) => ({
|
||||
dataViewId,
|
||||
documentType,
|
||||
defaultColumns,
|
||||
isLoading,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue