diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx new file mode 100644 index 00000000000000..2d4404fd750c05 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.test.tsx @@ -0,0 +1,83 @@ +/* + * 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 from 'react'; + +import { createMockStore, mockGlobalState } from '../../../../common/mock'; +import '../../../../common/mock/match_media'; +import { TestProviders } from '../../../../common/mock/test_providers'; + +import { TabsContent } from '.'; +import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline'; +import { TimelineType } from '../../../../../common/api/timeline'; +import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability'; +import { render, screen, waitFor } from '@testing-library/react'; + +jest.mock('../../../../common/hooks/esql/use_esql_availability', () => ({ + useEsqlAvailability: jest.fn().mockReturnValue({ + isESQLTabInTimelineEnabled: true, + }), +})); + +const useEsqlAvailabilityMock = useEsqlAvailability as jest.Mock; + +describe('Timeline', () => { + describe('esql tab', () => { + const esqlTabSubj = `timelineTabs-${TimelineTabs.esql}`; + const defaultProps = { + renderCellValue: () => {}, + rowRenderers: [], + timelineId: TimelineId.test, + timelineType: TimelineType.default, + timelineDescription: '', + }; + + it('should show the esql tab', () => { + render( + + + + ); + expect(screen.getByTestId(esqlTabSubj)).toBeVisible(); + }); + + it('should not show the esql tab when the advanced setting is disabled', async () => { + useEsqlAvailabilityMock.mockReturnValue({ + isESQLTabInTimelineEnabled: false, + }); + render( + + + + ); + + await waitFor(() => { + expect(screen.queryByTestId(esqlTabSubj)).toBeNull(); + }); + }); + + it('should show the esql tab when the advanced setting is disabled, but an esql query is present', async () => { + useEsqlAvailabilityMock.mockReturnValue({ + isESQLTabInTimelineEnabled: false, + }); + + const stateWithSavedSearchId = structuredClone(mockGlobalState); + stateWithSavedSearchId.timeline.timelineById[TimelineId.test].savedSearchId = 'test-id'; + const mockStore = createMockStore(stateWithSavedSearchId); + + render( + + + + ); + + await waitFor(() => { + expect(screen.queryByTestId(esqlTabSubj)).toBeVisible(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx index 2795624acb683c..2e164677735dd4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/index.tsx @@ -43,6 +43,7 @@ import * as i18n from './translations'; import { useLicense } from '../../../../common/hooks/use_license'; import { TIMELINE_CONVERSATION_TITLE } from '../../../../assistant/content/conversations/translations'; import { initializeTimelineSettings } from '../../../store/actions'; +import { selectTimelineESQLSavedSearchId } from '../../../store/selectors'; const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>( ({ $isVisible = false, isOverflowYScroll = false }) => ({ @@ -110,6 +111,10 @@ const ActiveTimelineTab = memo( }) => { const { hasAssistantPrivilege } = useAssistantAvailability(); const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); + const timelineESQLSavedSearch = useShallowEqualSelector((state) => + selectTimelineESQLSavedSearchId(state, timelineId) + ); + const shouldShowESQLTab = isESQLTabInTimelineEnabled || timelineESQLSavedSearch != null; const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); const getTab = useCallback( (tab: TimelineTabs) => { @@ -177,7 +182,7 @@ const ActiveTimelineTab = memo( timelineId={timelineId} /> - {showTimeline && isESQLTabInTimelineEnabled && activeTimelineTab === TimelineTabs.esql && ( + {showTimeline && shouldShowESQLTab && activeTimelineTab === TimelineTabs.esql && ( = ({ timelineDescription, }) => { const aiAssistantFlyoutMode = useIsExperimentalFeatureEnabled('aiAssistantFlyoutMode'); - const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); const { hasAssistantPrivilege } = useAssistantAvailability(); const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); @@ -267,9 +271,14 @@ const TabsContentComponent: React.FC = ({ const getAppNotes = useMemo(() => getNotesSelector(), []); const getTimelineNoteIds = useMemo(() => getNoteIdsSelector(), []); const getTimelinePinnedEventNotes = useMemo(() => getEventIdToNoteIdsSelector(), []); + const { isESQLTabInTimelineEnabled } = useEsqlAvailability(); + const timelineESQLSavedSearch = useShallowEqualSelector((state) => + selectTimelineESQLSavedSearchId(state, timelineId) + ); const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); + const shouldShowESQLTab = isESQLTabInTimelineEnabled || timelineESQLSavedSearch != null; const numberOfPinnedEvents = useShallowEqualSelector((state) => getNumberOfPinnedEvents(state, timelineId) @@ -372,7 +381,7 @@ const TabsContentComponent: React.FC = ({ {i18n.QUERY_TAB} {showTimeline && } - {isESQLTabInTimelineEnabled && ( + {shouldShowESQLTab && ( time */ const selectTimelineKqlQuery = createSelector(selectTimelineById, (timeline) => timeline?.kqlQuery); +/** + * Selector that returns the timeline esql saved search id. + */ +export const selectTimelineESQLSavedSearchId = createSelector( + selectTimelineById, + (timeline) => timeline?.savedSearchId +); + /** * Selector that returns the kqlQuery.filterQuery.kuery.expression of a timeline. */