diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index 13b9c9ef4f519..1616c5e84247f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiModalBody, EuiModalHeader } from '@elastic/eui'; +import { EuiModalBody, EuiModalHeader, EuiSpacer } from '@elastic/eui'; import React, { Fragment, memo, useMemo } from 'react'; import styled from 'styled-components'; @@ -62,11 +62,10 @@ export const OpenTimelineModalBody = memo( const SearchRowContent = useMemo( () => ( - {!!timelineFilter && timelineFilter} {!!templateTimelineFilter && templateTimelineFilter} ), - [timelineFilter, templateTimelineFilter] + [templateTimelineFilter] ); return ( @@ -84,9 +83,14 @@ export const OpenTimelineModalBody = memo( <> + {!!timelineFilter && ( + <> + {timelineFilter} + + + )} & { */ export const TitleRow = React.memo( ({ children, onAddTimelinesToFavorites, selectedTimelinesCount, title }) => ( - + {onAddTimelinesToFavorites && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 84907c74cdace..ae743ad30eef1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -146,7 +146,7 @@ export const OPEN_TIMELINE = i18n.translate( export const OPEN_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.open.timeline.openTimelineTitle', { - defaultMessage: 'Open Timeline', + defaultMessage: 'Open', } ); @@ -274,12 +274,6 @@ export const SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES = (totalTimelineTemplates: } ); -export const FILTER_TIMELINES = (timelineType: string) => - i18n.translate('xpack.securitySolution.open.timeline.filterByTimelineTypesTitle', { - values: { timelineType }, - defaultMessage: 'Only {timelineType}', - }); - export const TAB_TIMELINES = i18n.translate( 'xpack.securitySolution.timelines.components.tabs.timelinesTitle', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 999ef5d7aa01a..f5ba9959ae898 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -221,13 +221,11 @@ export enum TimelineTabsStyle { } export interface TimelineTab { - count: number | undefined; disabled: boolean; href: string; id: TimelineTypeLiteral; name: string; onClick: (ev: { preventDefault: () => void }) => void; - withNext: boolean; } export interface TemplateTimelineFilter { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx new file mode 100644 index 0000000000000..1d39dd169ffaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx @@ -0,0 +1,193 @@ +/* + * 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 { fireEvent, render } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { + useTimelineTypes, + UseTimelineTypesArgs, + UseTimelineTypesResult, +} from './use_timeline_types'; + +jest.mock('react-router-dom', () => { + return { + useParams: jest.fn().mockReturnValue('default'), + useHistory: jest.fn().mockReturnValue([]), + }; +}); + +jest.mock('../../../common/components/link_to', () => { + return { + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ + formatUrl: jest.fn(), + search: '', + }), + }; +}); + +describe('useTimelineTypes', () => { + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + describe('timelineTabs', () => { + it('render timelineTabs', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + expect( + container.querySelector('[data-test-subj="timeline-tab-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="timeline-tab-template"]') + ).toHaveTextContent('Templates'); + }); + }); + + it('set timelineTypes correctly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'template', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + it('stays in the same tab if clicking again on current tab', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + }); + + describe('timelineFilters', () => { + it('render timelineFilters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]') + ).toHaveTextContent('Templates'); + }); + }); + + it('set timelineTypes correctly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'template', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + it('stays in the same tab if clicking again on current tab', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 728d8b6eeb488..a66fe43d305f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,7 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; @@ -24,7 +24,7 @@ export interface UseTimelineTypesArgs { export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; - timelineFilters: JSX.Element[]; + timelineFilters: JSX.Element; } export const useTimelineTypes = ({ @@ -59,51 +59,28 @@ export const useTimelineTypes = ({ (timelineTabsStyle: TimelineTabsStyle) => [ { id: TimelineType.default, - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES) - : i18n.TAB_TIMELINES, + name: i18n.TAB_TIMELINES, href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)), disabled: false, - withNext: true, - count: - timelineTabsStyle === TimelineTabsStyle.filter - ? defaultTimelineCount ?? undefined - : undefined, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES) - : i18n.TAB_TEMPLATES, + name: i18n.TAB_TEMPLATES, href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)), disabled: false, - withNext: false, - count: - timelineTabsStyle === TimelineTabsStyle.filter - ? templateTimelineCount ?? undefined - : undefined, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], - [ - defaultTimelineCount, - templateTimelineCount, - urlSearch, - formatUrl, - goToTimeline, - goToTemplateTimeline, - ] + [urlSearch, formatUrl, goToTimeline, goToTemplateTimeline] ); const onFilterClicked = useCallback( (tabId, tabStyle: TimelineTabsStyle) => { setTimelineTypes((prevTimelineTypes) => { - if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { - return tabId === TimelineType.default ? TimelineType.template : TimelineType.default; - } else if (prevTimelineTypes !== tabId) { + if (prevTimelineTypes !== tabId) { setTimelineTypes(tabId); } return prevTimelineTypes; @@ -139,21 +116,23 @@ export const useTimelineTypes = ({ }, [tabName]); const timelineFilters = useMemo(() => { - return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( - void }) => { - tab.onClick(ev); - onFilterClicked(tab.id, TimelineTabsStyle.filter); - }} - withNext={tab.withNext} - > - {tab.name} - - )); + return ( + + {getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( + void }) => { + tab.onClick(ev); + onFilterClicked(tab.id, TimelineTabsStyle.filter); + }} + > + {tab.name} + + ))} + + ); }, [timelineType, getFilterOrTabs, onFilterClicked]); return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a6b0a2426c682..562309b1cfbc8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19258,7 +19258,6 @@ "xpack.securitySolution.open.timeline.exportSelectedButton": "選択した項目のエクスポート", "xpack.securitySolution.open.timeline.favoriteSelectedButton": "選択中のお気に入り", "xpack.securitySolution.open.timeline.favoritesTooltip": "お気に入り", - "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "{timelineType}のみ", "xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最終更新:", "xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "savedObjectId がありません", "xpack.securitySolution.open.timeline.modifiedByTableHeader": "変更者:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3272b063531ef..11156bc48af0e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19305,7 +19305,6 @@ "xpack.securitySolution.open.timeline.exportSelectedButton": "导出所选", "xpack.securitySolution.open.timeline.favoriteSelectedButton": "收藏所选", "xpack.securitySolution.open.timeline.favoritesTooltip": "收藏夹", - "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "仅 {timelineType}", "xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最后修改时间", "xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "缺失 savedObjectId", "xpack.securitySolution.open.timeline.modifiedByTableHeader": "修改者",