Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [SIEM] fix timelineType for selectable timeline (#66549) #67216

Merged
merged 4 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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