[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:
parent
93c3a6319e
commit
47eaa0689a
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
),
|
||||
},
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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) => (
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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": "シグナルイベント",
|
||||
|
|
|
@ -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": "信号事件",
|
||||
|
|
Loading…
Reference in a new issue