Skip to content

Commit

Permalink
[Security solution][Endpoint] New Event Filters sub-section under Adm…
Browse files Browse the repository at this point in the history
…inistration area (elastic#97903)

* Add Event Filters section to the Admin area (behind feature flag)
* new `PaginatedContent` generic component
* Refactor Trusted Apps grid view to use PaginatedContent
* Refactor usages of `getTestId()` to use new hook
  • Loading branch information
paul-tavares authored and madirey committed May 11, 2021
1 parent 3890818 commit 5de7357
Show file tree
Hide file tree
Showing 27 changed files with 1,752 additions and 751 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,17 @@ ItemDetailsAction.displayName = 'ItemDetailsAction';

export type ItemDetailsCardProps = PropsWithChildren<{
'data-test-subj'?: string;
className?: string;
}>;
export const ItemDetailsCard = memo<ItemDetailsCardProps>(
({ children, 'data-test-subj': dataTestSubj }) => {
({ children, 'data-test-subj': dataTestSubj, className }) => {
const childElements = useMemo(
() => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]),
[children]
);

return (
<EuiPanel paddingSize="none" data-test-subj={dataTestSubj}>
<EuiPanel paddingSize="none" data-test-subj={dataTestSubj} className={className}>
<EuiFlexGroup direction="row">
<SummarySection grow={2}>
<EuiDescriptionList compressed type="column">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export const mockGlobalState: State = {
{ id: 'error-id-1', title: 'title-1', message: ['error-message-1'] },
{ id: 'error-id-2', title: 'title-2', message: ['error-message-2'] },
],
enableExperimental: {
eventFilteringEnabled: false,
trustedAppsByPolicyEnabled: false,
},
},
hosts: {
page: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH
export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.policies})`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`;
export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`;
export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`;

// --[ STORE ]---------------------------------------------------------------------------
/** The SIEM global store namespace where the management state will be mounted */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
MANAGEMENT_DEFAULT_PAGE_SIZE,
MANAGEMENT_PAGE_SIZE_OPTIONS,
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
Expand All @@ -23,6 +24,7 @@ import { AdministrationSubTab } from '../types';
import { appendSearch } from '../../common/components/link_to/helpers';
import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types';
import { TrustedAppsListPageLocation } from '../pages/trusted_apps/state';
import { EventFiltersListPageUrlSearchParams } from '../pages/event_filters/types';

// Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150
type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never ? T1 : never;
Expand Down Expand Up @@ -178,3 +180,13 @@ export const getTrustedAppsListPath = (location?: Partial<TrustedAppsListPageLoc
querystring.stringify(normalizeTrustedAppsPageLocation(location))
)}`;
};

export const getEventFiltersListPath = (
location?: Partial<EventFiltersListPageUrlSearchParams>
): string => {
const path = generatePath(MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, {
tabName: AdministrationSubTab.eventFilters,
});

return `${path}${appendSearch(querystring.stringify(location))}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const TRUSTED_APPS_TAB = i18n.translate('xpack.securitySolution.trustedAp
defaultMessage: 'Trusted applications',
});

export const EVENT_FILTERS_TAB = i18n.translate('xpack.securitySolution.eventFiltersTab', {
defaultMessage: 'Event filters',
});

export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administration.list.beta', {
defaultMessage: 'Beta',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ import { HeaderPage } from '../../common/components/header_page';
import { SiemNavigation } from '../../common/components/navigation';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { AdministrationSubTab } from '../types';
import { ENDPOINTS_TAB, TRUSTED_APPS_TAB, BETA_BADGE_LABEL } from '../common/translations';
import { getEndpointListPath, getTrustedAppsListPath } from '../common/routing';
import {
ENDPOINTS_TAB,
TRUSTED_APPS_TAB,
BETA_BADGE_LABEL,
EVENT_FILTERS_TAB,
} from '../common/translations';
import {
getEndpointListPath,
getEventFiltersListPath,
getTrustedAppsListPath,
} from '../common/routing';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';

/** Ensure that all flyouts z-index in Administation area show the flyout header */
const EuiPanelStyled = styled(EuiPanel)`
Expand All @@ -34,6 +44,7 @@ interface AdministrationListPageProps {

export const AdministrationListPage: FC<AdministrationListPageProps & CommonProps> = memo(
({ beta, title, subtitle, actions, children, headerBackComponent, ...otherProps }) => {
const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled');
const badgeOptions = !beta ? undefined : { beta: true, text: BETA_BADGE_LABEL };

return (
Expand Down Expand Up @@ -66,6 +77,18 @@ export const AdministrationListPage: FC<AdministrationListPageProps & CommonProp
pageId: SecurityPageName.administration,
disabled: false,
},
...(isEventFilteringEnabled
? {
[AdministrationSubTab.eventFilters]: {
name: EVENT_FILTERS_TAB,
id: AdministrationSubTab.eventFilters,
href: getEventFiltersListPath(),
urlKey: 'administration',
pageId: SecurityPageName.administration,
disabled: false,
},
}
: {}),
}}
/>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useCallback } from 'react';

/**
* Returns a callback that can be used to generate new test ids (values for `data-test-subj`) that
* are prefix with a standard string. Will only generate test ids if a prefix is defiened.
* Use it in complex component where you might want to expose a `data-test-subj` prop and use that
* as a prefix to several other test ids inside of the complex component.
*
* @example
* // `props['data-test-subj'] = 'abc';
* const getTestId = useTestIdGenerator(props['data-test-subj']);
* getTestId('body'); // abc-body
* getTestId('some-other-ui-section'); // abc-some-other-ui-section
*
* @example
* // `props['data-test-subj'] = undefined;
* const getTestId = useTestIdGenerator(props['data-test-subj']);
* getTestId('body'); // undefined
*/
export const useTestIdGenerator = (prefix?: string): ((suffix: string) => string | undefined) => {
return useCallback(
(suffix: string): string | undefined => {
if (prefix) {
return `${prefix}-${suffix}`;
}
},
[prefix]
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './paginated_content';
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FC } from 'react';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { PaginatedContentProps, PaginatedContent } from './paginated_content';
import { act, fireEvent } from '@testing-library/react';

describe('when using PaginatedContent', () => {
interface Foo {
id: string;
}

interface ItemComponentProps {
item: Foo;
}

type ItemComponentType = FC<ItemComponentProps>;

type PropsForPaginatedContent = PaginatedContentProps<Foo, FC<ItemComponentProps>>;

const ItemComponent: ItemComponentType = jest.fn((props) => (
<div className="foo-item">{'hi'}</div>
));

const getPropsToRenderItem: PropsForPaginatedContent['itemComponentProps'] = jest.fn(
(item: Foo) => {
return { item };
}
);

let render: (
additionalProps?: Partial<PropsForPaginatedContent>
) => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let onChangeHandler: PropsForPaginatedContent['onChange'];

beforeEach(() => {
const mockedContext = createAppRootMockRenderer();

onChangeHandler = jest.fn();

render = (additionalProps) => {
const props: PropsForPaginatedContent = {
items: Array.from({ length: 10 }, (v, i) => ({ id: String(i) })),
ItemComponent,
onChange: onChangeHandler,
itemComponentProps: getPropsToRenderItem,
pagination: {
pageIndex: 0,
pageSizeOptions: [5, 10, 20],
pageSize: 5,
totalItemCount: 10,
},
'data-test-subj': 'test',
...(additionalProps ?? {}),
};
renderResult = mockedContext.render(<PaginatedContent<Foo, ItemComponentType> {...props} />);
return renderResult;
};
});

it('should render items using provided component', () => {
render({ itemId: 'id' }); // Using `itemsId` prop just to ensure that branch of code is executed

expect(renderResult.baseElement.querySelectorAll('.foo-item').length).toBe(10);
expect(getPropsToRenderItem).toHaveBeenNthCalledWith(1, { id: '0' });
expect(ItemComponent).toHaveBeenNthCalledWith(1, { item: { id: '0' } }, {});
expect(renderResult.getByTestId('test-footer')).not.toBeNull();
});

it('should show default "no items found message" when no data to display', () => {
render({ items: [] });

expect(renderResult.getByText('No items found')).not.toBeNull();
});

it('should allow for a custom no items found message to be displayed', () => {
render({ items: [], noItemsMessage: 'no Foo found!' });

expect(renderResult.getByText('no Foo found!')).not.toBeNull();
});

it('should show error if one is defined (even if `items` is not empty)', () => {
render({ error: 'something is wrong with foo' });

expect(renderResult.getByText('something is wrong with foo')).not.toBeNull();
expect(renderResult.baseElement.querySelectorAll('.foo-item').length).toBe(0);
});

it('should show a progress bar if `loading` is set to true', () => {
render({ loading: true });

expect(renderResult.baseElement.querySelector('.euiProgress')).not.toBeNull();
});

it('should NOT show a pagination footer if no props are defined for `pagination`', () => {
render({ pagination: undefined });

expect(renderResult.queryByTestId('test-footer')).toBeNull();
});

it('should apply `contentClassName` if one is defined', () => {
render({ contentClassName: 'foo-content' });

expect(renderResult.baseElement.querySelector('.foo-content')).not.toBeNull();
});

it('should call onChange when pagination is changed', () => {
render();

act(() => {
fireEvent.click(renderResult.getByTestId('pagination-button-next'));
});

expect(onChangeHandler).toHaveBeenCalledWith({
pageIndex: 1,
pageSize: 5,
});
});

it('should call onChange when page size is changed', () => {
render();

act(() => {
fireEvent.click(renderResult.getByTestId('tablePaginationPopoverButton'));
});

act(() => {
fireEvent.click(renderResult.getByTestId('tablePagination-10-rows'));
});

expect(onChangeHandler).toHaveBeenCalledWith({
pageIndex: 0,
pageSize: 10,
});
});

it('should ignore items, error, noItemsMessage when `children` is used', () => {
render({ children: <div data-test-subj="custom-content">{'children being used here'}</div> });
expect(renderResult.getByTestId('custom-content')).not.toBeNull();
expect(renderResult.baseElement.querySelectorAll('.foo-item').length).toBe(0);
});
});
Loading

0 comments on commit 5de7357

Please sign in to comment.