[8.x] [SecuritySolution][Notes] - make sure that timeline is saved before allowing users to save notes (#195842) (#196212)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[SecuritySolution][Notes] - make sure that timeline is saved before
allowing users to save notes
(#195842)](https://github.com/elastic/kibana/pull/195842)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Philippe
Oberti","email":"philippe.oberti@elastic.co"},"sourceCommit":{"committedDate":"2024-10-14T21:31:32Z","message":"[SecuritySolution][Notes]
- make sure that timeline is saved before allowing users to save notes
(#195842)","sha":"f7b808c543614d890ad2fd2477fd909f63a36c71","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["backport","release_note:skip","v9.0.0","Team:Threat
Hunting:Investigations","v8.16.0"],"title":"[SecuritySolution][Notes] -
make sure that timeline is saved before allowing users to save
notes","number":195842,"url":"https://github.com/elastic/kibana/pull/195842","mergeCommit":{"message":"[SecuritySolution][Notes]
- make sure that timeline is saved before allowing users to save notes
(#195842)","sha":"f7b808c543614d890ad2fd2477fd909f63a36c71"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195842","number":195842,"mergeCommit":{"message":"[SecuritySolution][Notes]
- make sure that timeline is saved before allowing users to save notes
(#195842)","sha":"f7b808c543614d890ad2fd2477fd909f63a36c71"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Philippe Oberti <philippe.oberti@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-15 10:21:16 +11:00 committed by GitHub
parent ee7bc7ed83
commit db28b89f61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 37 additions and 13 deletions

View file

@ -70,6 +70,7 @@ describe('AttachToActiveTimeline', () => {
[TimelineId.active]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
savedObjectId: 'savedObjectId',
status: 'active',
},
},
},
@ -104,6 +105,7 @@ describe('AttachToActiveTimeline', () => {
[TimelineId.active]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
savedObjectId: 'savedObjectId',
status: 'active',
},
},
},

View file

@ -10,6 +10,7 @@ import { EuiCallOut, EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiText } from '@el
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { useSelector } from 'react-redux';
import { TimelineStatusEnum } from '../../../../../common/api/timeline';
import type { State } from '../../../../common/store';
import { TimelineId } from '../../../../../common/types';
import { SaveTimelineButton } from '../../../../timelines/components/modal/actions/save_timeline_button';
@ -76,10 +77,9 @@ export const AttachToActiveTimeline = memo(
const timeline = useSelector((state: State) =>
timelineSelectors.selectTimelineById(state, TimelineId.active)
);
const timelineSavedObjectId = useMemo(() => timeline?.savedObjectId ?? '', [timeline]);
const isTimelineSaved: boolean = useMemo(
() => timelineSavedObjectId.length > 0,
[timelineSavedObjectId]
() => timeline.status === TimelineStatusEnum.active,
[timeline.status]
);
const onCheckboxChange = useCallback(

View file

@ -24,6 +24,8 @@ import { Flyouts } from '../../shared/constants/flyouts';
import { TimelineId } from '../../../../../common/types';
import { ReqStatus } from '../../../../notes';
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
import { TimelineStatusEnum } from '../../../../../common/api/timeline';
import type { State } from '../../../../common/store';
jest.mock('../../shared/hooks/use_which_flyout');
jest.mock('../../shared/hooks/use_basic_data_from_details_data');
@ -52,7 +54,7 @@ const panelContextValue = {
dataFormattedForFieldBrowser: [],
} as unknown as DocumentDetailsContext;
const mockGlobalStateWithSavedTimeline = {
const mockGlobalStateWithSavedTimeline: State = {
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
@ -61,6 +63,7 @@ const mockGlobalStateWithSavedTimeline = {
[TimelineId.active]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
savedObjectId: 'savedObjectId',
status: TimelineStatusEnum.active,
pinnedEventIds: {},
},
},

View file

@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingElastic, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data';
import type { TimelineModel } from '../../../..';
import { Flyouts } from '../../shared/constants/flyouts';
import { timelineSelectors } from '../../../../timelines/store';
import { TimelineId } from '../../../../../common/types';
@ -21,6 +22,7 @@ import { NotesList } from '../../../../notes/components/notes_list';
import { pinEvent } from '../../../../timelines/store/actions';
import type { State } from '../../../../common/store';
import type { Note } from '../../../../../common/api/timeline';
import { TimelineStatusEnum } from '../../../../../common/api/timeline';
import {
fetchNotesByDocumentIds,
ReqStatus,
@ -63,10 +65,17 @@ export const NotesDetails = memo(() => {
// if the flyout is open from a timeline and that timeline is saved, we automatically check the checkbox to associate the note to it
const isTimelineFlyout = useWhichFlyout() === Flyouts.timeline;
const timeline = useSelector((state: State) =>
const timeline: TimelineModel = useSelector((state: State) =>
timelineSelectors.selectTimelineById(state, TimelineId.active)
);
const timelineSavedObjectId = useMemo(() => timeline?.savedObjectId ?? '', [timeline]);
const timelineSavedObjectId = useMemo(
() => timeline.savedObjectId ?? '',
[timeline.savedObjectId]
);
const isTimelineSaved: boolean = useMemo(
() => timeline.status === TimelineStatusEnum.active,
[timeline.status]
);
// Automatically pin an associated event if it's attached to a timeline and it's not pinned yet
const onNoteAddInTimeline = useCallback(() => {
@ -141,7 +150,7 @@ export const NotesDetails = memo(() => {
{isTimelineFlyout && (
<AttachToActiveTimeline
setAttachToTimeline={setAttachToTimeline}
isCheckboxDisabled={timelineSavedObjectId.length === 0}
isCheckboxDisabled={!isTimelineSaved}
/>
)}
</AddNote>

View file

@ -18,6 +18,8 @@ import React from 'react';
import { TimelineId } from '../../../../../../common/types';
import { SAVE_TIMELINE_CALLOUT_TEST_ID } from '../../../notes/test_ids';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
import { TimelineStatusEnum } from '../../../../../../common/api/timeline';
import type { State } from '../../../../../common/store';
jest.mock('../../../../../common/hooks/use_experimental_features');
jest.mock('../../../../../common/components/user_privileges');
@ -38,7 +40,7 @@ jest.mock('react-redux', () => {
};
});
const mockGlobalStateWithSavedTimeline = {
const mockGlobalStateWithSavedTimeline: State = {
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
@ -47,11 +49,12 @@ const mockGlobalStateWithSavedTimeline = {
[TimelineId.active]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
savedObjectId: 'savedObjectId',
status: TimelineStatusEnum.active,
},
},
},
};
const mockGlobalStateWithUnSavedTimeline = {
const mockGlobalStateWithUnSavedTimeline: State = {
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,

View file

@ -21,6 +21,7 @@ import { css } from '@emotion/react';
import { useDispatch, useSelector } from 'react-redux';
import { FormattedRelative } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { TimelineModel } from '../../../../..';
import { SaveTimelineCallout } from '../../../notes/save_timeline';
import { AddNote } from '../../../../../notes/components/add_note';
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
@ -40,6 +41,7 @@ import {
selectSortedNotesBySavedObjectId,
} from '../../../../../notes';
import type { Note } from '../../../../../../common/api/timeline';
import { TimelineStatusEnum } from '../../../../../../common/api/timeline';
import { NotesList } from '../../../../../notes/components/notes_list';
import { OldNotes } from '../../../notes/old_notes';
import { Participants } from '../../../notes/participants';
@ -92,11 +94,16 @@ const NotesTabContentComponent: React.FC<NotesTabContentProps> = React.memo(({ t
const scrollToTop = useShallowEqualSelector((state) => getScrollToTop(state, timelineId));
useScrollToTop('#scrollableNotes', !!scrollToTop);
const timeline = useSelector((state: State) => selectTimelineById(state, timelineId));
const timelineSavedObjectId = useMemo(() => timeline?.savedObjectId ?? '', [timeline]);
const timeline: TimelineModel = useSelector((state: State) =>
selectTimelineById(state, timelineId)
);
const timelineSavedObjectId = useMemo(
() => timeline.savedObjectId ?? '',
[timeline.savedObjectId]
);
const isTimelineSaved: boolean = useMemo(
() => timelineSavedObjectId.length > 0,
[timelineSavedObjectId]
() => timeline.status === TimelineStatusEnum.active,
[timeline.status]
);
const fetchNotes = useCallback(