Skip to content

Commit

Permalink
adds export modals
Browse files Browse the repository at this point in the history
  • Loading branch information
dplumlee committed Jan 4, 2023
1 parent fd9656a commit 43d0e4f
Show file tree
Hide file tree
Showing 20 changed files with 245 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ interface ExceptionListHeaderComponentProps {
canUserEditList?: boolean;
securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common
onEditListDetails: (listDetails: ListDetails) => void;
onExportList: () => void;
onExportList: (includeExpiredExceptions: boolean) => void;
onDeleteList: () => void;
onManageRules: () => void;
onExportModalOpen: () => void;
}

export interface BackOptions {
Expand All @@ -54,6 +55,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
onExportList,
onDeleteList,
onManageRules,
onExportModalOpen,
}) => {
const { isModalVisible, listDetails, onEdit, onSave, onCancel } = useExceptionListHeader({
name,
Expand Down Expand Up @@ -100,6 +102,7 @@ const ExceptionListHeaderComponent: FC<ExceptionListHeaderComponentProps> = ({
onExportList={onExportList}
onDeleteList={onDeleteList}
onManageRules={onManageRules}
onExportModalOpen={onExportModalOpen}
/>,
]}
breadcrumbs={[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ interface MenuItemsProps {
linkedRules: Rule[];
canUserEditList?: boolean;
securityLinkAnchorComponent: React.ElementType; // This property needs to be removed to avoid the Prop Drilling, once we move all the common components from x-pack/security-solution/common
onExportList: () => void;
onExportList: (includeExpiredExceptions: boolean) => void;
onDeleteList: () => void;
onManageRules: () => void;
onExportModalOpen: () => void;
}

const MenuItemsComponent: FC<MenuItemsProps> = ({
Expand All @@ -32,6 +33,7 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
onExportList,
onDeleteList,
onManageRules,
onExportModalOpen,
}) => {
const referencedLinks = useMemo(
() =>
Expand Down Expand Up @@ -95,7 +97,7 @@ const MenuItemsComponent: FC<MenuItemsProps> = ({
icon: 'exportAction',
label: i18n.EXCEPTION_LIST_HEADER_EXPORT_ACTION,
onClick: () => {
if (typeof onExportList === 'function') onExportList();
if (typeof onExportList === 'function') onExportModalOpen();
},
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/* eslint-disable @typescript-eslint/naming-convention */

import * as t from 'io-ts';

export const include_expired_exceptions = t.keyof({ true: null, false: null });
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export const getExportExceptionListQuerySchemaMock = (): ExportExceptionListQuer
id: ID,
list_id: LIST_ID,
namespace_type: NAMESPACE_TYPE,
include_expired_exceptions: 'true',
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import * as t from 'io-ts';

import { id } from '../../common/id';
import { include_expired_exceptions } from '../../common/include_expired_exceptions';
import { list_id } from '../../common/list_id';
import { namespace_type } from '../../common/namespace_type';

Expand All @@ -17,6 +18,7 @@ export const exportExceptionListQuerySchema = t.exact(
id,
list_id,
namespace_type,
include_expired_exceptions,
// TODO: Add file_name here with a default value
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface ApiCallMemoProps {
// remove unnecessary validation checks
export interface ApiListExportProps {
id: string;
includeExpiredExceptions: boolean;
listId: string;
namespaceType: NamespaceType;
onError: (err: Error) => void;
Expand Down Expand Up @@ -133,6 +134,7 @@ export interface ExportExceptionListProps {
id: string;
listId: string;
namespaceType: NamespaceType;
includeExpiredExceptions: boolean;
signal: AbortSignal;
}

Expand Down
11 changes: 9 additions & 2 deletions packages/kbn-securitysolution-list-api/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,10 +532,11 @@ const addEndpointExceptionListWithValidation = async ({
export { addEndpointExceptionListWithValidation as addEndpointExceptionList };

/**
* Fetch an ExceptionList by providing a ExceptionList ID
* Export an ExceptionList by providing a ExceptionList ID
*
* @param http Kibana http service
* @param id ExceptionList ID (not list_id)
* @param includeExpiredExceptions boolean for including expired exceptions
* @param listId ExceptionList LIST_ID (not id)
* @param namespaceType ExceptionList namespace_type
* @param signal to cancel request
Expand All @@ -545,13 +546,19 @@ export { addEndpointExceptionListWithValidation as addEndpointExceptionList };
export const exportExceptionList = async ({
http,
id,
includeExpiredExceptions,
listId,
namespaceType,
signal,
}: ExportExceptionListProps): Promise<Blob> =>
http.fetch<Blob>(`${EXCEPTION_LIST_URL}/_export`, {
method: 'POST',
query: { id, list_id: listId, namespace_type: namespaceType },
query: {
id,
list_id: listId,
namespace_type: namespaceType,
include_expired_exceptions: includeExpiredExceptions,
},
signal,
});

Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-securitysolution-list-hooks/src/use_api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
},
async exportExceptionList({
id,
includeExpiredExceptions,
listId,
namespaceType,
onError,
Expand All @@ -123,6 +124,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => {
const blob = await Api.exportExceptionList({
http,
id,
includeExpiredExceptions,
listId,
namespaceType,
signal: abortCtrl.signal,
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/lists/public/exceptions/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ describe('Exceptions Lists API', () => {
await exportExceptionList({
http: httpMock,
id: 'some-id',
includeExpiredExceptions: true,
listId: 'list-id',
namespaceType: 'single',
signal: abortCtrl.signal,
Expand All @@ -718,6 +719,7 @@ describe('Exceptions Lists API', () => {
const exceptionResponse = await exportExceptionList({
http: httpMock,
id: 'some-id',
includeExpiredExceptions: true,
listId: 'list-id',
namespaceType: 'single',
signal: abortCtrl.signal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,17 @@ export const exportExceptionsRoute = (router: ListsPluginRouter): void => {
const siemResponse = buildSiemResponse(response);

try {
const { id, list_id: listId, namespace_type: namespaceType } = request.query;
const {
id,
list_id: listId,
namespace_type: namespaceType,
include_expired_exceptions: includeExpiredExceptionsString,
} = request.query;
const exceptionListsClient = await getExceptionListClient(context);

const includeExpiredExceptions = includeExpiredExceptionsString === 'true';
const exportContent = await exceptionListsClient.exportExceptionListAndItems({
id,
includeExpiredExceptions,
listId,
namespaceType,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,7 @@ export class ExceptionListClient {
listId,
id,
namespaceType,
includeExpiredExceptions,
}: ExportExceptionListAndItemsOptions): Promise<ExportExceptionListAndItemsReturn | null> => {
const { savedObjectsClient } = this;

Expand All @@ -986,6 +987,7 @@ export class ExceptionListClient {
'exceptionsListPreExport',
{
id,
includeExpiredExceptions,
listId,
namespaceType,
},
Expand All @@ -995,6 +997,7 @@ export class ExceptionListClient {

return exportExceptionListAndItems({
id,
includeExpiredExceptions,
listId,
namespaceType,
savedObjectsClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,8 @@ export interface ExportExceptionListAndItemsOptions {
id: IdOrUndefined;
/** saved object namespace (single | agnostic) */
namespaceType: NamespaceType;
/** whether or not to include expired exceptions */
includeExpiredExceptions: boolean;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
} from '@kbn/securitysolution-io-ts-list-types';
import { transformDataToNdjson } from '@kbn/securitysolution-utils';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { getSavedObjectType } from '@kbn/securitysolution-list-utils';

import { findExceptionListItemPointInTimeFinder } from './find_exception_list_item_point_in_time_finder';
import { getExceptionList } from './get_exception_list';
Expand All @@ -24,6 +25,7 @@ interface ExportExceptionListAndItemsOptions {
listId: ListIdOrUndefined;
savedObjectsClient: SavedObjectsClientContract;
namespaceType: NamespaceType;
includeExpiredExceptions: boolean;
}

export interface ExportExceptionListAndItemsReturn {
Expand All @@ -35,6 +37,7 @@ export const exportExceptionListAndItems = async ({
id,
listId,
namespaceType,
includeExpiredExceptions,
savedObjectsClient,
}: ExportExceptionListAndItemsOptions): Promise<ExportExceptionListAndItemsReturn | null> => {
const exceptionList = await getExceptionList({
Expand All @@ -52,10 +55,14 @@ export const exportExceptionListAndItems = async ({
const executeFunctionOnStream = (response: FoundExceptionListItemSchema): void => {
exceptionItems = [...exceptionItems, ...response.data];
};
const savedObjectPrefix = getSavedObjectType({ namespaceType });
const filter = includeExpiredExceptions
? undefined
: `(${savedObjectPrefix}.attributes.expire_time > "${new Date().toISOString()}" OR NOT ${savedObjectPrefix}.attributes.expire_time: *)`;

await findExceptionListItemPointInTimeFinder({
executeFunctionOnStream,
filter: undefined,
filter,
listId: exceptionList.list_id,
maxSize: undefined, // NOTE: This is unbounded when it is "undefined"
namespaceType: exceptionList.namespace_type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { ListExceptionItems } from '../list_exception_items';
import { useListDetailsView } from '../../hooks';
import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card';
import { ManageRules } from '../manage_rules';
import { ExportExceptionsListModal } from '../export_exceptions_list_modal';

interface ExceptionsListCardProps {
exceptionsList: ExceptionListInfo;
Expand All @@ -47,10 +48,12 @@ interface ExceptionsListCardProps {
}) => () => Promise<void>;
handleExport: ({
id,
includeExpiredExceptions,
listId,
namespaceType,
}: {
id: string;
includeExpiredExceptions: boolean;
listId: string;
namespaceType: NamespaceType;
}) => () => Promise<void>;
Expand Down Expand Up @@ -115,6 +118,9 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
emptyViewerTitle,
emptyViewerBody,
emptyViewerButtonText,
handleCancelExportModal,
handleConfirmExportModal,
showExportModal,
} = useExceptionsListCard({
exceptionsList,
handleExport,
Expand Down Expand Up @@ -246,6 +252,12 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
onSave={onSaveManageRules}
onCancel={onCancelManageRules}
onRuleSelectionChange={onRuleSelectionChange}
/>
) : null}
{showExportModal ? (
<ExportExceptionsListModal
handleCloseModal={handleCancelExportModal}
onModalConfirm={handleConfirmExportModal}
/>
) : null}
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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, useCallback, useState } from 'react';

import { EuiConfirmModal, EuiSwitch } from '@elastic/eui';
import * as i18n from '../../translations';

interface ExportExceptionsListModalProps {
handleCloseModal: () => void;
onModalConfirm: (includeExpired: boolean) => void;
}

export const ExportExceptionsListModal = memo<ExportExceptionsListModalProps>(
({ handleCloseModal, onModalConfirm }) => {
const [exportExpired, setExportExpired] = useState(true);

const handleSwitchChange = useCallback(() => {
setExportExpired(!exportExpired);
}, [setExportExpired, exportExpired]);

const handleConfirm = useCallback(() => {
onModalConfirm(exportExpired);
handleCloseModal();
}, [exportExpired, handleCloseModal, onModalConfirm]);

return (
<EuiConfirmModal
title={i18n.EXPORT_MODAL_TITLE}
onCancel={handleCloseModal}
onConfirm={handleConfirm}
cancelButtonText={i18n.EXPORT_MODAL_CANCEL_BUTTON}
confirmButtonText={i18n.EXPORT_MODAL_CONFIRM_BUTTON}
defaultFocusedButton="confirm"
>
<EuiSwitch
label={i18n.EXPORT_MODAL_INCLUDE_SWITCH_LABEL}
checked={exportExpired}
onChange={handleSwitchChange}
/>
</EuiConfirmModal>
);
}
);

ExportExceptionsListModal.displayName = 'ExportExceptionsListModal';
Loading

0 comments on commit 43d0e4f

Please sign in to comment.