[SIEM] fix timelineType for selectable timeline (#66549)

* fix timelineType for selectable timeline

* fix cypress test

* fix cypress test

* disable template timeline's tab

* rename flag

* update filter to return only default template

* update wording

* update placeholder according to timelinetype

* fix i18n

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Angela Chuang 2020-05-21 22:11:21 +01:00 committed by GitHub
parent 93c3a6319e
commit 47eaa0689a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 182 additions and 38 deletions

View file

@ -195,7 +195,7 @@ describe('Signal detection rules, custom', () => {
cy.get(DEFINITION_STEP)
.eq(DEFINITION_TIMELINE)
.invoke('text')
.should('eql', 'Default blank timeline');
.should('eql', 'None');
cy.get(SCHEDULE_STEP)
.eq(SCHEDULE_RUNS)

View file

@ -183,7 +183,7 @@ describe('Signal detection rules, machine learning', () => {
cy.get(DEFINITION_STEP)
.eq(DEFINITION_TIMELINE)
.invoke('text')
.should('eql', 'Default blank timeline');
.should('eql', 'None');
cy.get(SCHEDULE_STEP)
.eq(SCHEDULE_RUNS)

View file

@ -168,8 +168,7 @@ export const schema: FormSchema = {
helpText: i18n.translate(
'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText',
{
defaultMessage:
'Select an existing timeline to use as a template when investigating generated signals.',
defaultMessage: 'Select which timeline to use when investigating generated signals.',
}
),
},

View file

@ -475,7 +475,10 @@ describe('StatefulOpenTimeline', () => {
).toEqual('elastic');
});
test('it renders the tabs', async () => {
/**
* enable this test when createtTemplateTimeline is ready
*/
test.skip('it renders the tabs', async () => {
const wrapper = mount(
<TestProviders>
<MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}>

View file

@ -52,6 +52,12 @@ interface OwnProps<TCache = object> {
onOpenTimeline?: (timeline: TimelineModel) => void;
}
/**
* CreateTemplateTimelineBtn
* Remove the comment here to enable template timeline
*/
export const disableTemplate = true;
export type OpenTimelineOwnProps = OwnProps &
Pick<
OpenTimelineProps,
@ -275,7 +281,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
tabs={timelineTabs}
tabs={!disableTemplate ? timelineTabs : undefined}
title={title}
totalSearchResultsCount={totalCount}
/>
@ -302,7 +308,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
selectedItems={selectedItems}
sortDirection={sortDirection}
sortField={sortField}
tabs={timelineFilters}
tabs={!disableTemplate ? timelineFilters : undefined}
title={title}
totalSearchResultsCount={totalCount}
/>

View file

@ -151,7 +151,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
/>
<EuiPanel className={OPEN_TIMELINE_CLASS_NAME}>
{tabs}
{!!tabs && tabs}
<SearchRow
data-test-subj="search-row"
onlyFavorites={onlyFavorites}

View file

@ -161,7 +161,7 @@ export interface OpenTimelineProps {
/** the requested field to sort on */
sortField: string;
/** timeline / template timeline */
tabs: JSX.Element;
tabs?: JSX.Element;
/** The title of the Open Timeline component */
title: string;
/** The total (server-side) count of the search results */

View file

@ -13,6 +13,7 @@ import { OpenTimelineResult } from '../../open_timeline/types';
import { SelectableTimeline } from '../selectable_timeline';
import * as i18n from '../translations';
import { timelineActions } from '../../../../timelines/store/timeline';
import { TimelineType } from '../../../../../common/types/timeline';
interface InsertTimelinePopoverProps {
isDisabled: boolean;
@ -107,6 +108,7 @@ export const InsertTimelinePopoverComponent: React.FC<Props> = ({
getSelectableOptions={handleGetSelectableOptions}
onClosePopover={handleClosePopover}
onTimelineChange={onTimelineChange}
timelineType={TimelineType.default}
/>
</EuiPopover>
);

View file

@ -11,6 +11,7 @@ import { createGlobalStyle } from 'styled-components';
import { OpenTimelineResult } from '../../open_timeline/types';
import { SelectableTimeline } from '../selectable_timeline';
import * as i18n from '../translations';
import { TimelineType, TimelineTypeLiteral } from '../../../../../common/types/timeline';
const SearchTimelineSuperSelectGlobalStyle = createGlobalStyle`
.euiPopover__panel.euiPopover__panel-isOpen.timeline-search-super-select-popover__popoverPanel {
@ -24,6 +25,7 @@ interface SearchTimelineSuperSelectProps {
hideUntitled?: boolean;
timelineId: string | null;
timelineTitle: string | null;
timelineType?: TimelineTypeLiteral;
onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
}
@ -50,6 +52,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
hideUntitled = false,
timelineId,
timelineTitle,
timelineType = TimelineType.default,
onTimelineChange,
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
@ -121,6 +124,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
getSelectableOptions={handleGetSelectableOptions}
onClosePopover={handleClosePopover}
onTimelineChange={onTimelineChange}
timelineType={timelineType}
/>
<SearchTimelineSuperSelectGlobalStyle />
</EuiInputPopover>

View file

@ -0,0 +1,112 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow, ShallowWrapper, mount } from 'enzyme';
import { TimelineType } from '../../../../../common/types/timeline';
import { SortFieldTimeline, Direction } from '../../../../graphql/types';
import { SearchProps } from './';
describe('SelectableTimeline', () => {
const mockFetchAllTimeline = jest.fn();
const mockEuiSelectable = jest.fn();
jest.doMock('@elastic/eui', () => {
const originalModule = jest.requireActual('@elastic/eui');
return {
...originalModule,
EuiSelectable: mockEuiSelectable.mockImplementation(({ children }) => <div>{children}</div>),
};
});
jest.doMock('../../../containers/all', () => {
return {
useGetAllTimeline: jest.fn(() => ({
fetchAllTimeline: mockFetchAllTimeline,
timelines: [],
})),
};
});
const {
SelectableTimeline,
ORIGINAL_PAGE_SIZE,
} = jest.requireActual('./');
const props = {
hideUntitled: false,
getSelectableOptions: jest.fn(),
onClosePopover: jest.fn(),
onTimelineChange: jest.fn(),
timelineType: TimelineType.default,
};
describe('should render', () => {
let wrapper: ShallowWrapper;
describe('timeline', () => {
beforeAll(() => {
wrapper = shallow(<SelectableTimeline {...props} />);
});
afterAll(() => {
jest.clearAllMocks();
});
test('render placeholder', () => {
const searchProps: SearchProps = wrapper
.find('[data-test-subj="selectable-input"]')
.prop('searchProps');
expect(searchProps.placeholder).toEqual('e.g. Timeline name or description');
});
});
describe('template timeline', () => {
const templateTimelineProps = { ...props, timelineType: TimelineType.template };
beforeAll(() => {
wrapper = shallow(<SelectableTimeline {...templateTimelineProps} />);
});
afterAll(() => {
jest.clearAllMocks();
});
test('render placeholder', () => {
const searchProps: SearchProps = wrapper
.find('[data-test-subj="selectable-input"]')
.prop('searchProps');
expect(searchProps.placeholder).toEqual('e.g. Template timeline name or description');
});
});
});
describe('fetchAllTimeline', () => {
const args = {
pageInfo: {
pageIndex: 1,
pageSize: ORIGINAL_PAGE_SIZE,
},
search: '',
sort: {
sortField: SortFieldTimeline.updated,
sortOrder: Direction.desc,
},
onlyUserFavorite: false,
timelineType: TimelineType.default,
};
beforeAll(() => {
mount(<SelectableTimeline {...props} />);
});
afterAll(() => {
jest.clearAllMocks();
});
test('shoule be called with correct args', () => {
expect(mockFetchAllTimeline).toBeCalledWith(args);
});
});
});

View file

@ -21,7 +21,10 @@ import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
import { ListProps } from 'react-virtualized';
import styled from 'styled-components';
import { TimelineType, TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline';
import {
TimelineTypeLiteralWithNull,
TimelineTypeLiteral,
} from '../../../../../common/types/timeline';
import { useGetAllTimeline } from '../../../containers/all';
import { SortFieldTimeline, Direction } from '../../../../graphql/types';
@ -29,7 +32,6 @@ import { isUntitled } from '../../open_timeline/helpers';
import * as i18nTimeline from '../../open_timeline/translations';
import { OpenTimelineResult } from '../../open_timeline/types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import * as i18n from '../translations';
const MyEuiFlexItem = styled(EuiFlexItem)`
@ -66,7 +68,7 @@ const EuiSelectableContainer = styled.div<{ isLoading: boolean }>`
}
`;
const ORIGINAL_PAGE_SIZE = 50;
export const ORIGINAL_PAGE_SIZE = 50;
const POPOVER_HEIGHT = 260;
const TIMELINE_ITEM_HEIGHT = 50;
@ -77,7 +79,7 @@ export interface GetSelectableOptions {
searchTimelineValue: string;
}
interface SelectableTimelineProps {
export interface SelectableTimelineProps {
hideUntitled?: boolean;
getSelectableOptions: ({
timelines,
@ -87,6 +89,16 @@ interface SelectableTimelineProps {
}: GetSelectableOptions) => EuiSelectableOption[];
onClosePopover: () => void;
onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
timelineType: TimelineTypeLiteral;
}
export interface SearchProps {
'data-test-subj'?: string;
isLoading: boolean;
placeholder: string;
onSearch: (arg: string) => void;
incremental: boolean;
inputRef: (arg: HTMLElement) => void;
}
const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
@ -94,10 +106,11 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
getSelectableOptions,
onClosePopover,
onTimelineChange,
timelineType,
}) => {
const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE);
const [heightTrigger, setHeightTrigger] = useState(0);
const [searchTimelineValue, setSearchTimelineValue] = useState('');
const [searchTimelineValue, setSearchTimelineValue] = useState<string>('');
const [onlyFavorites, setOnlyFavorites] = useState(false);
const [searchRef, setSearchRef] = useState<HTMLElement | null>(null);
const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline();
@ -220,6 +233,17 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
[searchRef, onlyFavorites, handleOnToggleOnlyFavorites]
);
const searchProps: SearchProps = {
'data-test-subj': 'timeline-super-select-search-box',
isLoading: loading,
placeholder: useMemo(() => i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER(timelineType), [timelineType]),
onSearch: onSearchTimeline,
incremental: false,
inputRef: (ref: HTMLElement) => {
setSearchRef(ref);
},
};
useEffect(() => {
fetchAllTimeline({
pageInfo: {
@ -232,13 +256,14 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
sortOrder: Direction.desc,
},
onlyUserFavorite: onlyFavorites,
timelineType: TimelineType.default,
timelineType,
});
}, [onlyFavorites, pageSize, searchTimelineValue]);
}, [onlyFavorites, pageSize, searchTimelineValue, timelineType]);
return (
<EuiSelectableContainer isLoading={loading}>
<EuiSelectable
data-test-subj="selectable-input"
height={POPOVER_HEIGHT}
isLoading={loading && timelines.length === 0}
listProps={{
@ -255,22 +280,13 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
renderOption={renderTimelineOption}
onChange={handleTimelineChange}
searchable
searchProps={{
'data-test-subj': 'timeline-super-select-search-box',
isLoading: loading,
placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER,
onSearch: onSearchTimeline,
incremental: false,
inputRef: (ref: HTMLElement) => {
setSearchRef(ref);
},
}}
searchProps={searchProps}
singleSelection={true}
options={getSelectableOptions({
timelines,
onlyFavorites,
searchTimelineValue,
timelineType: TimelineType.default,
timelineType,
})}
>
{(list, search) => (

View file

@ -5,9 +5,10 @@
*/
import { i18n } from '@kbn/i18n';
import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline';
export const DEFAULT_TIMELINE_TITLE = i18n.translate('xpack.siem.timeline.defaultTimelineTitle', {
defaultMessage: 'Default blank timeline',
defaultMessage: 'None',
});
export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate(
@ -17,12 +18,11 @@ export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate(
}
);
export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate(
'xpack.siem.timeline.searchBoxPlaceholder',
{
defaultMessage: 'e.g. timeline name or description',
}
);
export const SEARCH_BOX_TIMELINE_PLACEHOLDER = (timelineType: TimelineTypeLiteral) =>
i18n.translate('xpack.siem.timeline.searchBoxPlaceholder', {
values: { timeline: timelineType === TimelineType.template ? 'Template timeline' : 'Timeline' },
defaultMessage: 'e.g. {timeline} name or description',
});
export const INSERT_TIMELINE = i18n.translate('xpack.siem.insert.timeline.insertTimelineButton', {
defaultMessage: 'Insert timeline link',

View file

@ -142,7 +142,12 @@ export const getAllTimeline = async (
searchFields: onlyUserFavorite
? ['title', 'description', 'favorite.keySearch']
: ['title', 'description'],
filter: getTimelineTypeFilter(timelineType, false),
/**
* CreateTemplateTimelineBtn
* Remove the comment here to enable template timeline and apply the change below
* filter: getTimelineTypeFilter(timelineType, false)
*/
filter: getTimelineTypeFilter(TimelineType.default, false),
sortField: sort != null ? sort.sortField : undefined,
sortOrder: sort != null ? sort.sortOrder : undefined,
};

View file

@ -14255,7 +14255,6 @@
"xpack.siem.timeline.callOut.unauthorized.message.description": "SIEM アプリケーションでタイムラインを自動保存するにはパーミッションが必要ですが、引き続きタイムラインを使用してセキュリティイベントの検索とフィルタリングを行うことはできます。",
"xpack.siem.timeline.categoryTooltip": "カテゴリー",
"xpack.siem.timeline.defaultTimelineDescription": "新しいタイムラインを作成するときにデフォルトで提供されるタイムライン。",
"xpack.siem.timeline.defaultTimelineTitle": "デフォルトの空白タイムライン",
"xpack.siem.timeline.descriptionTooltip": "説明",
"xpack.siem.timeline.destination": "送信先",
"xpack.siem.timeline.eventsSelect.actions.pinSelected": "選択項目にピン付け",
@ -14297,7 +14296,6 @@
"xpack.siem.timeline.rangePicker.oneMonth": "1 か月",
"xpack.siem.timeline.rangePicker.oneWeek": "1 週間",
"xpack.siem.timeline.rangePicker.oneYear": "1 年",
"xpack.siem.timeline.searchBoxPlaceholder": "例:タイムライン名、または説明",
"xpack.siem.timeline.searchOrFilter.eventTypeAllEvent": "すべてのイベント",
"xpack.siem.timeline.searchOrFilter.eventTypeRawEvent": "未加工イベント",
"xpack.siem.timeline.searchOrFilter.eventTypeSignalEvent": "シグナルイベント",

View file

@ -14304,7 +14304,6 @@
"xpack.siem.timeline.rangePicker.oneMonth": "1 个月",
"xpack.siem.timeline.rangePicker.oneWeek": "1 周",
"xpack.siem.timeline.rangePicker.oneYear": "1 年",
"xpack.siem.timeline.searchBoxPlaceholder": "例如时间线名称或描述",
"xpack.siem.timeline.searchOrFilter.eventTypeAllEvent": "所有事件",
"xpack.siem.timeline.searchOrFilter.eventTypeRawEvent": "原始事件",
"xpack.siem.timeline.searchOrFilter.eventTypeSignalEvent": "信号事件",