[Bug][Security Solution] - Reliably persist dataview selections for timeline (#211343)

resolves https://github.com/elastic/kibana/issues/198944

## Summary

Currently, the redux store can become out of sync with the state in the
UI, leading to the selected dataview not being preserved in the store,
and thereby not being saved when the timeline is saved. This PR sets the
selected dataview and patterns at the point of saving to ensure that
they are set and not overriden.

For additional background, see referenced issues.
This commit is contained in:
Michael Olorunnisola 2025-03-06 16:09:22 -05:00 committed by GitHub
parent ccae358d37
commit 4abf1a151e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 7 deletions

View file

@ -12,7 +12,7 @@ import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { TimelineTypeEnum, TimelineStatusEnum } from '../../../../common/api/timeline';
import { convertTimelineAsInput } from './timeline_save';
import type { TimelineModel } from '../model';
import { createMockStore, kibanaMock } from '../../../common/mock';
import { createMockStore, kibanaMock, mockGlobalState } from '../../../common/mock';
import { selectTimelineById } from '../selectors';
import { copyTimeline, persistTimeline } from '../../containers/api';
import { refreshTimelines } from './helpers';
@ -71,7 +71,14 @@ describe('Timeline save middleware', () => {
await store.dispatch(saveTimeline({ id: TimelineId.test, saveAsNew: false }));
expect(startTimelineSavingMock).toHaveBeenCalled();
expect(persistTimeline as unknown as jest.Mock).toHaveBeenCalled();
expect(persistTimeline as unknown as jest.Mock).toHaveBeenCalledWith(
expect.objectContaining({
timeline: expect.objectContaining({
dataViewId: mockGlobalState.sourcerer.sourcererScopes.timeline.selectedDataViewId,
indexNames: mockGlobalState.sourcerer.sourcererScopes.timeline.selectedPatterns,
}),
})
);
expect(refreshTimelines as unknown as jest.Mock).toHaveBeenCalled();
expect(endTimelineSavingMock).toHaveBeenCalled();
expect(selectTimelineById(store.getState(), TimelineId.test)).toEqual(

View file

@ -20,6 +20,7 @@ import {
isPhrasesFilter,
} from '@kbn/es-query';
import { sourcererSelectors } from '../../../sourcerer/store';
import {
updateTimeline,
startTimelineSaving,
@ -43,6 +44,7 @@ import type {
import type { TimelineModel } from '../model';
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
import { refreshTimelines } from './helpers';
import { SourcererScopeName } from '../../../sourcerer/store/model';
function isSaveTimelineAction(action: Action): action is ReturnType<typeof saveTimeline> {
return action.type === saveTimeline.type;
@ -55,10 +57,19 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
if (isSaveTimelineAction(action)) {
const { id: localTimelineId } = action.payload;
const timeline = selectTimelineById(store.getState(), localTimelineId);
const storeState = store.getState();
const timeline = selectTimelineById(storeState, localTimelineId);
const { timelineId, timelineVersion, templateTimelineId, templateTimelineVersion } =
extractTimelineIdsAndVersions(timeline);
const timelineTimeRange = inputsSelectors.timelineTimeRangeSelector(store.getState());
const timelineTimeRange = inputsSelectors.timelineTimeRangeSelector(storeState);
const selectedDataViewIdSourcerer = sourcererSelectors.sourcererScopeSelectedDataViewId(
storeState,
SourcererScopeName.timeline
);
const selectedPatterns = sourcererSelectors.sourcererScopeSelectedPatterns(
storeState,
SourcererScopeName.timeline
);
store.dispatch(startTimelineSaving({ id: localTimelineId }));
@ -68,6 +79,8 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
timelineId,
timeline: {
...convertTimelineAsInput(timeline, timelineTimeRange),
dataViewId: selectedDataViewIdSourcerer,
indexNames: selectedPatterns,
templateTimelineId,
templateTimelineVersion,
},
@ -78,6 +91,8 @@ export const saveTimelineMiddleware: (kibana: CoreStart) => Middleware<{}, State
version: timelineVersion,
timeline: {
...convertTimelineAsInput(timeline, timelineTimeRange),
dataViewId: selectedDataViewIdSourcerer,
indexNames: selectedPatterns,
templateTimelineId,
templateTimelineVersion,
},

View file

@ -40,8 +40,7 @@ import { closeTimeline, openTimelineById } from '../../../tasks/timeline';
const siemDataViewTitle = 'Security Default Data View';
const dataViews = ['logs-*', 'metrics-*', '.kibana-event-log-*'];
// FLAKY: https://github.com/elastic/kibana/issues/198944
describe.skip('Timeline scope', { tags: ['@ess', '@serverless', '@skipInServerless'] }, () => {
describe('Timeline scope', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
waitForRulesBootstrap();
});
@ -132,7 +131,7 @@ describe.skip('Timeline scope', { tags: ['@ess', '@serverless', '@skipInServerle
});
const defaultPatterns = [`auditbeat-*`, `${DEFAULT_ALERTS_INDEX}-default`];
it('alerts checkbox behaves as expected', () => {
it('alerts checkbox behaves as expected', { tags: ['@skipInServerless'] }, () => {
isDataViewSelection(siemDataViewTitle);
defaultPatterns.forEach((pattern) => isSourcererSelection(pattern));
openDataViewSelection();