[Security Solution] [Sourcerer] Bug fixes for hanging load and modified label (#120814) (#121123)

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-13 16:33:14 -05:00 committed by GitHub
parent c918e7a620
commit eeec2e7246
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 307 additions and 119 deletions

View file

@ -6,12 +6,7 @@
*/
import {
EuiButton,
EuiCallOut,
EuiCheckbox,
EuiComboBox,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiOutsideClickDetector,
EuiPopover,
@ -19,7 +14,7 @@ import {
EuiSpacer,
EuiSuperSelect,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { ChangeEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import * as i18n from './translations';
@ -27,12 +22,12 @@ import { sourcererActions, sourcererModel, sourcererSelectors } from '../../stor
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { usePickIndexPatterns } from './use_pick_index_patterns';
import { FormRow, PopoverContent, ResetButton, StyledButton, StyledFormRow } from './helpers';
import { FormRow, PopoverContent, 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';
import { AlertsCheckbox, SaveButtons, SourcererCallout } from './sub_components';
interface SourcererComponentProps {
scope: sourcererModel.SourcererScopeName;
@ -91,11 +86,12 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
kibanaDataViews,
missingPatterns,
scopeId,
selectedDataViewId,
selectedPatterns,
signalIndexName,
});
const onCheckboxChanged = useCallback(
const onCheckboxChanged: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setIsOnlyDetectionAlertsChecked(e.target.checked);
setDataViewId(defaultDataView.id);
@ -251,49 +247,35 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
<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}
/>
)}
<SourcererCallout
isOnlyDetectionAlerts={isOnlyDetectionAlerts}
title={isTimelineSourcerer ? i18n.CALL_OUT_TIMELINE_TITLE : i18n.CALL_OUT_TITLE}
/>
<EuiSpacer size="s" />
{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}
/>
</>
{(dataViewId === null && isModified === 'deprecated') ||
isModified === 'missingPatterns' ? (
<TemporarySourcerer
activePatterns={activePatterns}
indicesExist={indicesExist}
isModified={isModified}
isShowingUpdateModal={isShowingUpdateModal}
missingPatterns={missingPatterns}
onContinueWithoutUpdate={onContinueUpdateDeprecated}
onDismiss={setPopoverIsOpenCb}
onDismissModal={() => setIsShowingUpdateModal(false)}
onReset={resetDataSources}
onUpdateStepOne={isModified === 'deprecated' ? onUpdateDeprecated : onUpdateDataView}
onUpdateStepTwo={onUpdateDataView}
selectedPatterns={selectedPatterns}
/>
) : (
<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>
)}
<AlertsCheckbox
isShow={isTimelineSourcerer}
checked={isOnlyDetectionAlertsChecked}
onChange={onCheckboxChanged}
/>
{dataViewId && (
<StyledFormRow label={i18n.INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL}>
<EuiSuperSelect
@ -335,35 +317,12 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
/>
</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>
)}
<SaveButtons
disableSave={selectedOptions.length === 0}
isShow={!isDetectionsSourcerer}
onReset={resetDataSources}
onSave={handleSaveIndices}
/>
</>
<EuiSpacer size="s" />
</EuiForm>

View file

@ -0,0 +1,89 @@
/*
* 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, { ChangeEventHandler } from 'react';
import { EuiButton, EuiCallOut, EuiCheckbox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ResetButton, StyledFormRow } from './helpers';
import * as i18n from './translations';
interface SourcererCalloutProps {
isOnlyDetectionAlerts: boolean;
title: string;
}
export const SourcererCallout = React.memo<SourcererCalloutProps>(
({ isOnlyDetectionAlerts, title }) =>
isOnlyDetectionAlerts ? (
<EuiCallOut data-test-subj="sourcerer-callout" iconType="iInCircle" size="s" title={title} />
) : null
);
SourcererCallout.displayName = 'SourcererCallout';
interface AlertsCheckboxProps {
checked: boolean;
isShow: boolean;
onChange: ChangeEventHandler<HTMLInputElement>;
}
export const AlertsCheckbox = React.memo<AlertsCheckboxProps>(({ onChange, checked, isShow }) =>
isShow ? (
<StyledFormRow>
<EuiCheckbox
checked={checked}
data-test-subj="sourcerer-alert-only-checkbox"
id="sourcerer-alert-only-checkbox"
label={i18n.ALERTS_CHECKBOX_LABEL}
onChange={onChange}
/>
</StyledFormRow>
) : null
);
AlertsCheckbox.displayName = 'AlertsCheckbox';
interface SaveButtonsProps {
disableSave: boolean;
isShow: boolean;
onReset: () => void;
onSave: () => void;
}
export const SaveButtons = React.memo<SaveButtonsProps>(
({ disableSave, isShow, onReset, onSave }) =>
isShow ? (
<StyledFormRow>
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<ResetButton
aria-label={i18n.INDEX_PATTERNS_RESET}
data-test-subj="sourcerer-reset"
flush="left"
onClick={onReset}
title={i18n.INDEX_PATTERNS_RESET}
>
{i18n.INDEX_PATTERNS_RESET}
</ResetButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSave}
disabled={disableSave}
data-test-subj="sourcerer-save"
fill
fullWidth
size="s"
>
{i18n.SAVE_INDEX_PATTERNS}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</StyledFormRow>
) : null
);
SaveButtons.displayName = 'SaveButtons';

View file

@ -21,14 +21,15 @@ import {
import React, { useMemo } from 'react';
import * as i18n from './translations';
import { Blockquote, ResetButton } from './helpers';
import { UpdateDefaultDataViewModal } from './update_default_data_view_modal';
interface Props {
activePatterns?: string[];
indicesExist: boolean;
isModified: 'deprecated' | 'missingPatterns';
missingPatterns: string[];
onClick: () => void;
onClose: () => void;
onDismiss: () => void;
onReset: () => void;
onUpdate: () => void;
selectedPatterns: string[];
}
@ -44,13 +45,13 @@ const translations = {
},
};
export const TemporarySourcerer = React.memo<Props>(
export const TemporarySourcererComp = React.memo<Props>(
({
activePatterns,
indicesExist,
isModified,
onClose,
onClick,
onDismiss,
onReset,
onUpdate,
selectedPatterns,
missingPatterns,
@ -141,7 +142,7 @@ export const TemporarySourcerer = React.memo<Props>(
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>,
link: <EuiLink onClick={onReset}>{i18n.TOGGLE_TO_NEW_SOURCERER}</EuiLink>,
}}
/>
)}
@ -158,7 +159,7 @@ export const TemporarySourcerer = React.memo<Props>(
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>,
link: <EuiLink onClick={onReset}>{i18n.TOGGLE_TO_NEW_SOURCERER}</EuiLink>,
}}
/>
</>
@ -172,7 +173,7 @@ export const TemporarySourcerer = React.memo<Props>(
aria-label={i18n.INDEX_PATTERNS_CLOSE}
data-test-subj="sourcerer-deprecated-close"
flush="left"
onClick={onClose}
onClick={onDismiss}
title={i18n.INDEX_PATTERNS_CLOSE}
>
{i18n.INDEX_PATTERNS_CLOSE}
@ -185,4 +186,58 @@ export const TemporarySourcerer = React.memo<Props>(
}
);
TemporarySourcererComp.displayName = 'TemporarySourcererComp';
interface TemporarySourcererProps {
activePatterns?: string[];
indicesExist: boolean;
isModified: 'deprecated' | 'missingPatterns';
isShowingUpdateModal: boolean;
missingPatterns: string[];
onContinueWithoutUpdate: () => void;
onDismiss: () => void;
onDismissModal: () => void;
onReset: () => void;
onUpdateStepOne: () => void;
onUpdateStepTwo: () => void;
selectedPatterns: string[];
}
export const TemporarySourcerer = React.memo<TemporarySourcererProps>(
({
activePatterns,
indicesExist,
isModified,
missingPatterns,
onContinueWithoutUpdate,
onDismiss,
onReset,
onUpdateStepOne,
onUpdateStepTwo,
selectedPatterns,
isShowingUpdateModal,
onDismissModal,
}) => (
<>
<TemporarySourcererComp
activePatterns={activePatterns}
indicesExist={indicesExist}
isModified={isModified}
missingPatterns={missingPatterns}
onDismiss={onDismiss}
onReset={onReset}
onUpdate={onUpdateStepOne}
selectedPatterns={selectedPatterns}
/>
<UpdateDefaultDataViewModal
isShowing={isShowingUpdateModal}
missingPatterns={missingPatterns}
onDismissModal={onDismissModal}
onContinue={onContinueWithoutUpdate}
onUpdate={onUpdateStepTwo}
/>
</>
)
);
TemporarySourcerer.displayName = 'TemporarySourcerer';

View file

@ -25,7 +25,7 @@ import { Blockquote, ResetButton } from './helpers';
interface Props {
isShowing: boolean;
missingPatterns: string[];
onClose: () => void;
onDismissModal: () => void;
onContinue: () => void;
onUpdate: () => void;
}
@ -41,9 +41,9 @@ const MyEuiModal = styled(EuiModal)`
`;
export const UpdateDefaultDataViewModal = React.memo<Props>(
({ isShowing, onClose, onContinue, onUpdate, missingPatterns }) =>
({ isShowing, onDismissModal, onContinue, onUpdate, missingPatterns }) =>
isShowing ? (
<MyEuiModal onClose={onClose} data-test-subj="sourcerer-update-data-view-modal">
<MyEuiModal onClose={onDismissModal} data-test-subj="sourcerer-update-data-view-modal">
<EuiModalHeader>
<EuiModalHeaderTitle>
<h1>{i18n.UPDATE_SECURITY_DATA_VIEW}</h1>

View file

@ -19,6 +19,7 @@ interface UsePickIndexPatternsProps {
kibanaDataViews: sourcererModel.SourcererModel['kibanaDataViews'];
missingPatterns: string[];
scopeId: sourcererModel.SourcererScopeName;
selectedDataViewId: string | null;
selectedPatterns: string[];
signalIndexName: string | null;
}
@ -49,6 +50,7 @@ export const usePickIndexPatterns = ({
kibanaDataViews,
missingPatterns,
scopeId,
selectedDataViewId,
selectedPatterns,
signalIndexName,
}: UsePickIndexPatternsProps): UsePickIndexPatterns => {
@ -155,11 +157,11 @@ export const usePickIndexPatterns = ({
// when scope updates, check modified to set/remove alerts label
useEffect(() => {
onSetIsModified(
selectedOptions.map(({ label }) => label),
dataViewId
selectedPatterns.map((pattern) => pattern),
selectedDataViewId
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataViewId, missingPatterns, scopeId, selectedOptions]);
}, [selectedDataViewId, missingPatterns, scopeId, selectedPatterns]);
const onChangeCombo = useCallback((newSelectedOptions) => {
setSelectedOptions(newSelectedOptions);

View file

@ -39,8 +39,8 @@ const getEsFields = memoizeOne(
export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string) => void } => {
const { data } = useKibana().services;
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const abortCtrl = useRef<Record<string, AbortController>>({});
const searchSubscription$ = useRef<Record<string, Subscription>>({});
const dispatch = useDispatch();
const { addError, addWarning } = useAppToasts();
@ -54,16 +54,19 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
const indexFieldsSearch = useCallback(
(selectedDataViewId: string) => {
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
abortCtrl.current = {
...abortCtrl.current,
[selectedDataViewId]: new AbortController(),
};
setLoading({ id: selectedDataViewId, loading: true });
searchSubscription$.current = data.search
const subscription = data.search
.search<IndexFieldsStrategyRequest<'dataView'>, IndexFieldsStrategyResponse>(
{
dataViewId: selectedDataViewId,
onlyCheckIfIndicesExist: false,
},
{
abortSignal: abortCtrl.current.signal,
abortSignal: abortCtrl.current[selectedDataViewId].signal,
strategy: 'indexFields',
}
)
@ -82,11 +85,15 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
runtimeMappings: response.runtimeMappings,
})
);
searchSubscription$.current.unsubscribe();
if (searchSubscription$.current[selectedDataViewId]) {
searchSubscription$.current[selectedDataViewId].unsubscribe();
}
} else if (isErrorResponse(response)) {
setLoading({ id: selectedDataViewId, loading: false });
addWarning(i18n.ERROR_BEAT_FIELDS);
searchSubscription$.current.unsubscribe();
if (searchSubscription$.current[selectedDataViewId]) {
searchSubscription$.current[selectedDataViewId].unsubscribe();
}
}
},
error: (msg) => {
@ -98,12 +105,23 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
addError(msg, {
title: i18n.FAIL_BEAT_FIELDS,
});
searchSubscription$.current.unsubscribe();
if (searchSubscription$.current[selectedDataViewId]) {
searchSubscription$.current[selectedDataViewId].unsubscribe();
}
},
});
searchSubscription$.current = {
...searchSubscription$.current,
[selectedDataViewId]: subscription,
};
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
if (searchSubscription$.current[selectedDataViewId] != null) {
searchSubscription$.current[selectedDataViewId].unsubscribe();
}
if (abortCtrl.current[selectedDataViewId] != null) {
abortCtrl.current[selectedDataViewId].abort();
}
asyncSearch();
},
[addError, addWarning, data.search, dispatch, setLoading]
@ -111,8 +129,10 @@ export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string)
useEffect(() => {
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
Object.values(searchSubscription$.current).forEach((subscription) =>
subscription.unsubscribe()
);
Object.values(abortCtrl.current).forEach((signal) => signal.abort());
};
}, []);

View file

@ -52,13 +52,15 @@ jest.mock('../../utils/route/use_route_spy', () => ({
useRouteSpy: () => [mockRouteSpy],
}));
const mockSearch = jest.fn();
jest.mock('../../lib/kibana', () => ({
useToasts: jest.fn().mockReturnValue({
useToasts: () => ({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useKibana: jest.fn().mockReturnValue({
useKibana: () => ({
services: {
application: {
capabilities: {
@ -72,7 +74,7 @@ jest.mock('../../lib/kibana', () => ({
getTitles: jest.fn().mockImplementation(() => Promise.resolve(mockPatterns)),
},
search: {
search: jest.fn().mockImplementation(() => ({
search: mockSearch.mockImplementation(() => ({
subscribe: jest.fn().mockImplementation(() => ({
error: jest.fn(),
next: jest.fn(),
@ -188,6 +190,8 @@ describe('Sourcerer Hooks', () => {
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
payload: { loading: false },
});
expect(mockDispatch).toHaveBeenCalledTimes(7);
expect(mockSearch).toHaveBeenCalledTimes(2);
});
});
});
@ -216,6 +220,48 @@ describe('Sourcerer Hooks', () => {
});
});
});
it('index field search is not repeated when default and timeline have same dataViewId', async () => {
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockSearch).toHaveBeenCalledTimes(1);
});
});
});
it('index field search called twice when default and timeline have different dataViewId', async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: 'different-id',
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
await waitFor(() => {
expect(mockSearch).toHaveBeenCalledTimes(2);
});
});
});
describe('useSourcererDataView', () => {
it('Should exclude elastic cloud alias when selected patterns include "logs-*" as an alias', async () => {

View file

@ -84,8 +84,15 @@ export const useInitSourcerer = (
);
const { indexFieldsSearch } = useDataView();
const searchedIds = useRef<string[]>([]);
useEffect(
() => activeDataViewIds.forEach((id) => id != null && id.length > 0 && indexFieldsSearch(id)),
() =>
activeDataViewIds.forEach((id) => {
if (id != null && id.length > 0 && !searchedIds.current.includes(id)) {
searchedIds.current = [...searchedIds.current, id];
indexFieldsSearch(id);
}
}),
[activeDataViewIds, indexFieldsSearch]
);
@ -180,28 +187,33 @@ export const useInitSourcerer = (
},
[defaultDataView.title, dispatch, indexFieldsSearch, addError]
);
useEffect(() => {
const onSignalIndexUpdated = useCallback(() => {
if (
!loadingSignalIndex &&
signalIndexName != null &&
signalIndexNameSourcerer == null &&
defaultDataView.id.length > 0
) {
// update signal name also updates sourcerer
// we hit this the first time signal index is created
updateSourcererDataView(signalIndexName);
dispatch(sourcererActions.setSignalIndexName({ signalIndexName }));
}
}, [
defaultDataView.id,
defaultDataView.id.length,
dispatch,
indexFieldsSearch,
isSignalIndexExists,
loadingSignalIndex,
signalIndexName,
signalIndexNameSourcerer,
updateSourcererDataView,
]);
useEffect(() => {
onSignalIndexUpdated();
// because we only want onSignalIndexUpdated to run when signalIndexName updates,
// but we want to know about the updates from the dependencies of onSignalIndexUpdated
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signalIndexName]);
// Related to the detection page
useEffect(() => {
if (

View file

@ -282,13 +282,13 @@ export const EqlTabContentComponent: React.FC<Props> = ({
setFullScreen={setTimelineFullScreen}
/>
)}
<DatePicker grow={1}>
<DatePicker grow={10}>
<SuperDatePicker id="timeline" timelineId={timelineId} />
</DatePicker>
<EuiFlexItem grow={false}>
<TimelineDatePickerLock />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={1}>
{activeTab === TimelineTabs.eql && (
<Sourcerer scope={SourcererScopeName.timeline} />
)}

View file

@ -120,9 +120,14 @@ const DatePicker = styled(EuiFlexItem)`
width: auto;
}
`;
DatePicker.displayName = 'DatePicker';
const SourcererFlex = styled(EuiFlexItem)`
align-items: flex-end;
`;
SourcererFlex.displayName = 'SourcererFlex';
const VerticalRule = styled.div`
width: 2px;
height: 100%;
@ -355,7 +360,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
setFullScreen={setTimelineFullScreen}
/>
)}
<DatePicker grow={1}>
<DatePicker grow={10}>
<SuperDatePicker
id="timeline"
timelineId={timelineId}
@ -365,11 +370,11 @@ export const QueryTabContentComponent: React.FC<Props> = ({
<EuiFlexItem grow={false}>
<TimelineDatePickerLock />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SourcererFlex grow={1}>
{activeTab === TimelineTabs.query && (
<Sourcerer scope={SourcererScopeName.timeline} />
)}
</EuiFlexItem>
</SourcererFlex>
</EuiFlexGroup>
<TimelineHeaderContainer data-test-subj="timelineHeader">
<TimelineHeader