Skip to content
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 @@ -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