[Security solution][Endpoint] New Event Filters sub-section under Administration area (#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
This commit is contained in:
Paul Tavares 2021-04-23 11:56:18 -04:00 committed by GitHub
parent c9ce295a0b
commit 485692dbf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1752 additions and 751 deletions

View file

@ -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">

View file

@ -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: {

View file

@ -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 */

View file

@ -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,
@ -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;
@ -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))}`;
};

View file

@ -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',
});

View file

@ -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)`
@ -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 (
@ -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,
},
}
: {}),
}}
/>

View file

@ -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]
);
};

View file

@ -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';

View file

@ -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);
});
});

View file

@ -0,0 +1,230 @@
/*
* 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, {
ComponentProps,
ComponentType,
FunctionComponent,
Key,
memo,
ReactElement,
ReactNode,
useCallback,
useMemo,
useState,
} from 'react';
import {
CommonProps,
EuiEmptyPrompt,
EuiIcon,
EuiProgress,
EuiSpacer,
EuiTablePagination,
EuiTablePaginationProps,
EuiText,
Pagination,
} from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { v4 as generateUUI } from 'uuid';
import { useTestIdGenerator } from '../hooks/use_test_id_generator';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ComponentWithAnyProps = ComponentType<any>;
export interface PaginatedContentProps<T, C extends ComponentWithAnyProps> extends CommonProps {
items: T[];
onChange: (changes: { pageIndex: number; pageSize: number }) => void;
/**
* The React Component that will be used to render the `items`. use `itemComponentProps` below to
* define the props that will be given to this component
*/
ItemComponent: C;
/** A callback that will be used to retrieve the props for the `ItemComponent` */
itemComponentProps: (item: T) => ComponentProps<C>;
/** The item attribute that holds its unique value */
itemId?: keyof T;
loading?: boolean;
pagination?: Pagination;
noItemsMessage?: ReactNode;
/** Error to be displayed in the component's body area. Used when `items` is empty and `children` is not used */
error?: ReactNode;
/** Classname applied to the area that holds the content items */
contentClassName?: string;
/**
* Children can be used to define custom content if the default creation of items is not sufficient
* to accommodate a use case.
*
* **IMPORTANT** If defined several input props will be ignored, like `items`, `noItemsMessage`
* and `error` among others
*/
children?: ReactNode;
}
// Using `memo()` on generic typed Functional component is not supported (generic is lost),
// Work around below was created based on this discussion:
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-568218789
interface TypedGenericComponentMemo {
<T, C extends ComponentWithAnyProps>(p: PaginatedContentProps<T, C>): ReactElement<
PaginatedContentProps<T, C>,
FunctionComponent<PaginatedContentProps<T, C>>
>;
displayName: string;
}
const RootContainer = styled.div`
position: relative;
.body {
min-height: ${({ theme }) => theme.eui.gutterTypes.gutterExtraLarge};
&-content {
position: relative;
}
}
`;
const DefaultNoItemsFound = memo(() => {
return (
<EuiEmptyPrompt
title={
<FormattedMessage
id="xpack.securitySolution.endpoint.paginatedContent.noItemsFoundTitle"
defaultMessage="No items found"
/>
}
/>
);
});
DefaultNoItemsFound.displayName = 'DefaultNoItemsFound';
const ErrorMessage = memo<{ message: string }>(({ message }) => {
return (
<EuiText textAlign="center">
<EuiSpacer size="m" />
<EuiIcon type="minusInCircle" color="danger" /> {message}
<EuiSpacer size="m" />
</EuiText>
);
});
ErrorMessage.displayName = 'ErrorMessage';
/**
* A generic component to display paginated content. Provides "Items per Page" as well as pagination
* controls similar to the BasicTable of EUI. The props supported by this component (for the most part)
* support those that BasicTable accept.
*/
// eslint-disable-next-line react/display-name
export const PaginatedContent = memo(
<T extends object, C extends ComponentWithAnyProps>({
items,
ItemComponent,
itemComponentProps,
itemId,
onChange,
pagination,
loading,
noItemsMessage,
error,
contentClassName,
'data-test-subj': dataTestSubj,
'aria-label': ariaLabel,
className,
children,
}: PaginatedContentProps<T, C>) => {
const [itemKeys] = useState<WeakMap<T, string>>(new WeakMap());
const getTestId = useTestIdGenerator(dataTestSubj);
const pageCount = useMemo(
() => Math.ceil((pagination?.totalItemCount || 1) / (pagination?.pageSize || 1)),
[pagination?.pageSize, pagination?.totalItemCount]
);
const handleItemsPerPageChange: EuiTablePaginationProps['onChangeItemsPerPage'] = useCallback(
(pageSize) => {
onChange({ pageSize, pageIndex: pagination?.pageIndex || 0 });
},
[onChange, pagination?.pageIndex]
);
const handlePageChange: EuiTablePaginationProps['onChangePage'] = useCallback(
(pageIndex) => {
onChange({ pageIndex, pageSize: pagination?.pageSize || 10 });
},
[onChange, pagination?.pageSize]
);
const generatedBodyItemContent = useMemo(() => {
if (error) {
return 'string' === typeof error ? <ErrorMessage message={error} /> : error;
}
// This casting here is needed in order to avoid the following a TS error (TS2322)
// stating that the attributes given to the `ItemComponent` are not assignable to
// type 'LibraryManagedAttributes<C, any>'
// @see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34553
const Item = ItemComponent as ComponentType<ReturnType<typeof itemComponentProps>>;
if (items.length) {
return items.map((item) => {
let key: Key;
if (itemId) {
key = (item[itemId] as unknown) as Key;
} else {
if (itemKeys.has(item)) {
key = itemKeys.get(item)!;
} else {
key = generateUUI();
itemKeys.set(item, key);
}
}
return <Item {...itemComponentProps(item)} key={key} />;
});
}
return noItemsMessage || <DefaultNoItemsFound />;
}, [ItemComponent, error, itemComponentProps, itemId, itemKeys, items, noItemsMessage]);
return (
<RootContainer data-test-subj={dataTestSubj} aria-label={ariaLabel} className={className}>
{loading && <EuiProgress size="xs" color="primary" />}
<div className="body" data-test-subj={getTestId('body')}>
<EuiSpacer size="l" />
<div className={`body-content ${contentClassName}`}>
{children ? children : generatedBodyItemContent}
</div>
</div>
{pagination && (
<div data-test-subj={getTestId('footer')}>
<EuiSpacer size="l" />
<EuiTablePagination
activePage={pagination.pageIndex}
itemsPerPage={pagination.pageSize}
itemsPerPageOptions={pagination.pageSizeOptions}
pageCount={pageCount}
hidePerPageOptions={pagination.hidePerPageOptions}
onChangeItemsPerPage={handleItemsPerPageChange}
onChangePage={handlePageChange}
/>
</div>
)}
</RootContainer>
);
}
// See type description above to understand why this casting is needed
) as TypedGenericComponentMemo;
PaginatedContent.displayName = 'PaginatedContent';

View file

@ -0,0 +1,21 @@
/*
* 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 { Route, Switch } from 'react-router-dom';
import React from 'react';
import { NotFoundPage } from '../../../app/404';
import { MANAGEMENT_ROUTING_EVENT_FILTERS_PATH } from '../../common/constants';
import { EventFiltersListPage } from './view/event_filters_list_page';
export const EventFiltersContainer = () => {
return (
<Switch>
<Route path={MANAGEMENT_ROUTING_EVENT_FILTERS_PATH} exact component={EventFiltersListPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
);
};

View file

@ -0,0 +1,11 @@
/*
* 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 interface EventFiltersListPageUrlSearchParams {
page_index: number;
page_size: number;
}

View file

@ -0,0 +1,32 @@
/*
* 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, { memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AdministrationListPage } from '../../../components/administration_list_page';
export const EventFiltersListPage = memo(() => {
return (
<AdministrationListPage
beta={false}
title={
<FormattedMessage
id="xpack.securitySolution.eventFilters.list.pageTitle"
defaultMessage="Event Filters"
/>
}
subtitle={i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', {
defaultMessage: 'Something here about Event Filtering....',
})}
>
{/* <PaginatedContent />*/}
</AdministrationListPage>
);
});
EventFiltersListPage.displayName = 'EventFiltersListPage';

View file

@ -14,6 +14,7 @@ import { EuiText, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import {
MANAGEMENT_ROUTING_ENDPOINTS_PATH,
MANAGEMENT_ROUTING_EVENT_FILTERS_PATH,
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_ROOT_PATH,
MANAGEMENT_ROUTING_TRUSTED_APPS_PATH,
@ -28,14 +29,22 @@ import { GetUrlForApp } from '../../common/components/navigation/types';
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
import { ADMINISTRATION } from '../../app/home/translations';
import { AdministrationSubTab } from '../types';
import { ENDPOINTS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from '../common/translations';
import {
ENDPOINTS_TAB,
EVENT_FILTERS_TAB,
POLICIES_TAB,
TRUSTED_APPS_TAB,
} from '../common/translations';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled';
import { EventFiltersContainer } from './event_filters';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
const TabNameMappedToI18nKey: Record<AdministrationSubTab, string> = {
[AdministrationSubTab.endpoints]: ENDPOINTS_TAB,
[AdministrationSubTab.policies]: POLICIES_TAB,
[AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB,
[AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB,
};
export function getBreadcrumbs(
@ -88,6 +97,7 @@ NoPermissions.displayName = 'NoPermissions';
export const ManagementContainer = memo(() => {
const history = useHistory();
const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled');
const { allEnabled: isIngestEnabled } = useIngestEnabledCheck();
if (!isIngestEnabled) {
@ -99,6 +109,11 @@ export const ManagementContainer = memo(() => {
<Route path={MANAGEMENT_ROUTING_ENDPOINTS_PATH} component={EndpointsContainer} />
<Route path={MANAGEMENT_ROUTING_POLICIES_PATH} component={PolicyContainer} />
<Route path={MANAGEMENT_ROUTING_TRUSTED_APPS_PATH} component={TrustedAppsContainer} />
{isEventFilteringEnabled && (
<Route path={MANAGEMENT_ROUTING_EVENT_FILTERS_PATH} component={EventFiltersContainer} />
)}
<Route
path={MANAGEMENT_ROUTING_ROOT_PATH}
exact

View file

@ -30,6 +30,7 @@ import {
ENTRY_PROPERTY_TITLES,
OPERATOR_TITLE,
} from '../../translations';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
const ConditionEntryCell = memo<{
showLabel: boolean;
@ -76,9 +77,7 @@ export const ConditionEntryInput = memo<ConditionEntryInputProps>(
onVisited,
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useCallback((suffix: string) => dataTestSubj && `${dataTestSubj}-${suffix}`, [
dataTestSubj,
]);
const getTestId = useTestIdGenerator(dataTestSubj);
const fieldOptions = useMemo<Array<EuiSuperSelectOption<string>>>(() => {
const getDropdownDisplay = (field: ConditionEntryField) => (

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import React, { memo, useCallback } from 'react';
import React, { memo } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiHideFor, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { ConditionEntry, OperatingSystem } from '../../../../../../../common/endpoint/types';
import { AndOrBadge } from '../../../../../../common/components/and_or_badge';
import { ConditionEntryInput, ConditionEntryInputProps } from '../condition_entry_input';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
const ConditionGroupFlexGroup = styled(EuiFlexGroup)`
// The positioning of the 'and-badge' is done by using the EuiButton's height and adding on to it
@ -63,14 +64,8 @@ export const ConditionGroup = memo<ConditionGroupProps>(
onVisited,
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useCallback(
(suffix: string): string | undefined => {
if (dataTestSubj) {
return `${dataTestSubj}-${suffix}`;
}
},
[dataTestSubj]
);
const getTestId = useTestIdGenerator(dataTestSubj);
return (
<ConditionGroupFlexGroup gutterSize="xs" data-test-subj={dataTestSubj}>
{entries.length > 1 && (

View file

@ -44,6 +44,7 @@ import { ABOUT_TRUSTED_APPS, CREATE_TRUSTED_APP_ERROR } from '../translations';
import { defaultNewTrustedApp } from '../../store/builders';
import { getTrustedAppsListPath } from '../../../../common/routing';
import { useToasts } from '../../../../../common/lib/kibana';
import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator';
type CreateTrustedAppFlyoutProps = Omit<EuiFlyoutProps, 'hideCloseButton'>;
export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
@ -81,14 +82,7 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
};
}, [isLoadingPolicies, policyList]);
const getTestId = useCallback(
(suffix: string): string | undefined => {
if (dataTestSubj) {
return `${dataTestSubj}-${suffix}`;
}
},
[dataTestSubj]
);
const getTestId = useTestIdGenerator(dataTestSubj);
const handleCancelClick = useCallback(() => {
if (creationInProgress) {

View file

@ -42,6 +42,7 @@ import {
EffectedPolicySelection,
EffectedPolicySelectProps,
} from './effected_policy_select';
import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator';
const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
OperatingSystem.MAC,
@ -212,14 +213,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
>
>({});
const getTestId = useCallback(
(suffix: string): string | undefined => {
if (dataTestSubj) {
return `${dataTestSubj}-${suffix}`;
}
},
[dataTestSubj]
);
const getTestId = useTestIdGenerator(dataTestSubj);
const notifyOfChange = useCallback(
(updatedFormValues: TrustedAppFormState['item']) => {

View file

@ -26,6 +26,7 @@ import { getPolicyDetailPath } from '../../../../../common/routing';
import { useFormatUrl } from '../../../../../../common/components/link_to';
import { SecurityPageName } from '../../../../../../../common/constants';
import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
const NOOP = () => {};
const DEFAULT_LIST_PROPS: EuiSelectableProps['listProps'] = { bordered: true, showIcons: false };
@ -69,14 +70,7 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
}) => {
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
const getTestId = useCallback(
(suffix): string | undefined => {
if (dataTestSubj) {
return `${dataTestSubj}-${suffix}`;
}
},
[dataTestSubj]
);
const getTestId = useTestIdGenerator(dataTestSubj);
const selectableOptions: EffectedPolicyOption[] = useMemo(() => {
const isPolicySelected = new Set<string>(selected.map((policy) => policy.id));

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import React, { memo, useCallback } from 'react';
import React, { memo } from 'react';
import { CommonProps, EuiText, EuiPanel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ConditionGroup, ConditionGroupProps } from '../condition_group';
import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator';
export type LogicalConditionBuilderProps = CommonProps & ConditionGroupProps;
export const LogicalConditionBuilder = memo<LogicalConditionBuilderProps>(
@ -23,14 +24,8 @@ export const LogicalConditionBuilder = memo<LogicalConditionBuilderProps>(
onVisited,
'data-test-subj': dataTestSubj,
}) => {
const getTestId = useCallback(
(suffix: string): string | undefined => {
if (dataTestSubj) {
return `${dataTestSubj}-${suffix}`;
}
},
[dataTestSubj]
);
const getTestId = useTestIdGenerator(dataTestSubj);
return (
<div data-test-subj={dataTestSubj} className={className}>
<div>

View file

@ -8,7 +8,6 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import {
createSampleTrustedApp,
@ -21,13 +20,7 @@ import {
} from '../../../test_utils';
import { TrustedAppsGrid } from '.';
import { getMockTheme } from '../../../../../../common/lib/kibana/kibana_react.mock';
const mockTheme = getMockTheme({
eui: {
euiSize: '16px',
},
});
import { EuiThemeProvider } from '../../../../../../../../../../src/plugins/kibana_react/common';
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: () => () => 'mockId',
@ -38,7 +31,7 @@ const now = 111111;
const renderList = (store: ReturnType<typeof createGlobalNoMiddlewareStore>) => {
const Wrapper: React.FC = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme={mockTheme}>{children}</ThemeProvider>
<EuiThemeProvider>{children}</EuiThemeProvider>
</Provider>
);

View file

@ -5,18 +5,10 @@
* 2.0.
*/
import React, { FC, memo, useCallback, useEffect } from 'react';
import {
EuiTablePagination,
EuiFlexGroup,
EuiFlexItem,
EuiProgress,
EuiIcon,
EuiText,
EuiSpacer,
} from '@elastic/eui';
import React, { memo, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { Pagination } from '../../../state';
import {
@ -33,49 +25,26 @@ import {
useTrustedAppsStoreActionCallback,
} from '../../hooks';
import { NO_RESULTS_MESSAGE } from '../../translations';
import { TrustedAppCard, TrustedAppCardProps } from '../trusted_app_card';
import { getTrustedAppsListPath } from '../../../../../common/routing';
import {
PaginatedContent,
PaginatedContentProps,
} from '../../../../../components/paginated_content';
import { TrustedApp } from '../../../../../../../common/endpoint/types';
export interface PaginationBarProps {
pagination: Pagination;
onChange: (pagination: { size: number; index: number }) => void;
}
const PaginationBar = ({ pagination, onChange }: PaginationBarProps) => {
const pageCount = Math.ceil(pagination.totalItemCount / pagination.pageSize);
type TrustedAppCardType = typeof TrustedAppCard;
useEffect(() => {
if (pageCount > 0 && pageCount < pagination.pageIndex + 1) {
onChange({ index: pageCount - 1, size: pagination.pageSize });
}
}, [pageCount, onChange, pagination]);
return (
<div>
<EuiTablePagination
activePage={pagination.pageIndex}
itemsPerPage={pagination.pageSize}
itemsPerPageOptions={pagination.pageSizeOptions}
pageCount={pageCount}
onChangeItemsPerPage={useCallback((size) => onChange({ index: 0, size }), [onChange])}
onChangePage={useCallback((index) => onChange({ index, size: pagination.pageSize }), [
pagination.pageSize,
onChange,
])}
/>
</div>
);
};
const GridMessage: FC = ({ children }) => (
<div className="euiTextAlign--center">
<EuiSpacer size="m" />
{children}
<EuiSpacer size="m" />
</div>
);
const RootWrapper = styled.div`
.trusted-app + .trusted-app {
margin-top: ${({ theme }) => theme.eui.spacerSizes.l};
}
`;
export const TrustedAppsGrid = memo(() => {
const history = useHistory();
@ -103,55 +72,32 @@ export const TrustedAppsGrid = memo(() => {
[history, location]
);
const handlePaginationChange = useTrustedAppsNavigateCallback(({ index, size }) => ({
page_index: index,
page_size: size,
const handlePaginationChange: PaginatedContentProps<
TrustedApp,
TrustedAppCardType
>['onChange'] = useTrustedAppsNavigateCallback(({ pageIndex, pageSize }) => ({
page_index: pageIndex,
page_size: pageSize,
}));
return (
<EuiFlexGroup direction="column" gutterSize="none">
{isLoading && (
<EuiFlexItem grow={false}>
<EuiProgress size="xs" color="primary" />
</EuiFlexItem>
)}
<EuiFlexItem>
{error && (
<GridMessage>
<EuiIcon type="minusInCircle" color="danger" /> {error}
</GridMessage>
)}
{!error && listItems.length === 0 && (
<GridMessage>
<EuiText size="s">{NO_RESULTS_MESSAGE}</EuiText>
</GridMessage>
)}
{!error && listItems.length > 0 && (
<>
<EuiSpacer size="l" />
<EuiFlexGroup direction="column">
{listItems.map((item) => (
<EuiFlexItem grow={false} key={item.id}>
<TrustedAppCard
trustedApp={item}
onDelete={handleTrustedAppDelete}
onEdit={handleTrustedAppEdit}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</>
)}
</EuiFlexItem>
{!error && pagination.totalItemCount > 0 && (
<EuiFlexItem grow={false}>
<EuiSpacer size="l" />
<PaginationBar pagination={pagination} onChange={handlePaginationChange} />
</EuiFlexItem>
)}
</EuiFlexGroup>
<RootWrapper>
<PaginatedContent<TrustedApp, TrustedAppCardType>
items={listItems as TrustedApp[]}
onChange={handlePaginationChange}
ItemComponent={TrustedAppCard}
itemComponentProps={(ta) => ({
trustedApp: ta,
onDelete: handleTrustedAppDelete,
onEdit: handleTrustedAppEdit,
className: 'trusted-app',
})}
loading={isLoading}
itemId="id"
error={error}
pagination={pagination}
/>
</RootWrapper>
);
});

View file

@ -150,10 +150,6 @@ export const LIST_VIEW_TOGGLE_LABEL = i18n.translate(
}
);
export const NO_RESULTS_MESSAGE = i18n.translate('xpack.securitySolution.trustedapps.noResults', {
defaultMessage: 'No items found',
});
export const CREATE_TRUSTED_APP_ERROR: { [K in string]: string } = {
[`duplicatedEntry.${ConditionEntryField.HASH}`]: i18n.translate(
'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.error.duplicated.hash',

View file

@ -32,6 +32,7 @@ export enum AdministrationSubTab {
endpoints = 'endpoints',
policies = 'policy',
trustedApps = 'trusted_apps',
eventFilters = 'event_filters',
}
/**

View file

@ -20354,7 +20354,6 @@
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "エントリを削除",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません",
"xpack.securitySolution.trustedapps.noResults": "項目が見つかりません",
"xpack.securitySolution.trustedapps.trustedapp.createdAt": "作成日",
"xpack.securitySolution.trustedapps.trustedapp.createdBy": "作成者",
"xpack.securitySolution.trustedapps.trustedapp.description": "説明",

View file

@ -20680,7 +20680,6 @@
"xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.removeLabel": "移除条目",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND",
"xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件",
"xpack.securitySolution.trustedapps.noResults": "找不到项目",
"xpack.securitySolution.trustedapps.trustedapp.createdAt": "创建日期",
"xpack.securitySolution.trustedapps.trustedapp.createdBy": "创建者",
"xpack.securitySolution.trustedapps.trustedapp.description": "描述",