diff --git a/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx b/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx index 34e797c0b4be4..7a6756bfe80ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx +++ b/x-pack/plugins/security_solution/public/common/components/discover_in_timeline/use_discover_in_timeline_actions.tsx @@ -58,7 +58,7 @@ export const useDiscoverInTimelineActions = ( const queryClient = useQueryClient(); - const { mutateAsync: saveSavedSearch, status } = useMutation({ + const { mutateAsync: saveSavedSearch, status: saveSavedSearchStatus } = useMutation({ mutationFn: ({ savedSearch, savedSearchOptions, @@ -189,7 +189,7 @@ export const useDiscoverInTimelineActions = ( * * */ const updateSavedSearch = useCallback( - async (savedSearch: SavedSearch, timelineId: string) => { + async (savedSearch: SavedSearch, timelineId: string, onUpdate?: () => void) => { savedSearch.timeRestore = true; savedSearch.timeRange = savedSearch.timeRange ?? discoverDataService.query.timefilter.timefilter.getTime(); @@ -219,7 +219,7 @@ export const useDiscoverInTimelineActions = ( // If no saved search exists. Create a new saved search instance and associate it with the timeline. try { // Make sure we're not creating a saved search while a previous creation call is in progress - if (status !== 'idle') { + if (saveSavedSearchStatus === 'loading') { return; } dispatch( @@ -244,6 +244,7 @@ export const useDiscoverInTimelineActions = ( ); // Also save the timeline, this will only happen once, in case there is no saved search id yet dispatch(timelineActions.saveTimeline({ id: TimelineId.active, saveAsNew: false })); + onUpdate?.(); } } catch (err) { dispatch( @@ -254,7 +255,7 @@ export const useDiscoverInTimelineActions = ( } } }, - [persistSavedSearch, savedSearchId, dispatch, discoverDataService, status] + [persistSavedSearch, savedSearchId, dispatch, discoverDataService, saveSavedSearchStatus] ); const initializeLocalSavedSearch = useCallback( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx index 4f1127e9beef4..1567c8b2e2e0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/esql/index.tsx @@ -18,6 +18,7 @@ import { isEqualWith } from 'lodash'; import type { SavedSearch } from '@kbn/saved-search-plugin/common'; import type { TimeRange } from '@kbn/es-query'; import { useDispatch } from 'react-redux'; +import { updateSavedSearchId } from '../../../../store/actions'; import { useDiscoverInTimelineContext } from '../../../../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import { useSourcererDataView } from '../../../../../common/containers/sourcerer'; import { useKibana } from '../../../../../common/lib/kibana'; @@ -89,7 +90,11 @@ export const DiscoverTabContent: FC = ({ timelineId }) ); const { status, savedSearchId, activeTab, savedObjectId, title, description } = timeline; - const { data: savedSearchById, isFetching } = useQuery({ + const { + data: savedSearchById, + isFetching, + status: savedSearchByIdStatus, + } = useQuery({ queryKey: ['savedSearchById', savedSearchId ?? ''], queryFn: () => (savedSearchId ? savedSearchService.get(savedSearchId) : Promise.resolve(null)), }); @@ -117,6 +122,12 @@ export const DiscoverTabContent: FC = ({ timelineId }) useEffect(() => { if (isFetching) return; + if (savedSearchByIdStatus === 'error' && savedSearchId) { + // when a timeline json is uploaded with a saved search Id that not longer + // exists, we need to reset the saved search Id in the timeline and remove th saved search + dispatch(updateSavedSearchId({ id: timelineId, savedSearchId: null })); + return; + } if (!savedObjectId) return; if (!status || status === 'draft') return; const latestState = getCombinedDiscoverSavedSearchState(); @@ -126,8 +137,9 @@ export const DiscoverTabContent: FC = ({ timelineId }) if (!index) return; if (!latestState || combinedDiscoverSavedSearchStateRef.current === latestState) return; if (isEqualWith(latestState, savedSearchById, savedSearchComparator)) return; - updateSavedSearch(latestState, timelineId); - combinedDiscoverSavedSearchStateRef.current = latestState; + updateSavedSearch(latestState, timelineId, function onUpdate() { + combinedDiscoverSavedSearchStateRef.current = latestState; + }); }, [ getCombinedDiscoverSavedSearchState, savedSearchById, @@ -139,6 +151,8 @@ export const DiscoverTabContent: FC = ({ timelineId }) isFetching, timelineId, dispatch, + savedSearchId, + savedSearchByIdStatus, ]); useEffect(() => { @@ -166,9 +180,14 @@ export const DiscoverTabContent: FC = ({ timelineId }) setDiscoverStateContainer(stateContainer); let savedSearchAppState; if (savedSearchId) { - const localSavedSearch = await savedSearchService.get(savedSearchId); - initializeLocalSavedSearch(localSavedSearch, timelineId); - savedSearchAppState = getAppStateFromSavedSearch(localSavedSearch); + try { + const localSavedSearch = await savedSearchService.get(savedSearchId); + initializeLocalSavedSearch(localSavedSearch, timelineId); + savedSearchAppState = getAppStateFromSavedSearch(localSavedSearch); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Stale Saved search Id which no longer exists', e); + } } const finalAppState = diff --git a/x-pack/plugins/security_solution/public/timelines/store/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/actions.ts index bb38d56773d15..ca247c855a06c 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/actions.ts @@ -272,7 +272,7 @@ export const updateTotalCount = actionCreator<{ id: string; totalCount: number } export const updateSavedSearchId = actionCreator<{ id: string; - savedSearchId: string; + savedSearchId: string | null; }>('UPDATE_DISCOVER_SAVED_SEARCH_ID'); export const initializeSavedSearch = actionCreator<{