Skip to content

Commit

Permalink
[7.x] [SIEM] fix timelineType for selectable timeline (elastic#66549) (
Browse files Browse the repository at this point in the history
…elastic#67216)

* [SIEM] fix timelineType for selectable timeline (elastic#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>

* fix lint

* lint

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
  • Loading branch information
3 people committed May 26, 2020
1 parent b8ba513 commit 49f7bc0
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,7 @@ describe('Signal detection rules, custom', () => {
.eq(DEFINITION_CUSTOM_QUERY)
.invoke('text')
.should('eql', `${newRule.customQuery} `);
cy.get(DEFINITION_STEP)
.eq(DEFINITION_TIMELINE)
.invoke('text')
.should('eql', 'Default blank timeline');
cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None');

cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m');
cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,7 @@ describe('Signal detection rules, machine learning', () => {
.invoke('text')
.should('eql', machineLearningRule.machineLearningJob);

cy.get(DEFINITION_STEP)
.eq(DEFINITION_TIMELINE)
.invoke('text')
.should('eql', 'Default blank timeline');
cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None');

cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m');
cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
}
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}
/>
Expand All @@ -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}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +108,7 @@ export const InsertTimelinePopoverComponent: React.FC<Props> = ({
getSelectableOptions={handleGetSelectableOptions}
onClosePopover={handleClosePopover}
onTimelineChange={onTimelineChange}
timelineType={TimelineType.default}
/>
</EuiPopover>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,6 +25,7 @@ interface SearchTimelineSuperSelectProps {
hideUntitled?: boolean;
timelineId: string | null;
timelineTitle: string | null;
timelineType?: TimelineTypeLiteral;
onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
}

Expand All @@ -50,6 +52,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
hideUntitled = false,
timelineId,
timelineTitle,
timelineType = TimelineType.default,
onTimelineChange,
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
Expand Down Expand Up @@ -121,6 +124,7 @@ const SearchTimelineSuperSelectComponent: React.FC<SearchTimelineSuperSelectProp
getSelectableOptions={handleGetSelectableOptions}
onClosePopover={handleClosePopover}
onTimelineChange={onTimelineChange}
timelineType={timelineType}
/>
<SearchTimelineSuperSelectGlobalStyle />
</EuiInputPopover>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ 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';
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)`
Expand Down Expand Up @@ -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;

Expand All @@ -77,7 +79,7 @@ export interface GetSelectableOptions {
searchTimelineValue: string;
}

interface SelectableTimelineProps {
export interface SelectableTimelineProps {
hideUntitled?: boolean;
getSelectableOptions: ({
timelines,
Expand All @@ -87,17 +89,28 @@ 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> = ({
hideUntitled = false,
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();
Expand Down Expand Up @@ -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: {
Expand All @@ -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={{
Expand All @@ -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) => (
Expand Down
Loading

0 comments on commit 49f7bc0

Please sign in to comment.