Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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>
  • Loading branch information
angorayc and elasticmachine committed May 21, 2020
1 parent 93c3a63 commit 47eaa06
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 38 deletions.
Expand Up @@ -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)
Expand Down
Expand Up @@ -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)
Expand Down
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
Expand Up @@ -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}>
Expand Down
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
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
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
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
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
@@ -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);
});
});
});
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

0 comments on commit 47eaa06

Please sign in to comment.