From f8076000ba5be23e69651365a8db091683eed759 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Fri, 26 Jun 2020 15:40:54 -0500 Subject: [PATCH 01/27] Add Frontend components for Value Lists Management Modal Imports and uses the hooks provided by the lists plugin. Tests coming next. --- .../value_lists_management_modal/form.tsx | 165 ++++++++++++++++++ .../value_lists_management_modal/index.tsx | 7 + .../value_lists_management_modal/modal.tsx | 144 +++++++++++++++ .../value_lists_management_modal/table.tsx | 96 ++++++++++ .../translations.ts | 114 ++++++++++++ .../pages/detection_engine/rules/index.tsx | 14 ++ .../detection_engine/rules/translations.ts | 7 + .../public/common/lib/kibana/hooks.ts | 7 +- .../public/lists_plugin_deps.ts | 6 + 9 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx new file mode 100644 index 0000000000000..8245200e05987 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useState, ReactNode, useEffect, useRef } from 'react'; +import styled from 'styled-components'; +import { + EuiButton, + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiRadioGroup, +} from '@elastic/eui'; + +import { useImportList, ListSchema, Type } from '../../../lists_plugin_deps'; +import * as i18n from './translations'; +import { useKibana } from '../../../common/lib/kibana'; + +const InlineRadioGroup = styled(EuiRadioGroup)` + display: flex; + + .euiRadioGroup__item + .euiRadioGroup__item { + margin: 0 0 0 12px; + } +`; + +interface ListTypeOptions { + id: Type; + label: ReactNode; +} + +const options: ListTypeOptions[] = [ + { + id: 'keyword', + label: i18n.KEYWORDS_RADIO, + }, + { + id: 'ip', + label: i18n.IP_RADIO, + }, +]; + +const defaultListType: Type = 'keyword'; + +export interface ValueListsFormProps { + onError: (error: Error) => void; + onSuccess: (response: ListSchema) => void; +} + +export const ValueListsFormComponent: React.FC = ({ onError, onSuccess }) => { + const [files, setFiles] = useState(null); + const [type, setType] = useState(defaultListType); + const filePickerRef = useRef(null); + const { http } = useKibana().services; + const { start: importList, abort: abortImport, ...importState } = useImportList(); + + // EuiRadioGroup's onChange only infers 'string' from our options + const handleRadioChange = useCallback((t: string) => setType(t as Type), [setType]); + + const resetForm = useCallback(() => { + if (filePickerRef.current?.fileInput) { + filePickerRef.current.fileInput.value = ''; + filePickerRef.current.handleChange(); + } + setFiles(null); + setType(defaultListType); + }, [setType]); + + const handleCancel = useCallback(() => { + abortImport(); + }, [abortImport]); + + const handleSuccess = useCallback( + (response: ListSchema) => { + resetForm(); + onSuccess(response); + }, + [resetForm, onSuccess] + ); + const handleError = useCallback( + (error: Error) => { + onError(error); + }, + [onError] + ); + + const handleImport = useCallback(() => { + if (!importState.loading && files && files.length) { + try { + importList({ + file: files[0], + listId: undefined, + http, + type, + }) + .then(handleSuccess) + .catch(handleError); + } catch (error) { + handleError(error); + } + } + }, [importState.loading, files, importList, http, type, handleSuccess, handleError]); + + useEffect(() => { + return handleCancel; + }, [handleCancel]); + + return ( + + + + + + + + + + + + + + + + {importState.loading && ( + {i18n.CANCEL_BUTTON} + )} + + + + {i18n.UPLOAD_BUTTON} + + + + + + + + + ); +}; + +ValueListsFormComponent.displayName = 'ValueListsFormComponent'; + +export const ValueListsForm = React.memo(ValueListsFormComponent); + +ValueListsForm.displayName = 'ValueListsForm'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/index.tsx new file mode 100644 index 0000000000000..1fbe0e312bd8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ValueListsModal } from './modal'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx new file mode 100644 index 0000000000000..4d159a5bdfd22 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, +} from '@elastic/eui'; + +import { ListSchema, useExportList, useFindLists, useDeleteList } from '../../../lists_plugin_deps'; +import { useToasts, useKibana } from '../../../common/lib/kibana'; +import { GenericDownloader } from '../../../common/components/generic_downloader'; +import * as i18n from './translations'; +import { ValueListsTable } from './table'; +import { ValueListsForm } from './form'; + +interface ValueListsModalProps { + onClose: () => void; + showModal: boolean; +} + +export const ValueListsModalComponent: React.FC = ({ + onClose, + showModal, +}) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(20); + const { http } = useKibana().services; + const { start: findLists, ...lists } = useFindLists(); + const { start: deleteList } = useDeleteList(); + const { start: exportList } = useExportList(); + const [exportListId, setExportListId] = useState(); + const toasts = useToasts(); + + const fetchLists = useCallback(() => { + findLists({ http, pageIndex: pageIndex + 1, pageSize }); + }, [http, findLists, pageIndex, pageSize]); + + const handleDelete = useCallback( + async ({ id }: { id: string }) => { + await deleteList({ http, id }); + fetchLists(); + }, + [deleteList, http, fetchLists] + ); + + const handleExport = useCallback( + async ({ ids }: { ids: string[] }) => exportList({ http, listId: ids[0] }), + [http, exportList] + ); + const handleExportClick = useCallback(({ id }: { id: string }) => setExportListId(id), []); + const handleExportComplete = useCallback(() => setExportListId(undefined), []); + + const handleTableChange = useCallback( + ({ page: { index, size } }: { page: { index: number; size: number } }) => { + setPageIndex(index); + setPageSize(size); + }, + [setPageIndex, setPageSize] + ); + const handleUploadError = useCallback( + (error: Error) => { + if (error.name !== 'AbortError') { + toasts.addError(error, { title: i18n.UPLOAD_ERROR }); + } + }, + [toasts] + ); + const handleUploadSuccess = useCallback( + (response: ListSchema) => { + toasts.addSuccess({ + text: i18n.uploadSuccessMessage(response.name), + title: i18n.UPLOAD_SUCCESS, + }); + fetchLists(); + }, + [fetchLists, toasts] + ); + + useEffect(() => { + if (showModal) { + fetchLists(); + } + }, [showModal, fetchLists]); + + if (!showModal) { + return null; + } + + const pagination = { + pageIndex, + pageSize, + totalItemCount: lists.result?.total ?? 0, + pageSizeOptions: [10, 20, 50], + hidePerPageOptions: false, + }; + + return ( + + + + {i18n.MODAL_TITLE} + + + + + + + + {i18n.CLOSE_BUTTON} + + + + + ); +}; + +ValueListsModalComponent.displayName = 'ValueListsModalComponent'; + +export const ValueListsModal = React.memo(ValueListsModalComponent); + +ValueListsModal.displayName = 'ValueListsModal'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx new file mode 100644 index 0000000000000..b01c35ed8e29b --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiBasicTable, EuiBasicTableProps, EuiText, EuiPanel } from '@elastic/eui'; + +import { ListSchema } from '../../../../../lists/common/schemas/response'; +import * as i18n from './translations'; + +type TableProps = EuiBasicTableProps; +type ActionCallback = (item: ListSchema) => void; + +export interface ValueListsTableProps { + lists: TableProps['items']; + loading: boolean; + onChange: TableProps['onChange']; + onExport: ActionCallback; + onDelete: ActionCallback; + pagination: Exclude; +} + +const buildColumns = ( + onExport: ActionCallback, + onDelete: ActionCallback +): TableProps['columns'] => [ + { + field: 'name', + name: i18n.COLUMN_FILE_NAME, + truncateText: false, + }, + { + field: 'created_at', + name: i18n.COLUMN_UPLOAD_DATE, + truncateText: false, + }, + { + field: 'created_by', + name: i18n.COLUMN_CREATED_BY, + truncateText: false, + }, + { + name: i18n.COLUMN_ACTIONS, + actions: [ + { + name: i18n.ACTION_EXPORT_NAME, + description: i18n.ACTION_EXPORT_DESCRIPTION, + icon: 'exportAction', + type: 'icon', + onClick: onExport, + 'data-test-subj': 'action-export-value-list', + }, + { + name: i18n.ACTION_DELETE_NAME, + description: i18n.ACTION_DELETE_DESCRIPTION, + icon: 'trash', + type: 'icon', + onClick: onDelete, + 'data-test-subj': 'action-delete-value-list', + }, + ], + }, +]; + +export const ValueListsTableComponent: React.FC = ({ + lists, + loading, + onChange, + onExport, + onDelete, + pagination, +}) => { + const columns = buildColumns(onExport, onDelete); + return ( + + +

{i18n.TABLE_TITLE}

+
+ +
+ ); +}; + +ValueListsTableComponent.displayName = 'ValueListsTableComponent'; + +export const ValueListsTable = React.memo(ValueListsTableComponent); + +ValueListsTable.displayName = 'ValueListsTable'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts new file mode 100644 index 0000000000000..54fff1b96846c --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const MODAL_TITLE = i18n.translate('xpack.siem.lists.uploadValueListTitle', { + defaultMessage: 'Upload value lists', +}); + +export const FILE_PICKER_LABEL = i18n.translate('xpack.siem.lists.uploadValueListDescription', { + defaultMessage: 'Upload single value lists to use while writing rules or rule exceptions.', +}); + +export const FILE_PICKER_PROMPT = i18n.translate('xpack.siem.lists.uploadValueListPrompt', { + defaultMessage: 'Select or drag and drop a file', +}); + +export const CLOSE_BUTTON = i18n.translate('xpack.siem.lists.closeValueListsModalTitle', { + defaultMessage: 'Close', +}); + +export const CANCEL_BUTTON = i18n.translate('xpack.siem.lists.cancelValueListsUploadTitle', { + defaultMessage: 'Cancel upload', +}); + +export const UPLOAD_BUTTON = i18n.translate('xpack.siem.lists.valueListsUploadButton', { + defaultMessage: 'Upload list', +}); + +export const UPLOAD_SUCCESS = i18n.translate('xpack.siem.lists.valueListsUploadSuccess', { + defaultMessage: 'Value list uploaded', +}); + +export const UPLOAD_ERROR = i18n.translate('xpack.siem.lists.valueListsUploadError', { + defaultMessage: 'There was an error uploading the value list.', +}); + +export const uploadSuccessMessage = (fileName: string) => + i18n.translate('xpack.siem.lists.valueListsUploadSuccess', { + defaultMessage: "Value list '{fileName}' was uploaded", + values: { fileName }, + }); + +export const COLUMN_FILE_NAME = i18n.translate('xpack.siem.lists.valueListsTable.fileNameColumn', { + defaultMessage: 'Filename', +}); + +export const COLUMN_UPLOAD_DATE = i18n.translate( + 'xpack.siem.lists.valueListsTable.uploadDateColumn', + { + defaultMessage: 'Upload Date', + } +); + +export const COLUMN_CREATED_BY = i18n.translate( + 'xpack.siem.lists.valueListsTable.createdByColumn', + { + defaultMessage: 'Created by', + } +); + +export const COLUMN_ACTIONS = i18n.translate('xpack.siem.lists.valueListsTable.actionsColumn', { + defaultMessage: 'Actions', +}); + +export const ACTION_EXPORT_NAME = i18n.translate( + 'xpack.siem.lists.valueListsTable.exportActionName', + { + defaultMessage: 'Export', + } +); + +export const ACTION_EXPORT_DESCRIPTION = i18n.translate( + 'xpack.siem.lists.valueListsTable.exportActionDescription', + { + defaultMessage: 'Export value list', + } +); + +export const ACTION_DELETE_NAME = i18n.translate( + 'xpack.siem.lists.valueListsTable.deleteActionName', + { + defaultMessage: 'Remove', + } +); + +export const ACTION_DELETE_DESCRIPTION = i18n.translate( + 'xpack.siem.lists.valueListsTable.deleteActionDescription', + { + defaultMessage: 'Remove value list', + } +); + +export const TABLE_TITLE = i18n.translate('xpack.siem.lists.valueListsTable.title', { + defaultMessage: 'Value lists', +}); + +export const LIST_TYPES_RADIO_LABEL = i18n.translate( + 'xpack.siem.lists.valueListsForm.listTypesRadioLabel', + { + defaultMessage: 'Type of value list', + } +); + +export const IP_RADIO = i18n.translate('xpack.siem.lists.valueListsForm.ipRadioLabel', { + defaultMessage: 'IP addresses', +}); + +export const KEYWORDS_RADIO = i18n.translate('xpack.siem.lists.valueListsForm.keywordsRadioLabel', { + defaultMessage: 'Keywords', +}); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx index 7684f710952e6..d9a762c1578bf 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx @@ -24,6 +24,7 @@ import { useUserInfo } from '../../../components/user_info'; import { AllRules } from './all'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; +import { ValueListsModal } from '../../../components/value_lists_management_modal'; import { UpdatePrePackagedRulesCallOut } from '../../../components/rules/pre_packaged_rules/update_callout'; import { getPrePackagedRuleStatus, redirectToDetections, userHasNoPermissions } from './helpers'; import * as i18n from './translations'; @@ -36,6 +37,9 @@ type Func = (refreshPrePackagedRule?: boolean) => void; const RulesPageComponent: React.FC = () => { const history = useHistory(); const [showImportModal, setShowImportModal] = useState(false); + const [isValueListsModalShown, setIsValueListsModalShown] = useState(false); + const showValueListsModal = useCallback(() => setIsValueListsModalShown(true), []); + const hideValueListsModal = useCallback(() => setIsValueListsModalShown(false), []); const refreshRulesData = useRef(null); const { loading, @@ -107,6 +111,7 @@ const RulesPageComponent: React.FC = () => { return ( <> {userHasNoPermissions(canUserCRUD) && } + setShowImportModal(false)} @@ -157,6 +162,15 @@ const RulesPageComponent: React.FC = () => { )} + + + {i18n.UPLOAD_VALUE_LISTS} + + useUiSetting(DEFAULT_DATE_FORMAT); @@ -23,6 +25,9 @@ export const useTimeZone = (): string => { export const useBasePath = (): string => useKibana().services.http.basePath.get(); +export const useToasts = (): StartServices['notifications']['toasts'] => + useKibana().services.notifications.toasts; + interface UserRealm { name: string; type: string; diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index f3a724a755a48..bd087544e7626 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -9,6 +9,10 @@ export { useExceptionList, usePersistExceptionItem, usePersistExceptionList, + useFindLists, + useDeleteList, + useExportList, + useImportList, ExceptionIdentifiers, ExceptionList, Pagination, @@ -22,10 +26,12 @@ export { EntryExists, EntryNested, EntriesArray, + ListSchema, NamespaceType, Operator, OperatorType, OperatorTypeEnum, + Type, entriesNested, entriesExists, entriesList, From cc34d86cf4f983422bb8470a35340fcc1a2a96a2 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Fri, 26 Jun 2020 19:29:51 -0500 Subject: [PATCH 02/27] Update value list components to use newest Lists API * uses useEffect on a task's state instead of promise chaining * handles the fact that API calls can be rejected with strings * uses exportList function instead of hook --- x-pack/plugins/lists/public/index.tsx | 2 +- .../value_lists_management_modal/form.tsx | 36 ++++++++++--------- .../value_lists_management_modal/modal.tsx | 15 ++++---- .../public/lists_plugin_deps.ts | 2 +- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index 1ea24123ccb9a..7bddd119bed40 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -12,7 +12,7 @@ export { useExceptionList } from './exceptions/hooks/use_exception_list'; export { useFindLists } from './lists/hooks/use_find_lists'; export { useImportList } from './lists/hooks/use_import_list'; export { useDeleteList } from './lists/hooks/use_delete_list'; -export { useExportList } from './lists/hooks/use_export_list'; +export { exportList } from './lists/api'; export { ExceptionList, ExceptionIdentifiers, diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx index 8245200e05987..b76aec0b3aae1 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx @@ -47,7 +47,7 @@ const options: ListTypeOptions[] = [ const defaultListType: Type = 'keyword'; export interface ValueListsFormProps { - onError: (error: Error) => void; + onError: (reason: unknown) => void; onSuccess: (response: ListSchema) => void; } @@ -82,28 +82,32 @@ export const ValueListsFormComponent: React.FC = ({ onError [resetForm, onSuccess] ); const handleError = useCallback( - (error: Error) => { - onError(error); + (reason: unknown) => { + onError(reason); }, [onError] ); const handleImport = useCallback(() => { if (!importState.loading && files && files.length) { - try { - importList({ - file: files[0], - listId: undefined, - http, - type, - }) - .then(handleSuccess) - .catch(handleError); - } catch (error) { - handleError(error); - } + importList({ + file: files[0], + listId: undefined, + http, + type, + }); } - }, [importState.loading, files, importList, http, type, handleSuccess, handleError]); + }, [importState.loading, files, importList, http, type]); + + useEffect(() => { + const { error, loading, result } = importState; + + if (!loading && result) { + handleSuccess(result); + } else if (!loading && error) { + handleError(error); + } + }, [handleError, handleSuccess, importState]); useEffect(() => { return handleCancel; diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index 4d159a5bdfd22..2b129879ac2f8 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -16,7 +16,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { ListSchema, useExportList, useFindLists, useDeleteList } from '../../../lists_plugin_deps'; +import { ListSchema, exportList, useFindLists, useDeleteList } from '../../../lists_plugin_deps'; import { useToasts, useKibana } from '../../../common/lib/kibana'; import { GenericDownloader } from '../../../common/components/generic_downloader'; import * as i18n from './translations'; @@ -37,7 +37,6 @@ export const ValueListsModalComponent: React.FC = ({ const { http } = useKibana().services; const { start: findLists, ...lists } = useFindLists(); const { start: deleteList } = useDeleteList(); - const { start: exportList } = useExportList(); const [exportListId, setExportListId] = useState(); const toasts = useToasts(); @@ -54,8 +53,9 @@ export const ValueListsModalComponent: React.FC = ({ ); const handleExport = useCallback( - async ({ ids }: { ids: string[] }) => exportList({ http, listId: ids[0] }), - [http, exportList] + async ({ ids }: { ids: string[] }) => + exportList({ http, listId: ids[0], signal: new AbortController().signal }), + [http] ); const handleExportClick = useCallback(({ id }: { id: string }) => setExportListId(id), []); const handleExportComplete = useCallback(() => setExportListId(undefined), []); @@ -68,9 +68,10 @@ export const ValueListsModalComponent: React.FC = ({ [setPageIndex, setPageSize] ); const handleUploadError = useCallback( - (error: Error) => { - if (error.name !== 'AbortError') { - toasts.addError(error, { title: i18n.UPLOAD_ERROR }); + (error: unknown) => { + if (!String(error).includes('AbortError')) { + const reportedError = error instanceof Error ? error : new Error(String(error)); + toasts.addError(reportedError, { title: i18n.UPLOAD_ERROR }); } }, [toasts] diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index bd087544e7626..3f427d623104c 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -5,13 +5,13 @@ */ export { + exportList, useApi, useExceptionList, usePersistExceptionItem, usePersistExceptionList, useFindLists, useDeleteList, - useExportList, useImportList, ExceptionIdentifiers, ExceptionList, From af6a86ede6f8be9e0259c31b297daf8a1bbcec24 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Fri, 26 Jun 2020 19:33:11 -0500 Subject: [PATCH 03/27] Close modal on outside click --- .../alerts/components/value_lists_management_modal/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index 2b129879ac2f8..147fff76ec191 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -106,7 +106,7 @@ export const ValueListsModalComponent: React.FC = ({ }; return ( - + {i18n.MODAL_TITLE} From bf7fb727975bb953caac529993cce1f27ae13d9f Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 14:14:21 -0500 Subject: [PATCH 04/27] Add hook for using a cursor with paged API calls. For e.g. findLists, we can send along a cursor to optimize our query. On the backend, this cursor is used as part of a search_after query. --- .../public/common/hooks/use_cursor.test.ts | 53 +++++++++++++++++++ .../lists/public/common/hooks/use_cursor.ts | 34 ++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts create mode 100644 x-pack/plugins/lists/public/common/hooks/use_cursor.ts diff --git a/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts b/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts new file mode 100644 index 0000000000000..502571ca34fda --- /dev/null +++ b/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { UseCursorArgs, useCursor } from './use_cursor'; + +describe('useCursor', () => { + it('returns undefined cursor if no values have been set', () => { + const { result } = renderHook((args: UseCursorArgs) => useCursor(args)); + + expect(result.current[0]).toBeUndefined(); + }); + + it('retrieves a cursor for a known set of args', () => { + const { rerender, result } = renderHook((args: UseCursorArgs) => useCursor(args)); + act(() => { + result.current[1]('new_cursor', { pageIndex: 1, pageSize: 1 }); + }); + rerender({ pageIndex: 1, pageSize: 1 }); + + expect(result.current[0]).toEqual('new_cursor'); + }); + + it('returns undefined cursor for an unknown set of args', () => { + const { rerender, result } = renderHook((args: UseCursorArgs) => useCursor(args)); + act(() => { + result.current[1]('new_cursor', { pageIndex: 1, pageSize: 1 }); + }); + rerender({ pageIndex: 1, pageSize: 100 }); + + expect(result.current[0]).toBeUndefined(); + }); + + it('remembers multiple cursors', () => { + const { rerender, result } = renderHook((args: UseCursorArgs) => useCursor(args)); + + act(() => { + result.current[1]('new_cursor', { pageIndex: 1, pageSize: 1 }); + }); + rerender({ pageIndex: 1, pageSize: 1 }); + expect(result.current[0]).toEqual('new_cursor'); + + act(() => { + result.current[1]('another_cursor', { pageIndex: 2, pageSize: 2 }); + }); + rerender({ pageIndex: 2, pageSize: 2 }); + expect(result.current[0]).toEqual('another_cursor'); + }); +}); diff --git a/x-pack/plugins/lists/public/common/hooks/use_cursor.ts b/x-pack/plugins/lists/public/common/hooks/use_cursor.ts new file mode 100644 index 0000000000000..420482859c71d --- /dev/null +++ b/x-pack/plugins/lists/public/common/hooks/use_cursor.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useState } from 'react'; + +export interface UseCursorArgs { + pageIndex: number; + pageSize: number; +} +type Cursor = string | undefined; +type SetCursor = (cursor: Cursor, args: UseCursorArgs) => void; +type UseCursor = (args: UseCursorArgs) => [Cursor, SetCursor]; + +const hash = (args: UseCursorArgs): string => JSON.stringify(args); + +export const useCursor: UseCursor = (args) => { + const [cache, setCache] = useState>({}); + + const setCursor = useCallback( + (cursor, _args) => { + setCache({ + ...cache, + [hash(_args)]: cursor, + }); + }, + [cache] + ); + + const cursor = cache[hash(args)]; + return [cursor, setCursor]; +}; From 38fa27c92d54a390a06a4b708ff3be82e5ab9079 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 17:07:27 -0500 Subject: [PATCH 05/27] Better implementation of useCursor * Does not require args for setCursor as they're already passed to the hook * Finds nearest cursor for the same page size Eventually this logic will also include sortField as part of the hash/lookup, but we do not currently use that on the frontend. --- .../public/common/hooks/use_cursor.test.ts | 84 +++++++++++++++---- .../lists/public/common/hooks/use_cursor.ts | 26 ++++-- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts b/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts index 502571ca34fda..310d4da0b6c04 100644 --- a/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts +++ b/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts @@ -6,48 +6,104 @@ import { act, renderHook } from '@testing-library/react-hooks'; -import { UseCursorArgs, useCursor } from './use_cursor'; +import { UseCursorProps, useCursor } from './use_cursor'; describe('useCursor', () => { it('returns undefined cursor if no values have been set', () => { - const { result } = renderHook((args: UseCursorArgs) => useCursor(args)); + const { result } = renderHook((props: UseCursorProps) => useCursor(props), { + initialProps: { pageIndex: 0, pageSize: 0 }, + }); expect(result.current[0]).toBeUndefined(); }); - it('retrieves a cursor for a known set of args', () => { - const { rerender, result } = renderHook((args: UseCursorArgs) => useCursor(args)); - act(() => { - result.current[1]('new_cursor', { pageIndex: 1, pageSize: 1 }); + it('retrieves a cursor for a known set of props', () => { + const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { + initialProps: { pageIndex: 0, pageSize: 0 }, }); rerender({ pageIndex: 1, pageSize: 1 }); + act(() => { + result.current[1]('new_cursor'); + }); expect(result.current[0]).toEqual('new_cursor'); }); - it('returns undefined cursor for an unknown set of args', () => { - const { rerender, result } = renderHook((args: UseCursorArgs) => useCursor(args)); + it('returns undefined cursor for an unknown set of props', () => { + const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { + initialProps: { pageIndex: 0, pageSize: 0 }, + }); act(() => { - result.current[1]('new_cursor', { pageIndex: 1, pageSize: 1 }); + result.current[1]('new_cursor'); }); - rerender({ pageIndex: 1, pageSize: 100 }); + rerender({ pageIndex: 1, pageSize: 1 }); expect(result.current[0]).toBeUndefined(); }); - it('remembers multiple cursors', () => { - const { rerender, result } = renderHook((args: UseCursorArgs) => useCursor(args)); + it('remembers cursor through rerenders', () => { + const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { + initialProps: { pageIndex: 0, pageSize: 0 }, + }); + rerender({ pageIndex: 1, pageSize: 1 }); act(() => { - result.current[1]('new_cursor', { pageIndex: 1, pageSize: 1 }); + result.current[1]('new_cursor'); }); + expect(result.current[0]).toEqual('new_cursor'); + + rerender({ pageIndex: 0, pageSize: 0 }); + expect(result.current[0]).toBeUndefined(); + rerender({ pageIndex: 1, pageSize: 1 }); expect(result.current[0]).toEqual('new_cursor'); + }); + it('remembers multiple cursors', () => { + const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { + initialProps: { pageIndex: 0, pageSize: 0 }, + }); + + rerender({ pageIndex: 1, pageSize: 1 }); act(() => { - result.current[1]('another_cursor', { pageIndex: 2, pageSize: 2 }); + result.current[1]('new_cursor'); }); + expect(result.current[0]).toEqual('new_cursor'); + rerender({ pageIndex: 2, pageSize: 2 }); + act(() => { + result.current[1]('another_cursor'); + }); expect(result.current[0]).toEqual('another_cursor'); }); + + it('returns the "nearest" cursor for the given page size', () => { + const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { + initialProps: { pageIndex: 0, pageSize: 0 }, + }); + + rerender({ pageIndex: 1, pageSize: 2 }); + act(() => { + result.current[1]('cursor1'); + }); + expect(result.current[0]).toEqual('cursor1'); + + rerender({ pageIndex: 2, pageSize: 2 }); + act(() => { + result.current[1]('cursor2'); + }); + expect(result.current[0]).toEqual('cursor2'); + + rerender({ pageIndex: 3, pageSize: 2 }); + act(() => { + result.current[1]('cursor3'); + }); + expect(result.current[0]).toEqual('cursor3'); + + rerender({ pageIndex: 4, pageSize: 2 }); + expect(result.current[0]).toEqual('cursor3'); + + rerender({ pageIndex: 6, pageSize: 2 }); + expect(result.current[0]).toEqual('cursor3'); + }); }); diff --git a/x-pack/plugins/lists/public/common/hooks/use_cursor.ts b/x-pack/plugins/lists/public/common/hooks/use_cursor.ts index 420482859c71d..9b2c28a338277 100644 --- a/x-pack/plugins/lists/public/common/hooks/use_cursor.ts +++ b/x-pack/plugins/lists/public/common/hooks/use_cursor.ts @@ -6,29 +6,37 @@ import { useCallback, useState } from 'react'; -export interface UseCursorArgs { +export interface UseCursorProps { pageIndex: number; pageSize: number; } type Cursor = string | undefined; -type SetCursor = (cursor: Cursor, args: UseCursorArgs) => void; -type UseCursor = (args: UseCursorArgs) => [Cursor, SetCursor]; +type SetCursor = (cursor: Cursor) => void; +type UseCursor = (props: UseCursorProps) => [Cursor, SetCursor]; -const hash = (args: UseCursorArgs): string => JSON.stringify(args); +const hash = (props: UseCursorProps): string => JSON.stringify(props); -export const useCursor: UseCursor = (args) => { +export const useCursor: UseCursor = (props) => { const [cache, setCache] = useState>({}); const setCursor = useCallback( - (cursor, _args) => { + (cursor) => { setCache({ ...cache, - [hash(_args)]: cursor, + [hash(props)]: cursor, }); }, - [cache] + [cache, props] ); - const cursor = cache[hash(args)]; + let cursor: Cursor; + for (let i = props.pageIndex; i >= 0; i--) { + const currentProps = { pageIndex: i, pageSize: props.pageSize }; + cursor = cache[hash(currentProps)]; + if (cursor) { + break; + } + } + return [cursor, setCursor]; }; From b606f57a179f01dcdd17f5db1f73920fdcadf6ec Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 17:34:54 -0500 Subject: [PATCH 06/27] Fixes useCursor hook functionality We were previously storing the cursor on the _current_ page, when it's only truly valid for the _next_ page (and beyond). This was causing a few issues, but now that it's fixed everything works great. --- .../public/common/hooks/use_cursor.test.ts | 31 ++++++++++++------- .../lists/public/common/hooks/use_cursor.ts | 11 ++++--- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts b/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts index 310d4da0b6c04..b8967086ef956 100644 --- a/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts +++ b/x-pack/plugins/lists/public/common/hooks/use_cursor.test.ts @@ -17,7 +17,7 @@ describe('useCursor', () => { expect(result.current[0]).toBeUndefined(); }); - it('retrieves a cursor for a known set of props', () => { + it('retrieves a cursor for the next page of a given page size', () => { const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { initialProps: { pageIndex: 0, pageSize: 0 }, }); @@ -26,18 +26,21 @@ describe('useCursor', () => { result.current[1]('new_cursor'); }); + expect(result.current[0]).toBeUndefined(); + + rerender({ pageIndex: 2, pageSize: 1 }); expect(result.current[0]).toEqual('new_cursor'); }); - it('returns undefined cursor for an unknown set of props', () => { + it('returns undefined cursor for an unknown search', () => { const { rerender, result } = renderHook((props: UseCursorProps) => useCursor(props), { initialProps: { pageIndex: 0, pageSize: 0 }, }); act(() => { result.current[1]('new_cursor'); }); - rerender({ pageIndex: 1, pageSize: 1 }); + rerender({ pageIndex: 1, pageSize: 2 }); expect(result.current[0]).toBeUndefined(); }); @@ -50,12 +53,14 @@ describe('useCursor', () => { act(() => { result.current[1]('new_cursor'); }); + + rerender({ pageIndex: 2, pageSize: 1 }); expect(result.current[0]).toEqual('new_cursor'); rerender({ pageIndex: 0, pageSize: 0 }); expect(result.current[0]).toBeUndefined(); - rerender({ pageIndex: 1, pageSize: 1 }); + rerender({ pageIndex: 2, pageSize: 1 }); expect(result.current[0]).toEqual('new_cursor'); }); @@ -68,12 +73,15 @@ describe('useCursor', () => { act(() => { result.current[1]('new_cursor'); }); - expect(result.current[0]).toEqual('new_cursor'); - rerender({ pageIndex: 2, pageSize: 2 }); act(() => { result.current[1]('another_cursor'); }); + + rerender({ pageIndex: 2, pageSize: 1 }); + expect(result.current[0]).toEqual('new_cursor'); + + rerender({ pageIndex: 3, pageSize: 2 }); expect(result.current[0]).toEqual('another_cursor'); }); @@ -86,19 +94,20 @@ describe('useCursor', () => { act(() => { result.current[1]('cursor1'); }); - expect(result.current[0]).toEqual('cursor1'); - rerender({ pageIndex: 2, pageSize: 2 }); act(() => { result.current[1]('cursor2'); }); - expect(result.current[0]).toEqual('cursor2'); - rerender({ pageIndex: 3, pageSize: 2 }); act(() => { result.current[1]('cursor3'); }); - expect(result.current[0]).toEqual('cursor3'); + + rerender({ pageIndex: 2, pageSize: 2 }); + expect(result.current[0]).toEqual('cursor1'); + + rerender({ pageIndex: 3, pageSize: 2 }); + expect(result.current[0]).toEqual('cursor2'); rerender({ pageIndex: 4, pageSize: 2 }); expect(result.current[0]).toEqual('cursor3'); diff --git a/x-pack/plugins/lists/public/common/hooks/use_cursor.ts b/x-pack/plugins/lists/public/common/hooks/use_cursor.ts index 9b2c28a338277..2409436ff3137 100644 --- a/x-pack/plugins/lists/public/common/hooks/use_cursor.ts +++ b/x-pack/plugins/lists/public/common/hooks/use_cursor.ts @@ -16,22 +16,23 @@ type UseCursor = (props: UseCursorProps) => [Cursor, SetCursor]; const hash = (props: UseCursorProps): string => JSON.stringify(props); -export const useCursor: UseCursor = (props) => { +export const useCursor: UseCursor = ({ pageIndex, pageSize }) => { const [cache, setCache] = useState>({}); const setCursor = useCallback( (cursor) => { setCache({ ...cache, - [hash(props)]: cursor, + [hash({ pageIndex: pageIndex + 1, pageSize })]: cursor, }); }, - [cache, props] + // eslint-disable-next-line react-hooks/exhaustive-deps + [pageIndex, pageSize] ); let cursor: Cursor; - for (let i = props.pageIndex; i >= 0; i--) { - const currentProps = { pageIndex: i, pageSize: props.pageSize }; + for (let i = pageIndex; i >= 0; i--) { + const currentProps = { pageIndex: i, pageSize }; cursor = cache[hash(currentProps)]; if (cursor) { break; From ce1edb2031df4b4df16393e9081cd6a2ccae5c3d Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 17:50:29 -0500 Subject: [PATCH 07/27] Add cursor to lists query This allows us to search_after a previous page's search, if available. --- x-pack/plugins/lists/public/index.tsx | 1 + x-pack/plugins/lists/public/lists/api.test.ts | 14 ++++++++++++-- x-pack/plugins/lists/public/lists/api.ts | 6 ++++-- .../public/lists/hooks/use_find_lists.test.ts | 2 +- x-pack/plugins/lists/public/lists/types.ts | 1 + .../value_lists_management_modal/modal.tsx | 19 ++++++++++++++++--- .../public/lists_plugin_deps.ts | 1 + 7 files changed, 36 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index 7bddd119bed40..125de209ba612 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -13,6 +13,7 @@ export { useFindLists } from './lists/hooks/use_find_lists'; export { useImportList } from './lists/hooks/use_import_list'; export { useDeleteList } from './lists/hooks/use_delete_list'; export { exportList } from './lists/api'; +export { useCursor } from './common/hooks/use_cursor'; export { ExceptionList, ExceptionIdentifiers, diff --git a/x-pack/plugins/lists/public/lists/api.test.ts b/x-pack/plugins/lists/public/lists/api.test.ts index 38556e2eabc18..948f837b1dcb4 100644 --- a/x-pack/plugins/lists/public/lists/api.test.ts +++ b/x-pack/plugins/lists/public/lists/api.test.ts @@ -88,6 +88,7 @@ describe('Value Lists API', () => { it('GETs from the lists endpoint', async () => { const abortCtrl = new AbortController(); await findLists({ + cursor: undefined, http: httpMock, pageIndex: 1, pageSize: 10, @@ -105,6 +106,7 @@ describe('Value Lists API', () => { it('sends pagination as query parameters', async () => { const abortCtrl = new AbortController(); await findLists({ + cursor: undefined, http: httpMock, pageIndex: 1, pageSize: 10, @@ -121,7 +123,11 @@ describe('Value Lists API', () => { it('rejects with an error if request payload is invalid (and does not make API call)', async () => { const abortCtrl = new AbortController(); - const payload: ApiPayload = { pageIndex: 10, pageSize: 0 }; + const payload: ApiPayload = { + cursor: undefined, + pageIndex: 10, + pageSize: 0, + }; await expect( findLists({ @@ -135,7 +141,11 @@ describe('Value Lists API', () => { it('rejects with an error if response payload is invalid', async () => { const abortCtrl = new AbortController(); - const payload: ApiPayload = { pageIndex: 1, pageSize: 10 }; + const payload: ApiPayload = { + cursor: undefined, + pageIndex: 1, + pageSize: 10, + }; const badResponse = { ...getFoundListSchemaMock(), cursor: undefined }; httpMock.fetch.mockResolvedValue(badResponse); diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index d615239f4eb01..aea99f5a86bc5 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -55,6 +55,7 @@ const findLists = async ({ }; const findListsWithValidation = async ({ + cursor, http, pageIndex, pageSize, @@ -62,8 +63,9 @@ const findListsWithValidation = async ({ }: FindListsParams): Promise => pipe( { - page: String(pageIndex), - per_page: String(pageSize), + cursor: cursor?.toString(), + page: pageIndex?.toString(), + per_page: pageSize?.toString(), }, (payload) => fromEither(validateEither(findListSchema, payload)), chain((payload) => tryCatch(() => findLists({ http, signal, ...payload }), String)), diff --git a/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts index 0d63acbe0bd2c..c8b26f695f557 100644 --- a/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts +++ b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts @@ -25,7 +25,7 @@ describe('useFindLists', () => { it('invokes Api.findLists', async () => { const { result, waitForNextUpdate } = renderHook(() => useFindLists()); act(() => { - result.current.start({ http: httpMock, pageIndex: 1, pageSize: 10 }); + result.current.start({ cursor: undefined, http: httpMock, pageIndex: 1, pageSize: 10 }); }); await waitForNextUpdate(); diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts index 6421ad174d4d9..f6933ef805786 100644 --- a/x-pack/plugins/lists/public/lists/types.ts +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -14,6 +14,7 @@ export interface ApiParams { export type ApiPayload = Omit; export interface FindListsParams extends ApiParams { + cursor: string | undefined; pageSize: number | undefined; pageIndex: number | undefined; } diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index 147fff76ec191..0b878e6d8d296 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -16,7 +16,13 @@ import { EuiSpacer, } from '@elastic/eui'; -import { ListSchema, exportList, useFindLists, useDeleteList } from '../../../lists_plugin_deps'; +import { + ListSchema, + exportList, + useFindLists, + useDeleteList, + useCursor, +} from '../../../lists_plugin_deps'; import { useToasts, useKibana } from '../../../common/lib/kibana'; import { GenericDownloader } from '../../../common/components/generic_downloader'; import * as i18n from './translations'; @@ -34,6 +40,7 @@ export const ValueListsModalComponent: React.FC = ({ }) => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(20); + const [cursor, setCursor] = useCursor({ pageIndex, pageSize }); const { http } = useKibana().services; const { start: findLists, ...lists } = useFindLists(); const { start: deleteList } = useDeleteList(); @@ -41,8 +48,8 @@ export const ValueListsModalComponent: React.FC = ({ const toasts = useToasts(); const fetchLists = useCallback(() => { - findLists({ http, pageIndex: pageIndex + 1, pageSize }); - }, [http, findLists, pageIndex, pageSize]); + findLists({ cursor, http, pageIndex: pageIndex + 1, pageSize }); + }, [cursor, http, findLists, pageIndex, pageSize]); const handleDelete = useCallback( async ({ id }: { id: string }) => { @@ -93,6 +100,12 @@ export const ValueListsModalComponent: React.FC = ({ } }, [showModal, fetchLists]); + useEffect(() => { + if (!lists.loading && lists.result?.cursor) { + setCursor(lists.result.cursor); + } + }, [lists.loading, lists.result, setCursor]); + if (!showModal) { return null; } diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index 3f427d623104c..933c8d05f1827 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -13,6 +13,7 @@ export { useFindLists, useDeleteList, useImportList, + useCursor, ExceptionIdentifiers, ExceptionList, Pagination, From e39116c82aeeb0df4d4992fa54188d0eb46221c0 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 18:20:31 -0500 Subject: [PATCH 08/27] Do not validate response of export This is just a blob, so we have nothing to validate. --- x-pack/plugins/lists/public/lists/api.test.ts | 19 +------------------ x-pack/plugins/lists/public/lists/api.ts | 1 - 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/x-pack/plugins/lists/public/lists/api.test.ts b/x-pack/plugins/lists/public/lists/api.test.ts index 948f837b1dcb4..094cc14295e44 100644 --- a/x-pack/plugins/lists/public/lists/api.test.ts +++ b/x-pack/plugins/lists/public/lists/api.test.ts @@ -270,7 +270,7 @@ describe('Value Lists API', () => { describe('exportList', () => { beforeEach(() => { - httpMock.fetch.mockResolvedValue(getListResponseMock()); + httpMock.fetch.mockResolvedValue({}); }); it('POSTs to the export endpoint', async () => { @@ -320,22 +320,5 @@ describe('Value Lists API', () => { ).rejects.toEqual('Invalid value "23" supplied to "list_id"'); expect(httpMock.fetch).not.toHaveBeenCalled(); }); - - it('rejects with an error if response payload is invalid', async () => { - const abortCtrl = new AbortController(); - const payload: ApiPayload = { - listId: 'list-id', - }; - const badResponse = { ...getListResponseMock(), id: undefined }; - httpMock.fetch.mockResolvedValue(badResponse); - - await expect( - exportList({ - http: httpMock, - ...payload, - signal: abortCtrl.signal, - }) - ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); - }); }); }); diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts index aea99f5a86bc5..0e0ce0a6d20c2 100644 --- a/x-pack/plugins/lists/public/lists/api.ts +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -168,7 +168,6 @@ const exportListWithValidation = async ({ { list_id: listId }, (payload) => fromEither(validateEither(exportListItemQuerySchema, payload)), chain((payload) => tryCatch(() => exportList({ http, signal, ...payload }), String)), - chain((response) => fromEither(validateEither(listSchema, response))), flow(toPromise) ); From df70e5ce919ec96ea96df088720502ca1ce36ea5 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 18:37:56 -0500 Subject: [PATCH 09/27] Fix double callback post-import After uploading a list, the modal was being shown twice. Declaring the constituent state dependencies separately fixed the issue. --- .../components/value_lists_management_modal/form.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx index b76aec0b3aae1..1864675499a07 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx @@ -100,14 +100,12 @@ export const ValueListsFormComponent: React.FC = ({ onError }, [importState.loading, files, importList, http, type]); useEffect(() => { - const { error, loading, result } = importState; - - if (!loading && result) { - handleSuccess(result); - } else if (!loading && error) { - handleError(error); + if (!importState.loading && importState.result) { + handleSuccess(importState.result); + } else if (!importState.loading && importState.error) { + handleError(importState.error); } - }, [handleError, handleSuccess, importState]); + }, [handleError, handleSuccess, importState.error, importState.loading, importState.result]); useEffect(() => { return handleCancel; From 344b81c406d31cc49c5882057c4349f26249c3c0 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 30 Jun 2020 15:22:22 -0500 Subject: [PATCH 10/27] Update ValueListsForm to manually abort import request These hooks no longer care about/expose an abort function. In this one case where we need that functionality, we can do it ourselves relatively simply. --- .../components/value_lists_management_modal/form.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx index 1864675499a07..2cc313b209ffb 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx @@ -52,11 +52,12 @@ export interface ValueListsFormProps { } export const ValueListsFormComponent: React.FC = ({ onError, onSuccess }) => { + const ctrl = useRef(new AbortController()); const [files, setFiles] = useState(null); const [type, setType] = useState(defaultListType); const filePickerRef = useRef(null); const { http } = useKibana().services; - const { start: importList, abort: abortImport, ...importState } = useImportList(); + const { start: importList, ...importState } = useImportList(); // EuiRadioGroup's onChange only infers 'string' from our options const handleRadioChange = useCallback((t: string) => setType(t as Type), [setType]); @@ -71,8 +72,8 @@ export const ValueListsFormComponent: React.FC = ({ onError }, [setType]); const handleCancel = useCallback(() => { - abortImport(); - }, [abortImport]); + ctrl.current.abort(); + }, []); const handleSuccess = useCallback( (response: ListSchema) => { @@ -90,10 +91,12 @@ export const ValueListsFormComponent: React.FC = ({ onError const handleImport = useCallback(() => { if (!importState.loading && files && files.length) { + ctrl.current = new AbortController(); importList({ file: files[0], listId: undefined, http, + signal: ctrl.current.signal, type, }); } From 23fd1ee650a11db3167d946695231d9a22416dab Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 1 Jul 2020 15:54:29 -0500 Subject: [PATCH 11/27] Default modal table to five rows --- .../alerts/components/value_lists_management_modal/modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index 0b878e6d8d296..45d7429b527b7 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -39,7 +39,7 @@ export const ValueListsModalComponent: React.FC = ({ showModal, }) => { const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(20); + const [pageSize, setPageSize] = useState(5); const [cursor, setCursor] = useCursor({ pageIndex, pageSize }); const { http } = useKibana().services; const { start: findLists, ...lists } = useFindLists(); @@ -114,7 +114,7 @@ export const ValueListsModalComponent: React.FC = ({ pageIndex, pageSize, totalItemCount: lists.result?.total ?? 0, - pageSizeOptions: [10, 20, 50], + pageSizeOptions: [5, 10, 20], hidePerPageOptions: false, }; From e3670ea8035ff4229baa65e930dc70c9c96e91f3 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 1 Jul 2020 16:37:09 -0500 Subject: [PATCH 12/27] Update translation keys following plugin rename --- .../translations.ts | 98 ++++++++++++------- .../detection_engine/rules/translations.ts | 2 +- 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts index 54fff1b96846c..14a29bcad6b7b 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts @@ -6,109 +6,133 @@ import { i18n } from '@kbn/i18n'; -export const MODAL_TITLE = i18n.translate('xpack.siem.lists.uploadValueListTitle', { +export const MODAL_TITLE = i18n.translate('xpack.securitySolution.lists.uploadValueListTitle', { defaultMessage: 'Upload value lists', }); -export const FILE_PICKER_LABEL = i18n.translate('xpack.siem.lists.uploadValueListDescription', { - defaultMessage: 'Upload single value lists to use while writing rules or rule exceptions.', -}); +export const FILE_PICKER_LABEL = i18n.translate( + 'xpack.securitySolution.lists.uploadValueListDescription', + { + defaultMessage: 'Upload single value lists to use while writing rules or rule exceptions.', + } +); -export const FILE_PICKER_PROMPT = i18n.translate('xpack.siem.lists.uploadValueListPrompt', { - defaultMessage: 'Select or drag and drop a file', -}); +export const FILE_PICKER_PROMPT = i18n.translate( + 'xpack.securitySolution.lists.uploadValueListPrompt', + { + defaultMessage: 'Select or drag and drop a file', + } +); -export const CLOSE_BUTTON = i18n.translate('xpack.siem.lists.closeValueListsModalTitle', { - defaultMessage: 'Close', -}); +export const CLOSE_BUTTON = i18n.translate( + 'xpack.securitySolution.lists.closeValueListsModalTitle', + { + defaultMessage: 'Close', + } +); -export const CANCEL_BUTTON = i18n.translate('xpack.siem.lists.cancelValueListsUploadTitle', { - defaultMessage: 'Cancel upload', -}); +export const CANCEL_BUTTON = i18n.translate( + 'xpack.securitySolution.lists.cancelValueListsUploadTitle', + { + defaultMessage: 'Cancel upload', + } +); -export const UPLOAD_BUTTON = i18n.translate('xpack.siem.lists.valueListsUploadButton', { +export const UPLOAD_BUTTON = i18n.translate('xpack.securitySolution.lists.valueListsUploadButton', { defaultMessage: 'Upload list', }); -export const UPLOAD_SUCCESS = i18n.translate('xpack.siem.lists.valueListsUploadSuccess', { - defaultMessage: 'Value list uploaded', -}); +export const UPLOAD_SUCCESS = i18n.translate( + 'xpack.securitySolution.lists.valueListsUploadSuccess', + { + defaultMessage: 'Value list uploaded', + } +); -export const UPLOAD_ERROR = i18n.translate('xpack.siem.lists.valueListsUploadError', { +export const UPLOAD_ERROR = i18n.translate('xpack.securitySolution.lists.valueListsUploadError', { defaultMessage: 'There was an error uploading the value list.', }); export const uploadSuccessMessage = (fileName: string) => - i18n.translate('xpack.siem.lists.valueListsUploadSuccess', { + i18n.translate('xpack.securitySolution.lists.valueListsUploadSuccess', { defaultMessage: "Value list '{fileName}' was uploaded", values: { fileName }, }); -export const COLUMN_FILE_NAME = i18n.translate('xpack.siem.lists.valueListsTable.fileNameColumn', { - defaultMessage: 'Filename', -}); +export const COLUMN_FILE_NAME = i18n.translate( + 'xpack.securitySolution.lists.valueListsTable.fileNameColumn', + { + defaultMessage: 'Filename', + } +); export const COLUMN_UPLOAD_DATE = i18n.translate( - 'xpack.siem.lists.valueListsTable.uploadDateColumn', + 'xpack.securitySolution.lists.valueListsTable.uploadDateColumn', { defaultMessage: 'Upload Date', } ); export const COLUMN_CREATED_BY = i18n.translate( - 'xpack.siem.lists.valueListsTable.createdByColumn', + 'xpack.securitySolution.lists.valueListsTable.createdByColumn', { defaultMessage: 'Created by', } ); -export const COLUMN_ACTIONS = i18n.translate('xpack.siem.lists.valueListsTable.actionsColumn', { - defaultMessage: 'Actions', -}); +export const COLUMN_ACTIONS = i18n.translate( + 'xpack.securitySolution.lists.valueListsTable.actionsColumn', + { + defaultMessage: 'Actions', + } +); export const ACTION_EXPORT_NAME = i18n.translate( - 'xpack.siem.lists.valueListsTable.exportActionName', + 'xpack.securitySolution.lists.valueListsTable.exportActionName', { defaultMessage: 'Export', } ); export const ACTION_EXPORT_DESCRIPTION = i18n.translate( - 'xpack.siem.lists.valueListsTable.exportActionDescription', + 'xpack.securitySolution.lists.valueListsTable.exportActionDescription', { defaultMessage: 'Export value list', } ); export const ACTION_DELETE_NAME = i18n.translate( - 'xpack.siem.lists.valueListsTable.deleteActionName', + 'xpack.securitySolution.lists.valueListsTable.deleteActionName', { defaultMessage: 'Remove', } ); export const ACTION_DELETE_DESCRIPTION = i18n.translate( - 'xpack.siem.lists.valueListsTable.deleteActionDescription', + 'xpack.securitySolution.lists.valueListsTable.deleteActionDescription', { defaultMessage: 'Remove value list', } ); -export const TABLE_TITLE = i18n.translate('xpack.siem.lists.valueListsTable.title', { +export const TABLE_TITLE = i18n.translate('xpack.securitySolution.lists.valueListsTable.title', { defaultMessage: 'Value lists', }); export const LIST_TYPES_RADIO_LABEL = i18n.translate( - 'xpack.siem.lists.valueListsForm.listTypesRadioLabel', + 'xpack.securitySolution.lists.valueListsForm.listTypesRadioLabel', { defaultMessage: 'Type of value list', } ); -export const IP_RADIO = i18n.translate('xpack.siem.lists.valueListsForm.ipRadioLabel', { +export const IP_RADIO = i18n.translate('xpack.securitySolution.lists.valueListsForm.ipRadioLabel', { defaultMessage: 'IP addresses', }); -export const KEYWORDS_RADIO = i18n.translate('xpack.siem.lists.valueListsForm.keywordsRadioLabel', { - defaultMessage: 'Keywords', -}); +export const KEYWORDS_RADIO = i18n.translate( + 'xpack.securitySolution.lists.valueListsForm.keywordsRadioLabel', + { + defaultMessage: 'Keywords', + } +); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts index 7a30738eb53b6..e62e54d079c17 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/translations.ts @@ -21,7 +21,7 @@ export const IMPORT_RULE = i18n.translate( ); export const UPLOAD_VALUE_LISTS = i18n.translate( - 'xpack.siem.detectionEngine.rules.uploadValueListsButton', + 'xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton', { defaultMessage: 'Upload value lists', } From c36e95976fa0dfe70dc2b09ae6dec3d9bae0034b Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 1 Jul 2020 16:39:22 -0500 Subject: [PATCH 13/27] Try to fit table contents on a single row Dates were wrapping (and raw), and so were wrapped in a FormattedDate component. However, since this component didn't wrap, we needed to shrink/truncate the uploaded_by field as well as allow the fileName to truncate. --- .../value_lists_management_modal/modal.tsx | 2 +- .../value_lists_management_modal/table.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index 45d7429b527b7..38dea5e7cd4c7 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -120,7 +120,7 @@ export const ValueListsModalComponent: React.FC = ({ return ( - + {i18n.MODAL_TITLE} diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx index b01c35ed8e29b..2532d7c7154dc 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiBasicTable, EuiBasicTableProps, EuiText, EuiPanel } from '@elastic/eui'; import { ListSchema } from '../../../../../lists/common/schemas/response'; +import { FormattedDate } from '../../../common/components/formatted_date'; import * as i18n from './translations'; type TableProps = EuiBasicTableProps; @@ -29,17 +30,22 @@ const buildColumns = ( { field: 'name', name: i18n.COLUMN_FILE_NAME, - truncateText: false, + truncateText: true, }, { field: 'created_at', name: i18n.COLUMN_UPLOAD_DATE, - truncateText: false, + /* eslint-disable-next-line react/display-name */ + render: (value: ListSchema['created_at']) => ( + + ), + width: '30%', }, { field: 'created_by', name: i18n.COLUMN_CREATED_BY, - truncateText: false, + truncateText: true, + width: '15%', }, { name: i18n.COLUMN_ACTIONS, From 312fbde20bf3ed5566fce34f689164457d3b7ff6 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 12:13:45 -0500 Subject: [PATCH 14/27] Add helper function to prevent tests from logging errors https://github.com/enzymejs/enzyme/issues/2073 seems to be an ongoing issue, and causes components with useEffect to update after the test is completed. waitForUpdates ensures that updates have completed within an act() before continuing on. --- .../public/common/utils/test_utils.ts | 16 ++++++++++++++++ .../public/overview/pages/overview.test.tsx | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/common/utils/test_utils.ts diff --git a/x-pack/plugins/security_solution/public/common/utils/test_utils.ts b/x-pack/plugins/security_solution/public/common/utils/test_utils.ts new file mode 100644 index 0000000000000..5a3cddb74657d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/test_utils.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +// Temporary fix for https://github.com/enzymejs/enzyme/issues/2073 +export const waitForUpdates = async

(wrapper: ReactWrapper

) => { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + wrapper.update(); + }); +}; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index d6e8fb984ac0f..452e6d16451cc 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import '../../common/mock/match_media'; +import { waitForUpdates } from '../../common/utils/test_utils'; import { TestProviders } from '../../common/mock'; import { useWithSource } from '../../common/containers/source'; import { Overview } from './index'; @@ -31,7 +32,6 @@ describe('Overview', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: false, }); - const wrapper = mount( @@ -39,6 +39,7 @@ describe('Overview', () => { ); + await waitForUpdates(wrapper); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); @@ -55,6 +56,8 @@ describe('Overview', () => { ); + await waitForUpdates(wrapper); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); }); From 9587b7e201fcb0542a6fa66e85af5530e1e7df44 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 13:42:29 -0500 Subject: [PATCH 15/27] Add jest tests for our form, table, and modal components --- .../form.test.tsx | 109 +++++++++++++++++ .../value_lists_management_modal/form.tsx | 2 + .../modal.test.tsx | 63 ++++++++++ .../value_lists_management_modal/modal.tsx | 4 +- .../table.test.tsx | 113 ++++++++++++++++++ 5 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.test.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.test.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.test.tsx diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.test.tsx new file mode 100644 index 0000000000000..54fbe3d35ae31 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FormEvent } from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { waitForUpdates } from '../../../common/utils/test_utils'; +import { TestProviders } from '../../../common/mock'; +import { ValueListsForm } from './form'; +import { useImportList } from '../../../lists_plugin_deps'; + +jest.mock('../../../lists_plugin_deps'); +const mockUseImportList = useImportList as jest.Mock; + +const mockFile = ({ + name: 'foo.csv', + path: '/home/foo.csv', +} as unknown) as File; + +const mockSelectFile:

(container: ReactWrapper

, file: File) => Promise = async ( + container, + file +) => { + const fileChange = container.find('EuiFilePicker').prop('onChange'); + act(() => { + if (fileChange) { + fileChange(([file] as unknown) as FormEvent); + } + }); + await waitForUpdates(container); + expect( + container.find('button[data-test-subj="value-lists-form-import-action"]').prop('disabled') + ).not.toEqual(true); +}; + +describe('ValueListsForm', () => { + let mockImportList: jest.Mock; + + beforeEach(() => { + mockImportList = jest.fn(); + mockUseImportList.mockImplementation(() => ({ + start: mockImportList, + })); + }); + + it('disables upload button when file is absent', () => { + const container = mount( + + + + ); + + expect( + container.find('button[data-test-subj="value-lists-form-import-action"]').prop('disabled') + ).toEqual(true); + }); + + it('calls importList when upload is clicked', async () => { + const container = mount( + + + + ); + + await mockSelectFile(container, mockFile); + + container.find('button[data-test-subj="value-lists-form-import-action"]').simulate('click'); + await waitForUpdates(container); + + expect(mockImportList).toHaveBeenCalledWith(expect.objectContaining({ file: mockFile })); + }); + + it('calls onError if import fails', async () => { + mockUseImportList.mockImplementation(() => ({ + start: jest.fn(), + error: 'whoops', + })); + + const onError = jest.fn(); + const container = mount( + + + + ); + await waitForUpdates(container); + + expect(onError).toHaveBeenCalledWith('whoops'); + }); + + it('calls onSuccess if import succeeds', async () => { + mockUseImportList.mockImplementation(() => ({ + start: jest.fn(), + result: { mockResult: true }, + })); + + const onSuccess = jest.fn(); + const container = mount( + + + + ); + await waitForUpdates(container); + + expect(onSuccess).toHaveBeenCalledWith({ mockResult: true }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx index 2cc313b209ffb..40474ee923dbd 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/form.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import React, { useCallback, useState, ReactNode, useEffect, useRef } from 'react'; import styled from 'styled-components'; import { @@ -148,6 +149,7 @@ export const ValueListsFormComponent: React.FC = ({ onError diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.test.tsx new file mode 100644 index 0000000000000..daf1cbd68df91 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../../common/mock'; +import { ValueListsModal } from './modal'; +import { waitForUpdates } from '../../../common/utils/test_utils'; + +describe('ValueListsModal', () => { + it('renders nothing if showModal is false', () => { + const container = mount( + + + + ); + + expect(container.find('EuiModal')).toHaveLength(0); + }); + + it('renders modal if showModal is true', async () => { + const container = mount( + + + + ); + await waitForUpdates(container); + + expect(container.find('EuiModal')).toHaveLength(1); + }); + + it('calls onClose when modal is closed', async () => { + const onClose = jest.fn(); + const container = mount( + + + + ); + + container.find('button[data-test-subj="value-lists-modal-close-action"]').simulate('click'); + + await waitForUpdates(container); + + expect(onClose).toHaveBeenCalled(); + }); + + it('renders ValueListsForm and ValueListsTable', async () => { + const container = mount( + + + + ); + + await waitForUpdates(container); + + expect(container.find('ValueListsForm')).toHaveLength(1); + expect(container.find('ValueListsTable')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index 38dea5e7cd4c7..a9e0046817148 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -137,7 +137,9 @@ export const ValueListsModalComponent: React.FC = ({ /> - {i18n.CLOSE_BUTTON} + + {i18n.CLOSE_BUTTON} + { + it('renders a row for each list', () => { + const lists = Array(3).fill(getListResponseMock()); + const container = mount( + + + + ); + + expect(container.find('tbody tr')).toHaveLength(3); + }); + + it('calls onChange when pagination is modified', () => { + const lists = Array(3).fill(getListResponseMock()); + const onChange = jest.fn(); + const container = mount( + + + + ); + + act(() => { + container.find('button[data-test-subj="pagination-button-next"]').simulate('click'); + }); + + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ page: expect.objectContaining({ index: 1 }) }) + ); + }); + + it('calls onExport when export is clicked', () => { + const lists = Array(3).fill(getListResponseMock()); + const onExport = jest.fn(); + const container = mount( + + + + ); + + act(() => { + container + .find('tbody tr') + .first() + .find('button[data-test-subj="action-export-value-list"]') + .simulate('click'); + }); + + expect(onExport).toHaveBeenCalledWith(expect.objectContaining({ id: 'some-list-id' })); + }); + + it('calls onDelete when delete is clicked', () => { + const lists = Array(3).fill(getListResponseMock()); + const onDelete = jest.fn(); + const container = mount( + + + + ); + + act(() => { + container + .find('tbody tr') + .first() + .find('button[data-test-subj="action-delete-value-list"]') + .simulate('click'); + }); + + expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ id: 'some-list-id' })); + }); +}); From ab664831be257400752f427e7f5820b230d87dc2 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 13:46:21 -0500 Subject: [PATCH 16/27] Fix translation conflict --- .../alerts/components/value_lists_management_modal/modal.tsx | 2 +- .../components/value_lists_management_modal/translations.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index a9e0046817148..f4678da9bf54d 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -87,7 +87,7 @@ export const ValueListsModalComponent: React.FC = ({ (response: ListSchema) => { toasts.addSuccess({ text: i18n.uploadSuccessMessage(response.name), - title: i18n.UPLOAD_SUCCESS, + title: i18n.UPLOAD_SUCCESS_TITLE, }); fetchLists(); }, diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts index 14a29bcad6b7b..dca6e43a98143 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/translations.ts @@ -42,8 +42,8 @@ export const UPLOAD_BUTTON = i18n.translate('xpack.securitySolution.lists.valueL defaultMessage: 'Upload list', }); -export const UPLOAD_SUCCESS = i18n.translate( - 'xpack.securitySolution.lists.valueListsUploadSuccess', +export const UPLOAD_SUCCESS_TITLE = i18n.translate( + 'xpack.securitySolution.lists.valueListsUploadSuccessTitle', { defaultMessage: 'Value list uploaded', } From f16cf4e1d3250f66fa8d3883daab8c14353a251f Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 13:54:14 -0500 Subject: [PATCH 17/27] Add more waitForUpdates to new overview page tests Each of these logs a console.error without them. --- .../public/overview/pages/overview.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 1f1bff1b8eee5..8f1703e2397e1 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -105,6 +105,8 @@ describe('Overview', () => { ); + await waitForUpdates(wrapper); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(true); }); @@ -129,6 +131,8 @@ describe('Overview', () => { ); + await waitForUpdates(wrapper); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); @@ -148,6 +152,8 @@ describe('Overview', () => { ); + await waitForUpdates(wrapper); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); @@ -167,6 +173,8 @@ describe('Overview', () => { ); + await waitForUpdates(wrapper); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); }); From 5be0e87ee363f82aa9439645c45881929791e48f Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 14:01:48 -0500 Subject: [PATCH 18/27] Fix bad merge resolution That resulted in duplicate exports. --- x-pack/plugins/security_solution/public/lists_plugin_deps.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index d077a365bf84a..811096fed196a 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -30,7 +30,6 @@ export { EntryNested, EntryList, EntriesArray, - ListSchema, NamespaceType, Operator, OperatorEnum, From 0e4fee76f990792a1db4f2ca12039745e55c52aa Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 16:14:16 -0500 Subject: [PATCH 19/27] Make cursor an optional parameter to findLists This param is an optimization and not required for basic functionality. --- x-pack/plugins/lists/public/lists/api.test.ts | 11 ++++++----- .../lists/public/lists/hooks/use_find_lists.test.ts | 2 +- x-pack/plugins/lists/public/lists/types.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/lists/public/lists/api.test.ts b/x-pack/plugins/lists/public/lists/api.test.ts index 094cc14295e44..2ddda032adb77 100644 --- a/x-pack/plugins/lists/public/lists/api.test.ts +++ b/x-pack/plugins/lists/public/lists/api.test.ts @@ -88,7 +88,6 @@ describe('Value Lists API', () => { it('GETs from the lists endpoint', async () => { const abortCtrl = new AbortController(); await findLists({ - cursor: undefined, http: httpMock, pageIndex: 1, pageSize: 10, @@ -106,7 +105,7 @@ describe('Value Lists API', () => { it('sends pagination as query parameters', async () => { const abortCtrl = new AbortController(); await findLists({ - cursor: undefined, + cursor: 'cursor', http: httpMock, pageIndex: 1, pageSize: 10, @@ -116,7 +115,11 @@ describe('Value Lists API', () => { expect(httpMock.fetch).toHaveBeenCalledWith( '/api/lists/_find', expect.objectContaining({ - query: { page: 1, per_page: 10 }, + query: { + cursor: 'cursor', + page: 1, + per_page: 10, + }, }) ); }); @@ -124,7 +127,6 @@ describe('Value Lists API', () => { it('rejects with an error if request payload is invalid (and does not make API call)', async () => { const abortCtrl = new AbortController(); const payload: ApiPayload = { - cursor: undefined, pageIndex: 10, pageSize: 0, }; @@ -142,7 +144,6 @@ describe('Value Lists API', () => { it('rejects with an error if response payload is invalid', async () => { const abortCtrl = new AbortController(); const payload: ApiPayload = { - cursor: undefined, pageIndex: 1, pageSize: 10, }; diff --git a/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts index c8b26f695f557..0d63acbe0bd2c 100644 --- a/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts +++ b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts @@ -25,7 +25,7 @@ describe('useFindLists', () => { it('invokes Api.findLists', async () => { const { result, waitForNextUpdate } = renderHook(() => useFindLists()); act(() => { - result.current.start({ cursor: undefined, http: httpMock, pageIndex: 1, pageSize: 10 }); + result.current.start({ http: httpMock, pageIndex: 1, pageSize: 10 }); }); await waitForNextUpdate(); diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts index f6933ef805786..95a21820536e4 100644 --- a/x-pack/plugins/lists/public/lists/types.ts +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -14,7 +14,7 @@ export interface ApiParams { export type ApiPayload = Omit; export interface FindListsParams extends ApiParams { - cursor: string | undefined; + cursor?: string | undefined; pageSize: number | undefined; pageIndex: number | undefined; } From 728b2729a6350b228d6233dad91340e2c6483002 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 17:12:55 -0500 Subject: [PATCH 20/27] Tweaking Table column sizes Makes actions column smaller, leaving more room for everything else. --- .../alerts/components/value_lists_management_modal/table.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx index 2532d7c7154dc..07d52603a6fd1 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/table.tsx @@ -45,7 +45,7 @@ const buildColumns = ( field: 'created_by', name: i18n.COLUMN_CREATED_BY, truncateText: true, - width: '15%', + width: '20%', }, { name: i18n.COLUMN_ACTIONS, @@ -67,6 +67,7 @@ const buildColumns = ( 'data-test-subj': 'action-delete-value-list', }, ], + width: '15%', }, ]; From 0abbfc422a6356db70cacea2834f08d2cf214420 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 2 Jul 2020 22:15:38 -0500 Subject: [PATCH 21/27] Fix bug where onSuccess is called upon pagination change Because fetchLists changes when pagination does, and handleUploadSuccess changes with fetchLists, our useEffect in Form was being fired on every pagination change due to its onSuccess changing. The solution in this instance is to remove fetchLists from handleUploadSuccess's dependencies, as we merely want to invoke fetchLists from it, not change our reference. --- .../alerts/components/value_lists_management_modal/modal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx index f4678da9bf54d..eea13fae7a273 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/value_lists_management_modal/modal.tsx @@ -91,7 +91,8 @@ export const ValueListsModalComponent: React.FC = ({ }); fetchLists(); }, - [fetchLists, toasts] + // eslint-disable-next-line react-hooks/exhaustive-deps + [toasts] ); useEffect(() => { From 8fb7e4cd6c29cbac37317a47bbfcab8e8b726b79 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 13 Jul 2020 18:24:04 -0500 Subject: [PATCH 22/27] Fix failing test It looks like this broke because EuiTable's pagination changed from a button to an anchor tag. --- .../components/value_lists_management_modal/table.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.test.tsx index 114050423d003..d0ed41ea58588 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/table.test.tsx @@ -33,7 +33,7 @@ describe('ValueListsTable', () => { }); it('calls onChange when pagination is modified', () => { - const lists = Array(3).fill(getListResponseMock()); + const lists = Array(6).fill(getListResponseMock()); const onChange = jest.fn(); const container = mount( @@ -49,7 +49,7 @@ describe('ValueListsTable', () => { ); act(() => { - container.find('button[data-test-subj="pagination-button-next"]').simulate('click'); + container.find('a[data-test-subj="pagination-button-next"]').simulate('click'); }); expect(onChange).toHaveBeenCalledWith( From e4966143651f54fb7d33851afdadb866186db97b Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 13 Jul 2020 18:33:19 -0500 Subject: [PATCH 23/27] Hide page size options on ValueLists modal table These have style issues, and anything above 5 rows causes the modal to scroll, so we're going to disable it for now. --- .../components/value_lists_management_modal/modal.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index eea13fae7a273..bf946a66b253f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -115,8 +115,7 @@ export const ValueListsModalComponent: React.FC = ({ pageIndex, pageSize, totalItemCount: lists.result?.total ?? 0, - pageSizeOptions: [5, 10, 20], - hidePerPageOptions: false, + hidePerPageOptions: true, }; return ( From 3f12007872b82164fd8bb000456c6b3454ab662c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 13 Jul 2020 18:56:16 -0500 Subject: [PATCH 24/27] Update error callbacks now that we have Errors We don't display the nice errors in the case of an ApiError right now, but this is better than it was. --- .../components/value_lists_management_modal/form.tsx | 6 +++--- .../components/value_lists_management_modal/modal.tsx | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 40474ee923dbd..54c3f1e4a33cc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -48,7 +48,7 @@ const options: ListTypeOptions[] = [ const defaultListType: Type = 'keyword'; export interface ValueListsFormProps { - onError: (reason: unknown) => void; + onError: (error: Error) => void; onSuccess: (response: ListSchema) => void; } @@ -84,8 +84,8 @@ export const ValueListsFormComponent: React.FC = ({ onError [resetForm, onSuccess] ); const handleError = useCallback( - (reason: unknown) => { - onError(reason); + (error: Error) => { + onError(error); }, [onError] ); diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index bf946a66b253f..3e8fdb5a4e389 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -75,10 +75,9 @@ export const ValueListsModalComponent: React.FC = ({ [setPageIndex, setPageSize] ); const handleUploadError = useCallback( - (error: unknown) => { - if (!String(error).includes('AbortError')) { - const reportedError = error instanceof Error ? error : new Error(String(error)); - toasts.addError(reportedError, { title: i18n.UPLOAD_ERROR }); + (error: Error) => { + if (error.name !== 'AbortError') { + toasts.addError(error, { title: i18n.UPLOAD_ERROR }); } }, [toasts] From bc984c4d93f7c0a60eb68c4a6d2e14024ffaac06 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 13 Jul 2020 18:57:17 -0500 Subject: [PATCH 25/27] Synchronize delete with the subsequent fetch Our start() no longer resolves in a meaningful way, so we instead need to perform the refetch in an effect watching the result of our delete. --- .../value_lists_management_modal/modal.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index 3e8fdb5a4e389..067b5a5c0b160 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -43,7 +43,7 @@ export const ValueListsModalComponent: React.FC = ({ const [cursor, setCursor] = useCursor({ pageIndex, pageSize }); const { http } = useKibana().services; const { start: findLists, ...lists } = useFindLists(); - const { start: deleteList } = useDeleteList(); + const { start: deleteList, result: deleteResult } = useDeleteList(); const [exportListId, setExportListId] = useState(); const toasts = useToasts(); @@ -52,13 +52,18 @@ export const ValueListsModalComponent: React.FC = ({ }, [cursor, http, findLists, pageIndex, pageSize]); const handleDelete = useCallback( - async ({ id }: { id: string }) => { - await deleteList({ http, id }); - fetchLists(); + ({ id }: { id: string }) => { + deleteList({ http, id }); }, - [deleteList, http, fetchLists] + [deleteList, http] ); + useEffect(() => { + if (deleteResult != null) { + fetchLists(); + } + }, [deleteResult, fetchLists]); + const handleExport = useCallback( async ({ ids }: { ids: string[] }) => exportList({ http, listId: ids[0], signal: new AbortController().signal }), From 66d82415070fb1ef5c762aee0cda20cd92e22e6e Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 13 Jul 2020 19:05:58 -0500 Subject: [PATCH 26/27] Cast our unknown error to an Error useAsync generally does not know how what its tasks are going to be rejected with, hence the unknown. For these API calls we know that it will be an Error, but I don't currently have a way to type that generally. For now, we'll cast it where we use it. --- .../detections/components/value_lists_management_modal/form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 54c3f1e4a33cc..45403e518b192 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -107,7 +107,7 @@ export const ValueListsFormComponent: React.FC = ({ onError if (!importState.loading && importState.result) { handleSuccess(importState.result); } else if (!importState.loading && importState.error) { - handleError(importState.error); + handleError(importState.error as Error); } }, [handleError, handleSuccess, importState.error, importState.loading, importState.result]); From 59e3b0202c564037a2259b718591705db1bace7c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 13 Jul 2020 19:08:31 -0500 Subject: [PATCH 27/27] Import lists code from our new, standardized modules --- .../components/value_lists_management_modal/form.test.tsx | 4 ++-- .../components/value_lists_management_modal/form.tsx | 2 +- .../components/value_lists_management_modal/modal.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx index 54fbe3d35ae31..ce5d19259e9ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.test.tsx @@ -10,9 +10,9 @@ import { act } from 'react-dom/test-utils'; import { waitForUpdates } from '../../../common/utils/test_utils'; import { TestProviders } from '../../../common/mock'; import { ValueListsForm } from './form'; -import { useImportList } from '../../../lists_plugin_deps'; +import { useImportList } from '../../../shared_imports'; -jest.mock('../../../lists_plugin_deps'); +jest.mock('../../../shared_imports'); const mockUseImportList = useImportList as jest.Mock; const mockFile = ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx index 45403e518b192..b8416c3242e4a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/form.tsx @@ -17,7 +17,7 @@ import { EuiRadioGroup, } from '@elastic/eui'; -import { useImportList, ListSchema, Type } from '../../../lists_plugin_deps'; +import { useImportList, ListSchema, Type } from '../../../shared_imports'; import * as i18n from './translations'; import { useKibana } from '../../../common/lib/kibana'; diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx index 067b5a5c0b160..0a935a9cdb1c4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx @@ -22,7 +22,7 @@ import { useFindLists, useDeleteList, useCursor, -} from '../../../lists_plugin_deps'; +} from '../../../shared_imports'; import { useToasts, useKibana } from '../../../common/lib/kibana'; import { GenericDownloader } from '../../../common/components/generic_downloader'; import * as i18n from './translations';