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 @@ -8,10 +8,13 @@
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';

import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui';
import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui';
import type { EuiFlyoutSize } from '@elastic/eui/src/components/flyout/flyout';
import { useLocation } from 'react-router-dom';
import { useIsMounted } from '@kbn/securitysolution-hook-utils';
import { HeaderMenu } from '@kbn/securitysolution-exception-list-components';
import { useApi } from '@kbn/securitysolution-list-hooks';
import { AutoDownload } from '../../../common/components/auto_download/auto_download';
import type { ServerApiError } from '../../../common/types';
import { AdministrationListPage } from '../administration_list_page';

Expand Down Expand Up @@ -41,10 +44,11 @@ import { useUrlParams } from '../../hooks/use_url_params';
import type { ListPageRouteState, MaybeImmutable } from '../../../../common/endpoint/types';
import { DEFAULT_EXCEPTION_LIST_ITEM_SEARCHABLE_FIELDS } from '../../../../common/endpoint/service/artifacts/constants';
import { ArtifactDeleteModal } from './components/artifact_delete_modal';
import { useToasts } from '../../../common/lib/kibana';
import { useKibana, useToasts } from '../../../common/lib/kibana';
import { useMemoizedRouteState } from '../../common/hooks';
import { BackToExternalAppSecondaryButton } from '../back_to_external_app_secondary_button';
import { BackToExternalAppButton } from '../back_to_external_app_button';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';

type ArtifactEntryCardType = typeof ArtifactEntryCard;

Expand Down Expand Up @@ -92,6 +96,8 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
allowCardDeleteAction = true,
CardDecorator,
}) => {
const { services } = useKibana();
const { http } = services;
const { state: routeState } = useLocation<ListPageRouteState | undefined>();
const getTestId = useTestIdGenerator(dataTestSubj);
const toasts = useToasts();
Expand All @@ -101,6 +107,10 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
const {
urlParams: { filter, includedPolicies },
} = useUrlParams<ArtifactListPageUrlParams>();
const { exportExceptionList } = useApi(http);
const areEndpointExceptionsMovedUnderManagementFFEnabled = useIsExperimentalFeatureEnabled(
'endpointExceptionsMovedUnderManagement'
);

const {
isPageInitializing,
Expand Down Expand Up @@ -131,6 +141,8 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
undefined | ExceptionListItemSchema
>(undefined);

const [exportedData, setExportedData] = useState<Blob>();

const labels = useMemo<typeof artifactListPageLabels>(() => {
return {
...artifactListPageLabels,
Expand Down Expand Up @@ -238,6 +250,33 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
setSelectedItemForEdit(undefined);
}, []);

const handleExport = useCallback(
() =>
exportExceptionList({
id: apiClient.listId,
listId: apiClient.listId,
includeExpiredExceptions: true,
namespaceType: 'agnostic',

onError: (exportError: Error) =>
toasts?.addError(exportError, { title: labels.pageExportErrorToastTitle }),

onSuccess: (blob) => {
setExportedData(blob);
toasts?.addSuccess(labels.pageExportSuccessToastTitle);
},
}),
[
exportExceptionList,
apiClient.listId,
toasts,
labels.pageExportErrorToastTitle,
labels.pageExportSuccessToastTitle,
]
);

const handleOnDownload = useCallback(() => setExportedData(undefined), []);

const description = useMemo(() => {
const subtitleText = labels.pageAboutInfo ? (
<span data-test-subj="header-panel-subtitle">{labels.pageAboutInfo}</span>
Expand Down Expand Up @@ -267,20 +306,51 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
title={labels.pageTitle}
subtitle={description}
actions={
allowCardCreateAction && (
<EuiButton
fill
iconType="plusInCircle"
isDisabled={isFlyoutOpened}
onClick={handleOpenCreateFlyoutClick}
data-test-subj={getTestId('pageAddButton')}
>
{labels.pageAddButtonTitle}
</EuiButton>
)
<EuiFlexGroup alignItems="center">
{allowCardCreateAction && (
<EuiButton
fill
iconType="plusInCircle"
isDisabled={isFlyoutOpened}
onClick={handleOpenCreateFlyoutClick}
data-test-subj={getTestId('pageAddButton')}
>
{labels.pageAddButtonTitle}
</EuiButton>
)}

{areEndpointExceptionsMovedUnderManagementFFEnabled && (
<HeaderMenu
iconType="boxesHorizontal"
dataTestSubj={getTestId('exportImportMenu')}
actions={[
{
key: 'ImportButton',
icon: 'importAction',
label: labels.pageImportButtonTitle,
onClick: () => {},
disabled: !allowCardCreateAction,
},
{
key: 'ExportButton',
icon: 'exportAction',
label: labels.pageExportButtonTitle,
onClick: handleExport,
},
]}
disableActions={isLoading}
/>
)}
</EuiFlexGroup>
}
data-test-subj={getTestId('container')}
>
<AutoDownload
blob={exportedData}
name={`${apiClient.listId}.ndjson`}
onDownload={handleOnDownload}
/>

{isFlyoutOpened && (
<ArtifactFlyout
apiClient={apiClient}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ describe('When using the ArtifactListPage component', () => {
let history: AppContextTestRender['history'];
let mockedApi: ReturnType<typeof trustedAppsAllHttpMocks>;
let getFirstCard: ArtifactListPageRenderingSetup['getFirstCard'];
let setExperimentalFlag: ArtifactListPageRenderingSetup['setExperimentalFlag'];

beforeEach(() => {
const renderSetup = getArtifactListPageRenderingSetup();

({ history, mockedApi, getFirstCard } = renderSetup);
({ history, mockedApi, getFirstCard, setExperimentalFlag } = renderSetup);

mockUseGetEndpointSpecificPolicies.mockReturnValue({
data: mockedApi.responseProvider.endpointPackagePolicyList(),
Expand Down Expand Up @@ -149,6 +150,46 @@ describe('When using the ArtifactListPage component', () => {
expect(getAllByText('mock decorator')).toHaveLength(10);
});

describe('Import and export', () => {
beforeEach(() => {
setExperimentalFlag({ endpointExceptionsMovedUnderManagement: true });
});

it('should not show import and export actions with feature flag disabled', async () => {
setExperimentalFlag({ endpointExceptionsMovedUnderManagement: false });

const { queryByTestId } = await renderWithListData();

expect(queryByTestId('testPage-exportImportMenuButtonIcon')).not.toBeInTheDocument();
});

it('should show import and export actions', async () => {
const { getByTestId } = await renderWithListData();

expect(getByTestId('testPage-exportImportMenuButtonIcon')).toBeInTheDocument();

await userEvent.click(getByTestId('testPage-exportImportMenuButtonIcon'));
expect(getByTestId('testPage-exportImportMenuActionItemImportButton')).toBeInTheDocument();
expect(getByTestId('testPage-exportImportMenuActionItemExportButton')).toBeInTheDocument();
});

it('should enable import and export buttons when user can create artifacts', async () => {
const { getByTestId } = await renderWithListData({ allowCardCreateAction: true });

await userEvent.click(getByTestId('testPage-exportImportMenuButtonIcon'));
expect(getByTestId('testPage-exportImportMenuActionItemImportButton')).toBeEnabled();
expect(getByTestId('testPage-exportImportMenuActionItemExportButton')).toBeEnabled();
});

it('should disable import button when user cannot create artifacts', async () => {
const { getByTestId } = await renderWithListData({ allowCardCreateAction: false });

await userEvent.click(getByTestId('testPage-exportImportMenuButtonIcon'));
expect(getByTestId('testPage-exportImportMenuActionItemImportButton')).toBeDisabled();
expect(getByTestId('testPage-exportImportMenuActionItemExportButton')).toBeEnabled();
});
});

describe('and interacting with card actions', () => {
const clickCardAction = async (action: 'edit' | 'delete') => {
await getFirstCard({ showActions: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ export const artifactListPageLabels = Object.freeze({
pageAddButtonTitle: i18n.translate('xpack.securitySolution.artifactListPage.addButtonTitle', {
defaultMessage: 'Add artifact',
}),
pageImportButtonTitle: i18n.translate(
'xpack.securitySolution.artifactListPage.importButtonTitle',
{
defaultMessage: 'Import artifact list',
}
),
pageExportButtonTitle: i18n.translate(
'xpack.securitySolution.artifactListPage.exportButtonTitle',
{
defaultMessage: 'Export artifact list',
}
),
pageExportSuccessToastTitle: i18n.translate(
'xpack.securitySolution.artifactListPage.exportSuccessToastTitle',
{
defaultMessage: 'Artifact list exported successfully',
}
),
pageExportErrorToastTitle: i18n.translate(
'xpack.securitySolution.artifactListPage.exportErrorToastTitle',
{
defaultMessage: 'Artifact list export failed',
}
),

// ------------------------------
// EMPTY state labels
Expand Down Expand Up @@ -108,6 +132,10 @@ export type ArtifactListPageRequiredLabels = Pick<
| 'pageTitle'
| 'pageAboutInfo'
| 'pageAddButtonTitle'
| 'pageImportButtonTitle'
| 'pageExportButtonTitle'
| 'pageExportSuccessToastTitle'
| 'pageExportErrorToastTitle'
| 'getShowingCountLabel'
| 'cardActionEditLabel'
| 'cardActionDeleteLabel'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import { EuiLink } from '@elastic/eui';

import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { useHttp } from '../../../../common/lib/kibana';
import type { ArtifactListPageProps } from '../../../components/artifact_list_page';
import type { ArtifactListPageLabels } from '../../../components/artifact_list_page';
import { ArtifactListPage } from '../../../components/artifact_list_page';
import { BlocklistsApiClient } from '../services';
import { BlockListForm } from './components/blocklist_form';

const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = {
const BLOCKLIST_PAGE_LABELS: ArtifactListPageLabels = {
pageTitle: i18n.translate('xpack.securitySolution.blocklist.pageTitle', {
defaultMessage: 'Blocklist',
}),
Expand All @@ -29,6 +29,24 @@ const BLOCKLIST_PAGE_LABELS: ArtifactListPageProps['labels'] = {
pageAddButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageAddButtonTitle', {
defaultMessage: 'Add blocklist entry',
}),
pageImportButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageImportButtonTitle', {
defaultMessage: 'Import blocklist',
}),
pageExportButtonTitle: i18n.translate('xpack.securitySolution.blocklist.pageExportButtonTitle', {
defaultMessage: 'Export blocklist',
}),
pageExportSuccessToastTitle: i18n.translate(
'xpack.securitySolution.blocklist.pageExportSuccessToastTitle',
{
defaultMessage: 'Blocklist exported successfully',
}
),
pageExportErrorToastTitle: i18n.translate(
'xpack.securitySolution.blocklist.pageExportErrorToastTitle',
{
defaultMessage: 'Blocklist export failed',
}
),
getShowingCountLabel: (total) =>
i18n.translate('xpack.securitySolution.blocklist.showingTotal', {
defaultMessage:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ export const ENDPOINT_EXCEPTIONS_PAGE_LABELS: ArtifactListPageLabels = {
'xpack.securitySolution.endpointExceptions.pageAddButtonTitle',
{ defaultMessage: 'Add endpoint exception' }
),
pageImportButtonTitle: i18n.translate(
'xpack.securitySolution.endpointExceptions.pageImportButtonTitle',
{
defaultMessage: 'Import endpoint exception list',
}
),
pageExportButtonTitle: i18n.translate(
'xpack.securitySolution.endpointExceptions.pageExportButtonTitle',
{
defaultMessage: 'Export endpoint exception list',
}
),
pageExportSuccessToastTitle: i18n.translate(
'xpack.securitySolution.endpointExceptions.exportSuccessToastTitle',
{
defaultMessage: 'Endpoint exception list exported successfully',
}
),
pageExportErrorToastTitle: i18n.translate(
'xpack.securitySolution.endpointExceptions.exportErrorToastTitle',
{
defaultMessage: 'Endpoint exception list export failed',
}
),
getShowingCountLabel: (total) =>
i18n.translate('xpack.securitySolution.endpointExceptions.showingTotal', {
defaultMessage:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { EuiLink } from '@elastic/eui';

import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { useHttp } from '../../../../common/lib/kibana';
import type { ArtifactListPageProps } from '../../../components/artifact_list_page';
import type { ArtifactListPageLabels } from '../../../components/artifact_list_page';
import { ArtifactListPage } from '../../../components/artifact_list_page';
import { EventFiltersApiClient } from '../service/api_client';
import { EventFiltersForm } from './components/form';
Expand Down Expand Up @@ -50,7 +50,7 @@ export const RULE_NAME = i18n.translate('xpack.securitySolution.eventFilter.form
defaultMessage: 'Endpoint Event Filtering',
});

const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageProps['labels'] = {
const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageLabels = {
pageTitle: i18n.translate('xpack.securitySolution.eventFilters.pageTitle', {
defaultMessage: 'Event Filters',
}),
Expand All @@ -61,6 +61,30 @@ const EVENT_FILTERS_PAGE_LABELS: ArtifactListPageProps['labels'] = {
pageAddButtonTitle: i18n.translate('xpack.securitySolution.eventFilters.pageAddButtonTitle', {
defaultMessage: 'Add event filter',
}),
pageImportButtonTitle: i18n.translate(
'xpack.securitySolution.eventFilters.pageImportButtonTitle',
{
defaultMessage: 'Import event filter list',
}
),
pageExportButtonTitle: i18n.translate(
'xpack.securitySolution.eventFilters.pageExportButtonTitle',
{
defaultMessage: 'Export event filter list',
}
),
pageExportSuccessToastTitle: i18n.translate(
'xpack.securitySolution.eventFilters.exportSuccessToastTitle',
{
defaultMessage: 'Event filter list exported successfully',
}
),
pageExportErrorToastTitle: i18n.translate(
'xpack.securitySolution.eventFilters.exportErrorToastTitle',
{
defaultMessage: 'Event filter list export failed',
}
),
getShowingCountLabel: (total) =>
i18n.translate('xpack.securitySolution.eventFilters.showingTotal', {
defaultMessage: 'Showing {total} {total, plural, one {event filter} other {event filters}}',
Expand Down
Loading
Loading