mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
ee7bc7ed83
commit
db28b89f61
6 changed files with 37 additions and 13 deletions
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue