From e4f900fa39143873412113932ffcdeb9c2390dae Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Thu, 4 Jun 2020 11:29:04 -0600 Subject: [PATCH 1/8] modal wip --- .../cases/components/all_cases/columns.tsx | 274 +++---- .../cases/components/all_cases/index.tsx | 682 +++++++++--------- .../components/all_cases_modal/index.tsx | 56 ++ .../all_cases_modal/translations.ts | 10 + .../public/common/utils/route/spy_routes.tsx | 37 +- .../components/flyout/header/index.tsx | 11 +- .../flyout/header_with_close_button/index.tsx | 2 +- .../insert_timeline_popover/index.tsx | 19 +- .../timeline/properties/helpers.tsx | 30 + .../components/timeline/properties/index.tsx | 39 + .../timeline/properties/properties_right.tsx | 69 +- .../timeline/properties/translations.ts | 7 + 12 files changed, 721 insertions(+), 515 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx index ddd860a8720c57..162966a2df28a6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx @@ -5,12 +5,13 @@ */ import React, { useCallback } from 'react'; import { - EuiBadge, - EuiTableFieldDataColumnType, - EuiTableComputedColumnType, - EuiTableActionsColumnType, EuiAvatar, + EuiBadge, EuiLink, + EuiTableActionsColumnType, + EuiTableComputedColumnType, + EuiTableFieldDataColumnType, + HorizontalAlignment, } from '@elastic/eui'; import styled from 'styled-components'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; @@ -39,142 +40,151 @@ const renderStringField = (field: string, dataTestSubj: string) => export const getCasesColumns = ( actions: Array>, - filterStatus: string -): CasesColumns[] => [ - { - name: i18n.NAME, - render: (theCase: Case) => { - if (theCase.id != null && theCase.title != null) { - const caseDetailsLinkComponent = ( - - {theCase.title} - - ); - return theCase.status === 'open' ? ( - caseDetailsLinkComponent - ) : ( - <> - + filterStatus: string, + isModal: boolean +): CasesColumns[] => { + const columns = [ + { + name: i18n.NAME, + render: (theCase: Case) => { + if (theCase.id != null && theCase.title != null) { + const caseDetailsLinkComponent = !isModal ? ( + + {theCase.title} + + ) : ( + {theCase.title} + ); + return theCase.status === 'open' ? ( + caseDetailsLinkComponent + ) : ( + <> {caseDetailsLinkComponent} - {i18n.CLOSED} - - - ); - } - return getEmptyTagValue(); + + {i18n.CLOSED} + + + ); + } + return getEmptyTagValue(); + }, }, - }, - { - field: 'createdBy', - name: i18n.REPORTER, - render: (createdBy: Case['createdBy']) => { - if (createdBy != null) { - return ( - <> - - - {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} - - - ); - } - return getEmptyTagValue(); + { + field: 'createdBy', + name: i18n.REPORTER, + render: (createdBy: Case['createdBy']) => { + if (createdBy != null) { + return ( + <> + + + {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} + + + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'tags', + name: i18n.TAGS, + render: (tags: Case['tags']) => { + if (tags != null && tags.length > 0) { + return ( + + {tags.map((tag: string, i: number) => ( + + {tag} + + ))} + + ); + } + return getEmptyTagValue(); + }, + truncateText: true, }, - }, - { - field: 'tags', - name: i18n.TAGS, - render: (tags: Case['tags']) => { - if (tags != null && tags.length > 0) { - return ( - - {tags.map((tag: string, i: number) => ( - - {tag} - - ))} - - ); - } - return getEmptyTagValue(); + { + align: 'right' as HorizontalAlignment, + field: 'totalComment', + name: i18n.COMMENTS, + sortable: true, + render: (totalComment: Case['totalComment']) => + totalComment != null + ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) + : getEmptyTagValue(), }, - truncateText: true, - }, - { - align: 'right', - field: 'totalComment', - name: i18n.COMMENTS, - sortable: true, - render: (totalComment: Case['totalComment']) => - totalComment != null - ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) - : getEmptyTagValue(), - }, - filterStatus === 'open' - ? { - field: 'createdAt', - name: i18n.OPENED_ON, - sortable: true, - render: (createdAt: Case['createdAt']) => { - if (createdAt != null) { - return ( - - - - ); - } - return getEmptyTagValue(); - }, - } - : { - field: 'closedAt', - name: i18n.CLOSED_ON, - sortable: true, - render: (closedAt: Case['closedAt']) => { - if (closedAt != null) { - return ( - - - - ); - } - return getEmptyTagValue(); + filterStatus === 'open' + ? { + field: 'createdAt', + name: i18n.OPENED_ON, + sortable: true, + render: (createdAt: Case['createdAt']) => { + if (createdAt != null) { + return ( + + + + ); + } + return getEmptyTagValue(); + }, + } + : { + field: 'closedAt', + name: i18n.CLOSED_ON, + sortable: true, + render: (closedAt: Case['closedAt']) => { + if (closedAt != null) { + return ( + + + + ); + } + return getEmptyTagValue(); + }, }, + { + name: i18n.EXTERNAL_INCIDENT, + render: (theCase: Case) => { + if (theCase.id != null) { + return ; + } + return getEmptyTagValue(); }, - { - name: i18n.EXTERNAL_INCIDENT, - render: (theCase: Case) => { - if (theCase.id != null) { - return ; - } - return getEmptyTagValue(); }, - }, - { - name: i18n.INCIDENT_MANAGEMENT_SYSTEM, - render: (theCase: Case) => { - if (theCase.externalService != null) { - return renderStringField( - `${theCase.externalService.connectorName}`, - `case-table-column-connector` - ); - } - return getEmptyTagValue(); + { + name: i18n.INCIDENT_MANAGEMENT_SYSTEM, + render: (theCase: Case) => { + if (theCase.externalService != null) { + return renderStringField( + `${theCase.externalService.connectorName}`, + `case-table-column-connector` + ); + } + return getEmptyTagValue(); + }, + }, + { + name: i18n.ACTIONS, + actions, }, - }, - { - name: i18n.ACTIONS, - actions, - }, -]; + ]; + if (isModal) { + columns.pop(); // remove actions if in modal + } + return columns; +}; interface Props { theCase: Case; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 12f0d02eb10f69..6f7f87d35e670f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -72,7 +72,6 @@ const ProgressLoader = styled(EuiProgress)` z-index: ${theme.eui.euiZHeader}; `} `; - const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; @@ -83,357 +82,374 @@ const getSortField = (field: string): SortFieldCase => { }; interface AllCasesProps { + onRowClick?: (id: string) => void; + isModal?: boolean; userCanCrud: boolean; } -export const AllCases = React.memo(({ userCanCrud }) => { - const urlSearch = useGetUrlSearch(navTabs.case); - const { actionLicense } = useGetActionLicense(); - const { - countClosedCases, - countOpenCases, - isLoading: isCasesStatusLoading, - fetchCasesStatus, - } = useGetCasesStatus(); - const { - data, - dispatchUpdateCaseProperty, - filterOptions, - loading, - queryParams, - selectedCases, - refetchCases, - setFilters, - setQueryParams, - setSelectedCases, - } = useGetCases(); - - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isLoading: isDeleting, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); +export const AllCases = React.memo( + ({ onRowClick = () => {}, isModal = false, userCanCrud }) => { + const urlSearch = useGetUrlSearch(navTabs.case); + const { actionLicense } = useGetActionLicense(); + const { + countClosedCases, + countOpenCases, + isLoading: isCasesStatusLoading, + fetchCasesStatus, + } = useGetCasesStatus(); + const { + data, + dispatchUpdateCaseProperty, + filterOptions, + loading, + queryParams, + selectedCases, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + } = useGetCases(); - // Update case - const { - dispatchResetIsUpdated, - isLoading: isUpdating, - isUpdated, - updateBulkStatus, - } = useUpdateCases(); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState([]); - const filterRefetch = useRef<() => void>(); - const setFilterRefetch = useCallback( - (refetchFilter: () => void) => { - filterRefetch.current = refetchFilter; - }, - [filterRefetch.current] - ); - const refreshCases = useCallback( - (dataRefresh = true) => { - if (dataRefresh) refetchCases(); - fetchCasesStatus(); - setSelectedCases([]); - setDeleteBulk([]); - if (filterRefetch.current != null) { - filterRefetch.current(); - } - }, - [filterOptions, queryParams, filterRefetch.current] - ); + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); - useEffect(() => { - if (isDeleted) { - refreshCases(); - dispatchResetIsDeleted(); - } - if (isUpdated) { - refreshCases(); - dispatchResetIsUpdated(); - } - }, [isDeleted, isUpdated]); - const confirmDeleteModal = useMemo( - () => ( - 0} - onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind( - null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] - )} - /> - ), - [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] - ); + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); + const filterRefetch = useRef<() => void>(); + const setFilterRefetch = useCallback( + (refetchFilter: () => void) => { + filterRefetch.current = refetchFilter; + }, + [filterRefetch.current] + ); + const refreshCases = useCallback( + (dataRefresh = true) => { + if (dataRefresh) refetchCases(); + fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); + if (filterRefetch.current != null) { + filterRefetch.current(); + } + }, + [filterOptions, queryParams, filterRefetch.current] + ); - const toggleDeleteModal = useCallback((deleteCase: Case) => { - handleToggleModal(); - setDeleteThisCase(deleteCase); - }, []); + useEffect(() => { + if (isDeleted) { + refreshCases(); + dispatchResetIsDeleted(); + } + if (isUpdated) { + refreshCases(); + dispatchResetIsUpdated(); + } + }, [isDeleted, isUpdated]); + const confirmDeleteModal = useMemo( + () => ( + 0} + onCancel={handleToggleModal} + onConfirm={handleOnDeleteConfirm.bind( + null, + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] + )} + /> + ), + [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + ); - const toggleBulkDeleteModal = useCallback( - (caseIds: string[]) => { + const toggleDeleteModal = useCallback((deleteCase: Case) => { handleToggleModal(); - if (caseIds.length === 1) { - const singleCase = selectedCases.find((theCase) => theCase.id === caseIds[0]); - if (singleCase) { - return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + setDeleteThisCase(deleteCase); + }, []); + + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find((theCase) => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } } - } - const convertToDeleteCases: DeleteCase[] = caseIds.map((id) => ({ id })); - setDeleteBulk(convertToDeleteCases); - }, - [selectedCases] - ); + const convertToDeleteCases: DeleteCase[] = caseIds.map((id) => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); - const handleUpdateCaseStatus = useCallback( - (status: string) => { - updateBulkStatus(selectedCases, status); - }, - [selectedCases] - ); + const handleUpdateCaseStatus = useCallback( + (status: string) => { + updateBulkStatus(selectedCases, status); + }, + [selectedCases] + ); - const selectedCaseIds = useMemo( - (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), - [selectedCases] - ); + const selectedCaseIds = useMemo( + (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), + [selectedCases] + ); - const getBulkItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] - ); - const handleDispatchUpdate = useCallback( - (args: Omit) => { - dispatchUpdateCaseProperty({ ...args, refetchCasesStatus: fetchCasesStatus }); - }, - [dispatchUpdateCaseProperty, fetchCasesStatus] - ); + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] + ); + const handleDispatchUpdate = useCallback( + (args: Omit) => { + dispatchUpdateCaseProperty({ ...args, refetchCasesStatus: fetchCasesStatus }); + }, + [dispatchUpdateCaseProperty, fetchCasesStatus] + ); - const actions = useMemo( - () => - getActions({ - caseStatus: filterOptions.status, - deleteCaseOnClick: toggleDeleteModal, - dispatchUpdate: handleDispatchUpdate, - }), - [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] - ); + const actions = useMemo( + () => + getActions({ + caseStatus: filterOptions.status, + deleteCaseOnClick: toggleDeleteModal, + dispatchUpdate: handleDispatchUpdate, + }), + [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] + ); - const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); + const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); - const tableOnChangeCallback = useCallback( - ({ page, sort }: EuiBasicTableOnChange) => { - let newQueryParams = queryParams; - if (sort) { - newQueryParams = { - ...newQueryParams, - sortField: getSortField(sort.field), - sortOrder: sort.direction, - }; - } - if (page) { - newQueryParams = { - ...newQueryParams, - page: page.index + 1, - perPage: page.size, - }; - } - setQueryParams(newQueryParams); - refreshCases(false); - }, - [queryParams] - ); + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newQueryParams = queryParams; + if (sort) { + newQueryParams = { + ...newQueryParams, + sortField: getSortField(sort.field), + sortOrder: sort.direction, + }; + } + if (page) { + newQueryParams = { + ...newQueryParams, + page: page.index + 1, + perPage: page.size, + }; + } + setQueryParams(newQueryParams); + refreshCases(false); + }, + [queryParams] + ); - const onFilterChangedCallback = useCallback( - (newFilterOptions: Partial) => { - if (newFilterOptions.status && newFilterOptions.status === 'closed') { - setQueryParams({ sortField: SortFieldCase.closedAt }); - } else if (newFilterOptions.status && newFilterOptions.status === 'open') { - setQueryParams({ sortField: SortFieldCase.createdAt }); - } - setFilters(newFilterOptions); - refreshCases(false); - }, - [filterOptions, queryParams] - ); + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial) => { + if (newFilterOptions.status && newFilterOptions.status === 'closed') { + setQueryParams({ sortField: SortFieldCase.closedAt }); + } else if (newFilterOptions.status && newFilterOptions.status === 'open') { + setQueryParams({ sortField: SortFieldCase.createdAt }); + } + setFilters(newFilterOptions); + refreshCases(false); + }, + [filterOptions, queryParams] + ); - const memoizedGetCasesColumns = useMemo( - () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), - [actions, filterOptions.status, userCanCrud] - ); - const memoizedPagination = useMemo( - () => ({ - pageIndex: queryParams.page - 1, - pageSize: queryParams.perPage, - totalItemCount: data.total, - pageSizeOptions: [5, 10, 15, 20, 25], - }), - [data, queryParams] - ); + const memoizedGetCasesColumns = useMemo( + () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status, isModal), + [actions, filterOptions.status, userCanCrud, isModal] + ); + const memoizedPagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 15, 20, 25], + }), + [data, queryParams] + ); - const sorting: EuiTableSortingType = { - sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, - }; - const euiBasicTableSelectionProps = useMemo>( - () => ({ onSelectionChange: setSelectedCases }), - [selectedCases] - ); - const isCasesLoading = useMemo( - () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, - [loading] - ); - const isDataEmpty = useMemo(() => data.total === 0, [data]); + const sorting: EuiTableSortingType = { + sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, + }; + const euiBasicTableSelectionProps = useMemo>( + () => ({ onSelectionChange: setSelectedCases }), + [selectedCases] + ); + const isCasesLoading = useMemo( + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] + ); + const isDataEmpty = useMemo(() => data.total === 0, [data]); - return ( - <> - {!isEmpty(actionsErrors) && ( - - )} - - - - - - - - - - } - titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} - urlSearch={urlSearch} - /> - - - - {i18n.CREATE_TITLE} - - - - - {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( - - )} - - - {isCasesLoading && isDataEmpty ? ( -
- -
- ) : ( -
- - - - - {i18n.SHOWING_CASES(data.total ?? 0)} - - - - - {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - - {userCanCrud && ( - - {i18n.BULK_ACTIONS} - - )} - - {i18n.REFRESH} - - - - - {i18n.NO_CASES}} - titleSize="xs" - body={i18n.NO_CASES_BODY} - actions={ - - {i18n.ADD_NEW_CASE} - - } + const TableWrap = useMemo(() => (isModal ? 'span' : Panel), [isModal]); + return ( + <> + {!isEmpty(actionsErrors) && ( + + )} + {!isModal && ( + + + + + + + + + + } + titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} + urlSearch={urlSearch} /> - } - onChange={tableOnChangeCallback} - pagination={memoizedPagination} - selection={userCanCrud ? euiBasicTableSelectionProps : {}} - sorting={sorting} - /> -
+ + + + {i18n.CREATE_TITLE} + + + + )} -
- {confirmDeleteModal} - - ); -}); + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( + + )} + + + {isCasesLoading && isDataEmpty ? ( +
+ +
+ ) : ( +
+ + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + {!isModal && ( + + + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} + + {userCanCrud && ( + + {i18n.BULK_ACTIONS} + + )} + + {i18n.REFRESH} + + + )} + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + rowProps={(item) => + isModal + ? { + onClick: () => onRowClick(item.id), + tabIndex: 0, + } + : {} + } + selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined} + sorting={sorting} + /> +
+ )} +
+ {confirmDeleteModal} + + ); + } +); AllCases.displayName = 'AllCases'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx new file mode 100644 index 00000000000000..9b2dfd1a5dd0b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx @@ -0,0 +1,56 @@ +/* + * 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 { + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, +} from '@elastic/eui'; +import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; +import { AllCases } from '../all_cases'; +import * as i18n from './translations'; + +interface AllCasesModalProps { + onCloseCaseModal: () => void; + showCaseModal: boolean; + onRowClick: (id: string) => void; +} + +export const AllCasesModalComponent = ({ + onCloseCaseModal, + onRowClick, + showCaseModal, +}: AllCasesModalProps) => { + const userPermissions = useGetUserSavedObjectPermissions(); + let modal; + if (showCaseModal) { + modal = ( + + + + {i18n.SELECT_CASE_TITLE} + + + + + + + ); + } + + return <>{modal}; +}; + +export const AllCasesModal = React.memo(AllCasesModalComponent); + +AllCasesModal.displayName = 'AllCasesModal'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts new file mode 100644 index 00000000000000..e0f84d85414241 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/translations.ts @@ -0,0 +1,10 @@ +/* + * 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 SELECT_CASE_TITLE = i18n.translate('xpack.securitySolution.case.caseModal.title', { + defaultMessage: 'Select case to attach timeline', +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index 9030e2713548bd..40c42fe166e74d 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -14,7 +14,7 @@ import { useRouteSpy } from './use_route_spy'; export const SpyRouteComponent = memo( ({ - location: { pathname, search }, + location: { pathname, search, state: locationState }, history, match: { params: { pageName, detailName, tabName, flowTarget }, @@ -41,7 +41,16 @@ export const SpyRouteComponent = memo( route: { detailName, flowTarget, - history, + history: { + ...history, + location: { + ...history.location, + state: { + ...history.location.state, + ...locationState, + }, + }, + }, pageName, pathName: pathname, state, @@ -55,7 +64,16 @@ export const SpyRouteComponent = memo( route: { detailName, flowTarget, - history, + history: { + ...history, + location: { + ...history.location, + state: { + ...history.location.state, + ...locationState, + }, + }, + }, pageName, pathName: pathname, search, @@ -71,7 +89,16 @@ export const SpyRouteComponent = memo( route: { detailName, flowTarget, - history, + history: { + ...history, + location: { + ...history.location, + state: { + ...history.location.state, + ...locationState, + }, + }, + }, pageName, pathName: pathname, search, @@ -81,7 +108,7 @@ export const SpyRouteComponent = memo( }); } } - }, [pathname, search, pageName, detailName, tabName, flowTarget, state]); + }, [pathname, search, pageName, detailName, tabName, flowTarget, state, locationState]); return null; } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index ab8a24889e9bf5..9451f92d046cd2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -22,6 +22,7 @@ import { timelineDefaults } from '../../../../timelines/store/timeline/defaults' import { InputsModelId } from '../../../../common/store/inputs/constants'; interface OwnProps { + onClose: () => void; timelineId: string; usersViewing: string[]; } @@ -33,14 +34,15 @@ const StatefulFlyoutHeader = React.memo( associateNote, createTimeline, description, - isFavorite, isDataInTimeline, isDatepickerLocked, - title, + isFavorite, noteIds, notesById, + onClose, status, timelineId, + title, toggleLock, updateDescription, updateIsFavorite, @@ -61,15 +63,16 @@ const StatefulFlyoutHeader = React.memo( isDataInTimeline={isDataInTimeline} isDatepickerLocked={isDatepickerLocked} isFavorite={isFavorite} - title={title} noteIds={noteIds} + onClose={onClose} status={status} timelineId={timelineId} + title={title} toggleLock={toggleLock} updateDescription={updateDescription} updateIsFavorite={updateIsFavorite} - updateTitle={updateTitle} updateNote={updateNote} + updateTitle={updateTitle} usersViewing={usersViewing} /> ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx index a4d9f0e8293dfc..a1bfac8c80fcc8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx @@ -40,7 +40,7 @@ const FlyoutHeaderWithCloseButtonComponent: React.FC<{ /> - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx index 2a673427d906cf..229361b95b955a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx @@ -38,21 +38,20 @@ export const InsertTimelinePopoverComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const { state } = useLocation(); - const [routerState, setRouterState] = useState(state ?? null); + const location = useLocation(); + const [routerStateImplemented, setRouterStateImplemented] = useState(false); useEffect(() => { - if (routerState && routerState.insertTimeline) { - dispatch( - timelineActions.showTimeline({ id: routerState.insertTimeline.timelineId, show: false }) - ); + const { state } = location; + if (state && state.insertTimeline && !routerStateImplemented) { + dispatch(timelineActions.showTimeline({ id: state.insertTimeline.timelineId, show: false })); onTimelineChange( - routerState.insertTimeline.timelineTitle, - routerState.insertTimeline.timelineSavedObjectId + state.insertTimeline.timelineTitle, + state.insertTimeline.timelineSavedObjectId ); - setRouterState(null); + setRouterStateImplemented(true); } - }, [routerState]); + }, [routerStateImplemented, location]); const handleClosePopover = useCallback(() => { setIsPopoverOpen(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 3ef10d394bc7bf..43333ecbf8e21e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -160,6 +160,36 @@ export const NewCase = React.memo( ); NewCase.displayName = 'NewCase'; +interface ExistingCaseProps { + onClosePopover: () => void; + onOpenCaseModal: () => void; + timelineStatus: TimelineStatus; +} +export const ExistingCase = React.memo( + ({ onClosePopover, onOpenCaseModal, timelineStatus }) => { + const handleClick = useCallback(() => { + onClosePopover(); + onOpenCaseModal(); + }, [onOpenCaseModal, onClosePopover]); + + return ( + <> + + {i18n.ATTACH_TIMELINE_TO_EXISTING_CASE} + + + ); + } +); +ExistingCase.displayName = 'ExistingCase'; + interface NewTimelineProps { createTimeline: CreateTimeline; onClosePopover: () => void; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index d8966a58748eda..9bd6bd57addfdd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -6,6 +6,8 @@ import React, { useState, useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Note } from '../../../../common/lib/note'; @@ -15,6 +17,11 @@ import { AssociateNote, UpdateNote } from '../../notes/helpers'; import { TimelineProperties } from './styles'; import { PropertiesRight } from './properties_right'; import { PropertiesLeft } from './properties_left'; +import { AllCasesModal } from '../../../../cases/components/all_cases_modal'; +import { SiemPageName } from '../../../../app/types'; +import * as i18n from './translations'; +import { State } from '../../../../common/store'; +import { timelineSelectors } from '../../../store/timeline'; type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; @@ -31,6 +38,7 @@ interface Props { isDatepickerLocked: boolean; isFavorite: boolean; noteIds: string[]; + onClose: () => void; timelineId: string; status: TimelineStatus; title: string; @@ -64,6 +72,7 @@ export const Properties = React.memo( isDatepickerLocked, isFavorite, noteIds, + onClose, status, timelineId, title, @@ -88,6 +97,30 @@ export const Properties = React.memo( onClosePopover(); setShowTimelineModal(true); }, []); + const [showCaseModal, setShowCaseModal] = useState(false); + const onCloseCaseModal = useCallback(() => setShowCaseModal(false), []); + const onOpenCaseModal = useCallback(() => setShowCaseModal(true), []); + const history = useHistory(); + const currentTimeline = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + + const onRowClick = useCallback( + (id: string) => { + onCloseCaseModal(); + history.push({ + pathname: `/${SiemPageName.case}/${id}`, + state: { + insertTimeline: { + timelineId, + timelineSavedObjectId: currentTimeline.savedObjectId, + timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE, + }, + }, + }); + }, + [currentTimeline, history, timelineId, title] + ); const datePickerWidth = useMemo( () => @@ -135,6 +168,7 @@ export const Properties = React.memo( onButtonClick={onButtonClick} onClosePopover={onClosePopover} onCloseTimelineModal={onCloseTimelineModal} + onOpenCaseModal={onOpenCaseModal} onOpenTimelineModal={onOpenTimelineModal} onToggleShowNotes={onToggleShowNotes} showActions={showActions} @@ -150,6 +184,11 @@ export const Properties = React.memo( updateNote={updateNote} usersViewing={usersViewing} /> + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx index 963b67838e811a..5d3ed61f47c571 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx @@ -14,7 +14,7 @@ import { EuiToolTip, EuiAvatar, } from '@elastic/eui'; -import { NewTimeline, Description, NotesButton, NewCase } from './helpers'; +import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers'; import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button'; import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; @@ -60,55 +60,57 @@ type UpdateDescription = ({ id, description }: { id: string; description: string export type UpdateNote = (note: Note) => void; interface Props { - onButtonClick: () => void; - onClosePopover: () => void; - showActions: boolean; + associateNote: AssociateNote; createTimeline: CreateTimeline; - timelineId: string; - isDataInTimeline: boolean; - showNotes: boolean; - showNotesFromWidth: boolean; - showDescription: boolean; - showUsersView: boolean; - usersViewing: string[]; description: string; - updateDescription: UpdateDescription; - associateNote: AssociateNote; getNotesByIds: (noteIds: string[]) => Note[]; + isDataInTimeline: boolean; noteIds: string[]; - onToggleShowNotes: () => void; + onButtonClick: () => void; + onClosePopover: () => void; onCloseTimelineModal: () => void; + onOpenCaseModal: () => void; onOpenTimelineModal: () => void; + onToggleShowNotes: () => void; + showActions: boolean; + showDescription: boolean; + showNotes: boolean; + showNotesFromWidth: boolean; showTimelineModal: boolean; + showUsersView: boolean; status: TimelineStatus; + timelineId: string; title: string; + updateDescription: UpdateDescription; updateNote: UpdateNote; + usersViewing: string[]; } const PropertiesRightComponent: React.FC = ({ - onButtonClick, - showActions, - onClosePopover, + associateNote, createTimeline, - timelineId, - isDataInTimeline, - showNotesFromWidth, - showNotes, - showDescription, - showUsersView, - usersViewing, description, - updateDescription, - associateNote, getNotesByIds, + isDataInTimeline, noteIds, - onToggleShowNotes, - updateNote, - showTimelineModal, + onButtonClick, + onClosePopover, onCloseTimelineModal, + onOpenCaseModal, onOpenTimelineModal, - title, + onToggleShowNotes, + showActions, + showDescription, + showNotes, + showNotesFromWidth, + showTimelineModal, + showUsersView, status, + timelineId, + title, + updateDescription, + updateNote, + usersViewing, }) => ( @@ -148,6 +150,13 @@ const PropertiesRightComponent: React.FC = ({ timelineStatus={status} /> + + + Date: Thu, 4 Jun 2020 14:52:52 -0600 Subject: [PATCH 2/8] fix bug wip --- .../common/components/url_state/helpers.ts | 5 + .../public/common/utils/route/spy_routes.tsx | 108 +++++++++++++----- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 8f13e4dd0cdcf7..ef39a522133366 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -193,6 +193,7 @@ export const updateUrlStateString = ({ if (urlKey === CONSTANTS.appQuery) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.query === '') { + console.log('1', history); return replaceStateInLocation({ history, pathName, @@ -204,6 +205,7 @@ export const updateUrlStateString = ({ } else if (urlKey === CONSTANTS.timerange && updateTimerange) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.global != null) { + console.log('2', history); return replaceStateInLocation({ history, pathName, @@ -215,6 +217,7 @@ export const updateUrlStateString = ({ } else if (urlKey === CONSTANTS.filters) { const queryState = decodeRisonUrlState(newUrlStateString); if (isEmpty(queryState)) { + console.log('3', history); return replaceStateInLocation({ history, pathName, @@ -226,6 +229,7 @@ export const updateUrlStateString = ({ } else if (urlKey === CONSTANTS.timeline) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.id === '') { + console.log('4', history); return replaceStateInLocation({ history, pathName, @@ -235,6 +239,7 @@ export const updateUrlStateString = ({ }); } } + console.log('5', history); return search; }; diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index 40c42fe166e74d..a791e25d48a2e0 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -36,21 +36,13 @@ export const SpyRouteComponent = memo( useEffect(() => { if (pageName && !deepEqual(route.pathName, pathname)) { if (isInitializing && detailName == null) { + console.log('here1'); dispatch({ type: 'updateRouteWithOutSearch', route: { detailName, flowTarget, - history: { - ...history, - location: { - ...history.location, - state: { - ...history.location.state, - ...locationState, - }, - }, - }, + history, pageName, pathName: pathname, state, @@ -59,21 +51,13 @@ export const SpyRouteComponent = memo( }); setIsInitializing(false); } else { + console.log('here2', { state, locationState, history }); dispatch({ type: 'updateRoute', route: { detailName, flowTarget, - history: { - ...history, - location: { - ...history.location, - state: { - ...history.location.state, - ...locationState, - }, - }, - }, + history, pageName, pathName: pathname, search, @@ -84,21 +68,13 @@ export const SpyRouteComponent = memo( } } else { if (pageName && !deepEqual(state, route.state)) { + console.log('here3'); dispatch({ type: 'updateRoute', route: { detailName, flowTarget, - history: { - ...history, - location: { - ...history.location, - state: { - ...history.location.state, - ...locationState, - }, - }, - }, + history, pageName, pathName: pathname, search, @@ -109,6 +85,78 @@ export const SpyRouteComponent = memo( } } }, [pathname, search, pageName, detailName, tabName, flowTarget, state, locationState]); + + // useEffect(() => { + // console.log('STATE', state); + // if (pageName && !deepEqual(route.pathName, pathname)) { + // if (isInitializing && detailName == null) { + // console.log('here1'); + // dispatch({ + // type: 'updateRouteWithOutSearch', + // route: { + // detailName, + // flowTarget, + // history, + // pageName, + // pathName: pathname, + // state: { ...state, ...locationState }, + // tabName, + // }, + // }); + // setIsInitializing(false); + // } else { + // console.log('here2', { state, locationState, history }); + // dispatch({ + // type: 'updateRoute', + // route: { + // detailName, + // flowTarget, + // history: { + // ...history, + // location: { + // ...history.location, + // state: { + // ...history.location.state, + // ...locationState, + // }, + // }, + // }, + // pageName, + // pathName: pathname, + // search, + // state: { ...state, ...locationState }, + // tabName, + // }, + // }); + // } + // } else { + // if (pageName && !deepEqual(state, route.state)) { + // console.log('here3'); + // dispatch({ + // type: 'updateRoute', + // route: { + // detailName, + // flowTarget, + // history: { + // ...history, + // location: { + // ...history.location, + // state: { + // ...history.location.state, + // ...locationState, + // }, + // }, + // }, + // pageName, + // pathName: pathname, + // search, + // state: { ...state, ...locationState }, + // tabName, + // }, + // }); + // } + // } + // }, [pathname, search, pageName, detailName, tabName, flowTarget, state, locationState]); return null; } ); From 316cfa25888072fb6440d0b599bb95b971a4d255 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Fri, 5 Jun 2020 13:36:07 -0600 Subject: [PATCH 3/8] wip, fixin tests --- .../cases/components/all_cases/index.tsx | 1 - .../public/cases/pages/case_details.tsx | 2 - .../public/common/utils/route/spy_routes.tsx | 79 +---------------- .../insert_timeline_popover/index.test.tsx | 30 +++---- .../insert_timeline_popover/index.tsx | 34 +++---- .../timeline/properties/helpers.tsx | 21 +++-- .../timeline/properties/index.test.tsx | 88 ++++++++++--------- .../components/timeline/properties/index.tsx | 22 ++--- .../timelines/store/timeline/actions.ts | 3 + .../timelines/store/timeline/reducer.ts | 7 +- .../timelines/store/timeline/selectors.ts | 5 +- .../public/timelines/store/timeline/types.ts | 7 ++ 12 files changed, 120 insertions(+), 179 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 6f7f87d35e670f..d48e6caf36c8b2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -436,7 +436,6 @@ export const AllCases = React.memo( isModal ? { onClick: () => onRowClick(item.id), - tabIndex: 0, } : {} } diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 5dfe12179b9900..780de303c02d34 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -10,7 +10,6 @@ import { useParams, Redirect } from 'react-router-dom'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; -import { SpyRoute } from '../../common/utils/route/spy_routes'; import { getCaseUrl } from '../../common/components/link_to'; import { navTabs } from '../../app/home/home_navigations'; import { CaseView } from '../components/case_view'; @@ -36,7 +35,6 @@ export const CaseDetailsPage = React.memo(() => { )} - ) : null; }); diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index a791e25d48a2e0..9030e2713548bd 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -14,7 +14,7 @@ import { useRouteSpy } from './use_route_spy'; export const SpyRouteComponent = memo( ({ - location: { pathname, search, state: locationState }, + location: { pathname, search }, history, match: { params: { pageName, detailName, tabName, flowTarget }, @@ -36,7 +36,6 @@ export const SpyRouteComponent = memo( useEffect(() => { if (pageName && !deepEqual(route.pathName, pathname)) { if (isInitializing && detailName == null) { - console.log('here1'); dispatch({ type: 'updateRouteWithOutSearch', route: { @@ -51,7 +50,6 @@ export const SpyRouteComponent = memo( }); setIsInitializing(false); } else { - console.log('here2', { state, locationState, history }); dispatch({ type: 'updateRoute', route: { @@ -68,7 +66,6 @@ export const SpyRouteComponent = memo( } } else { if (pageName && !deepEqual(state, route.state)) { - console.log('here3'); dispatch({ type: 'updateRoute', route: { @@ -84,79 +81,7 @@ export const SpyRouteComponent = memo( }); } } - }, [pathname, search, pageName, detailName, tabName, flowTarget, state, locationState]); - - // useEffect(() => { - // console.log('STATE', state); - // if (pageName && !deepEqual(route.pathName, pathname)) { - // if (isInitializing && detailName == null) { - // console.log('here1'); - // dispatch({ - // type: 'updateRouteWithOutSearch', - // route: { - // detailName, - // flowTarget, - // history, - // pageName, - // pathName: pathname, - // state: { ...state, ...locationState }, - // tabName, - // }, - // }); - // setIsInitializing(false); - // } else { - // console.log('here2', { state, locationState, history }); - // dispatch({ - // type: 'updateRoute', - // route: { - // detailName, - // flowTarget, - // history: { - // ...history, - // location: { - // ...history.location, - // state: { - // ...history.location.state, - // ...locationState, - // }, - // }, - // }, - // pageName, - // pathName: pathname, - // search, - // state: { ...state, ...locationState }, - // tabName, - // }, - // }); - // } - // } else { - // if (pageName && !deepEqual(state, route.state)) { - // console.log('here3'); - // dispatch({ - // type: 'updateRoute', - // route: { - // detailName, - // flowTarget, - // history: { - // ...history, - // location: { - // ...history.location, - // state: { - // ...history.location.state, - // ...locationState, - // }, - // }, - // }, - // pageName, - // pathName: pathname, - // search, - // state: { ...state, ...locationState }, - // tabName, - // }, - // }); - // } - // } - // }, [pathname, search, pageName, detailName, tabName, flowTarget, state, locationState]); + }, [pathname, search, pageName, detailName, tabName, flowTarget, state]); return null; } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx index 0a70413b7ea295..2ffbae1f7eb5c0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx @@ -17,6 +17,14 @@ jest.mock('react-redux', () => { return { ...reactRedux, useDispatch: () => mockDispatch, + useSelector: jest + .fn() + .mockReturnValueOnce({ + timelineId: 'timeline-id', + timelineSavedObjectId: '34578-3497-5893-47589-34759', + timelineTitle: 'Timeline title', + }) + .mockReturnValue(null), }; }); const mockLocation = { @@ -25,17 +33,6 @@ const mockLocation = { search: '', state: '', }; -const mockLocationWithState = { - ...mockLocation, - state: { - insertTimeline: { - timelineId: 'timeline-id', - timelineSavedObjectId: '34578-3497-5893-47589-34759', - timelineTitle: 'Timeline title', - }, - }, -}; - const onTimelineChange = jest.fn(); const defaultProps = { isDisabled: false, @@ -43,18 +40,21 @@ const defaultProps = { }; describe('Insert timeline popover ', () => { - beforeEach(() => { - jest.resetAllMocks(); + afterEach(() => { + jest.clearAllMocks(); }); it('should insert a timeline when passed in the router state', () => { - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocationWithState); mount(); - expect(mockDispatch).toBeCalledWith({ + expect(mockDispatch.mock.calls[0][0]).toEqual({ payload: { id: 'timeline-id', show: false }, type: 'x-pack/security_solution/local/timeline/SHOW_TIMELINE', }); expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); + expect(mockDispatch.mock.calls[1][0]).toEqual({ + payload: null, + type: 'x-pack/security_solution/local/timeline/SET_INSERT_TIMELINE', + }); }); it('should do nothing when router state', () => { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx index 229361b95b955a..ef4e9a305d23d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx @@ -6,14 +6,15 @@ import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { OpenTimelineResult } from '../../open_timeline/types'; import { SelectableTimeline } from '../selectable_timeline'; import * as i18n from '../translations'; -import { timelineActions } from '../../../../timelines/store/timeline'; +import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { TimelineType } from '../../../../../common/types/timeline'; +import { State } from '../../../../common/store'; +import { setInsertTimeline } from '../../../store/timeline/actions'; interface InsertTimelinePopoverProps { isDisabled: boolean; @@ -21,14 +22,6 @@ interface InsertTimelinePopoverProps { onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; } -interface RouterState { - insertTimeline: { - timelineId: string; - timelineSavedObjectId: string; - timelineTitle: string; - }; -} - type Props = InsertTimelinePopoverProps; export const InsertTimelinePopoverComponent: React.FC = ({ @@ -38,20 +31,17 @@ export const InsertTimelinePopoverComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const location = useLocation(); - const [routerStateImplemented, setRouterStateImplemented] = useState(false); + const insertTimeline = useSelector((state: State) => { + return timelineSelectors.selectInsertTimeline(state); + }); useEffect(() => { - const { state } = location; - if (state && state.insertTimeline && !routerStateImplemented) { - dispatch(timelineActions.showTimeline({ id: state.insertTimeline.timelineId, show: false })); - onTimelineChange( - state.insertTimeline.timelineTitle, - state.insertTimeline.timelineSavedObjectId - ); - setRouterStateImplemented(true); + if (insertTimeline != null) { + dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); + onTimelineChange(insertTimeline.timelineTitle, insertTimeline.timelineSavedObjectId); + dispatch(setInsertTimeline(null)); } - }, [routerStateImplemented, location]); + }, [insertTimeline, dispatch]); const handleClosePopover = useCallback(() => { setIsPopoverOpen(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 43333ecbf8e21e..99ef88538fde78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -21,7 +21,7 @@ import React, { useCallback } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { Note } from '../../../../common/lib/note'; @@ -33,6 +33,7 @@ import * as i18n from './translations'; import { SiemPageName } from '../../../../app/types'; import { timelineSelectors } from '../../../../timelines/store/timeline'; import { State } from '../../../../common/store'; +import { setInsertTimeline } from '../../../store/timeline/actions'; export const historyToolTip = 'The chronological history of actions related to this timeline'; export const streamLiveToolTip = 'Update the Timeline as new data arrives'; @@ -127,22 +128,24 @@ interface NewCaseProps { export const NewCase = React.memo( ({ onClosePopover, timelineId, timelineStatus, timelineTitle }) => { const history = useHistory(); + const dispatch = useDispatch(); const { savedObjectId } = useSelector((state: State) => timelineSelectors.selectTimeline(state, timelineId) ); + const handleClick = useCallback(() => { onClosePopover(); history.push({ pathname: `/${SiemPageName.case}/create`, - state: { - insertTimeline: { - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, - }, - }, }); - }, [onClosePopover, history, timelineId, timelineTitle]); + dispatch( + setInsertTimeline({ + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }) + ); + }, [dispatch, onClosePopover, history, timelineId, timelineTitle]); return ( ({ - width: mockedWidth, -})); +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useGetUserSavedObjectPermissions: jest.fn().mockReturnValue({ crud: true }), + }; +}); jest.mock('react-redux', () => { const originalModule = jest.requireActual('react-redux'); @@ -32,6 +30,7 @@ jest.mock('react-redux', () => { return { ...originalModule, useSelector: jest.fn().mockReturnValue({ savedObjectId: '1' }), + useHistory: jest.fn(), }; }); @@ -43,6 +42,15 @@ jest.mock('react-router-dom', () => { useHistory: jest.fn(), }; }); +let mockedWidth = 1000; +jest.mock('../../../../common/components/utils', () => { + const originalModule = jest.requireActual('../../../../common/components/utils'); + + return { + ...originalModule, + useThrottledResizeObserver: jest.fn().mockReturnValue({ width: mockedWidth }), + }; +}); describe('Properties', () => { const usersViewing = ['elastic']; @@ -58,7 +66,7 @@ describe('Properties', () => { test('renders correctly', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); @@ -91,7 +99,7 @@ describe('Properties', () => { test('renders correctly draft timeline', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); @@ -123,7 +131,7 @@ describe('Properties', () => { test('it renders an empty star icon when it is NOT a favorite', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect(wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').exists()).toEqual(true); @@ -151,7 +159,7 @@ describe('Properties', () => { test('it renders a filled star icon when it is a favorite', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect(wrapper.find('[data-test-subj="timeline-favorite-filled-star"]').exists()).toEqual(true); @@ -181,7 +189,7 @@ describe('Properties', () => { const title = 'foozle'; const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect(wrapper.find('[data-test-subj="timeline-title"]').first().props().value).toEqual(title); @@ -209,7 +217,7 @@ describe('Properties', () => { test('it renders the date picker with the lock icon', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( @@ -242,7 +250,7 @@ describe('Properties', () => { test('it renders the lock icon when isDatepickerLocked is true', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( wrapper @@ -274,7 +282,7 @@ describe('Properties', () => { test('it renders the unlock icon when isDatepickerLocked is false', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( wrapper @@ -309,7 +317,7 @@ describe('Properties', () => { mockedWidth = showDescriptionThreshold; const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( @@ -346,7 +354,7 @@ describe('Properties', () => { mockedWidth = showDescriptionThreshold - 1; const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( @@ -381,7 +389,7 @@ describe('Properties', () => { mockedWidth = showNotesThreshold; const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( @@ -416,7 +424,7 @@ describe('Properties', () => { mockedWidth = showNotesThreshold - 1; const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect( @@ -449,7 +457,7 @@ describe('Properties', () => { test('it renders a settings icon', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toEqual(true); @@ -479,7 +487,7 @@ describe('Properties', () => { const title = 'port scan'; const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(true); @@ -507,7 +515,7 @@ describe('Properties', () => { test('it does NOT render an avatar for the current user viewing the timeline when it does NOT have a title', () => { const wrapper = mount( - + { updateNote={jest.fn()} usersViewing={usersViewing} /> - + ); expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index 9bd6bd57addfdd..22b9393889d463 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -6,7 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; @@ -22,6 +22,7 @@ import { SiemPageName } from '../../../../app/types'; import * as i18n from './translations'; import { State } from '../../../../common/store'; import { timelineSelectors } from '../../../store/timeline'; +import { setInsertTimeline } from '../../../store/timeline/actions'; type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; @@ -38,7 +39,6 @@ interface Props { isDatepickerLocked: boolean; isFavorite: boolean; noteIds: string[]; - onClose: () => void; timelineId: string; status: TimelineStatus; title: string; @@ -72,7 +72,6 @@ export const Properties = React.memo( isDatepickerLocked, isFavorite, noteIds, - onClose, status, timelineId, title, @@ -87,6 +86,7 @@ export const Properties = React.memo( const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); const [showTimelineModal, setShowTimelineModal] = useState(false); + const dispatch = useDispatch(); const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); @@ -110,16 +110,16 @@ export const Properties = React.memo( onCloseCaseModal(); history.push({ pathname: `/${SiemPageName.case}/${id}`, - state: { - insertTimeline: { - timelineId, - timelineSavedObjectId: currentTimeline.savedObjectId, - timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE, - }, - }, }); + dispatch( + setInsertTimeline({ + timelineId, + timelineSavedObjectId: currentTimeline.savedObjectId, + timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE, + }) + ); }, - [currentTimeline, history, timelineId, title] + [currentTimeline, dispatch, history, timelineId, title] ); const datePickerWidth = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index e11d1bcc72e099..9e2f3ae284d4d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -16,6 +16,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../../graphql/types'; +import { InsertTimeline } from './types'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline'); @@ -96,6 +97,8 @@ export const addTimeline = actionCreator<{ timeline: TimelineModel; }>('ADD_TIMELINE'); +export const setInsertTimeline = actionCreator('SET_INSERT_TIMELINE'); + export const startTimelineSaving = actionCreator<{ id: string; }>('START_TIMELINE_SAVING'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index a8ae39527cdbfc..33b43cb9bb71b3 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -53,7 +53,7 @@ import { updateIsLoading, setSavedQueryId, setFilters, - updateEventType, + updateEventType, setInsertTimeline, } from './actions'; import { addNewTimeline, @@ -106,6 +106,7 @@ export const initialTimelineState: TimelineState = { newTimelineModel: null, }, showCallOutUnauthorizedMsg: false, + insertTimeline: null, }; /** The reducer for all timeline actions */ @@ -483,4 +484,8 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) + .case(setInsertTimeline, (state, insertTimeline) => ({ + ...state, + insertTimeline, + })) .build(); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index af7ac075468c38..a80a28660e28b9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -10,7 +10,7 @@ import { isFromKueryExpressionValid } from '../../../common/lib/keury'; import { State } from '../../../common/store/types'; import { TimelineModel } from './model'; -import { AutoSavedWarningMsg, TimelineById } from './types'; +import { AutoSavedWarningMsg, InsertTimeline, TimelineById } from './types'; const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; @@ -22,6 +22,9 @@ const selectCallOutUnauthorizedMsg = (state: State): boolean => export const selectTimeline = (state: State, timelineId: string): TimelineModel => state.timeline.timelineById[timelineId]; +export const selectInsertTimeline = (state: State): InsertTimeline | null => + state.timeline.insertTimeline; + export const autoSaveMsgSelector = createSelector(selectAutoSaveMsg, (autoSaveMsg) => autoSaveMsg); export const timelineByIdSelector = createSelector( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts index 1cc4517d2c9642..aa6c3086142872 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts @@ -16,6 +16,12 @@ export interface TimelineById { [id: string]: TimelineModel; } +export interface InsertTimeline { + timelineId: string; + timelineSavedObjectId: string | null; + timelineTitle: string; +} + export const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference /** The state of all timelines is stored here */ @@ -23,6 +29,7 @@ export interface TimelineState { timelineById: TimelineById; autoSavedWarningMsg: AutoSavedWarningMsg; showCallOutUnauthorizedMsg: boolean; + insertTimeline: InsertTimeline | null; } export interface ActionTimeline extends Action { From 9a6a7b5f3fe4f73537b4a4ae0aa2bfd25add22d0 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 8 Jun 2020 11:17:33 -0600 Subject: [PATCH 4/8] tests pass --- .../public/common/mock/index.ts | 1 + .../header_with_close_button/index.test.tsx | 14 + .../__snapshots__/timeline.test.tsx.snap | 51 ++- .../timeline/properties/index.test.tsx | 344 +++--------------- .../components/timeline/timeline.test.tsx | 29 +- 5 files changed, 121 insertions(+), 318 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/mock/index.ts b/x-pack/plugins/security_solution/public/common/mock/index.ts index bdad0ab1712abe..30eb4c63f40b85 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index.ts @@ -15,3 +15,4 @@ export * from './test_providers'; export * from './utils'; export * from './mock_ecs'; export * from './timeline_results'; +export * from './kibana_react'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx index 9a52e9cf4e5387..3db301261d42dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx @@ -10,6 +10,20 @@ import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { FlyoutHeaderWithCloseButton } from '.'; +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); +jest.mock('../../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../../common/lib/kibana'); + return { + ...originalModule, + useGetUserSavedObjectPermissions: jest.fn(), + }; +}); describe('FlyoutHeaderWithCloseButton', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 4ed0b52fc0f146..4e6cce618880b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -606,19 +606,40 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` }, "filters": Array [], "uiSettings": Object { - "get": [Function], - "get$": [MockFunction], - "getAll": [MockFunction], - "getSaved$": [MockFunction], - "getUpdate$": [MockFunction], - "getUpdateErrors$": [MockFunction], - "isCustom": [MockFunction], - "isDeclared": [MockFunction], - "isDefault": [MockFunction], - "isOverridden": [MockFunction], - "overrideLocalDefault": [MockFunction], - "remove": [MockFunction], - "set": [MockFunction], + "get": [MockFunction] { + "calls": Array [ + Array [ + "query:allowLeadingWildcards", + ], + Array [ + "query:queryString:options", + ], + Array [ + "courier:ignoreFilterIfFieldNotInIndex", + ], + Array [ + "dateFormat:tz", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + Object { + "type": "return", + "value": undefined, + }, + Object { + "type": "return", + "value": undefined, + }, + Object { + "type": "return", + "value": undefined, + }, + ], + }, }, "updated$": Subject { "_isScalar": false, @@ -826,7 +847,7 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` } inputId="timeline" /> - - + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 60b01e83efe1dc..089c4a4939a5b7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -6,7 +6,7 @@ import { mount } from 'enzyme'; import React from 'react'; - +import { TestProviders } from '../../../../common/mock'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { mockGlobalState, @@ -14,15 +14,21 @@ import { SUB_PLUGINS_REDUCER, } from '../../../../common/mock'; import { createStore, State } from '../../../../common/store'; -import { TestProviders } from '../../../../common/mock/test_providers'; +import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; + jest.mock('../../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../../common/lib/kibana'); return { ...originalModule, - useGetUserSavedObjectPermissions: jest.fn().mockReturnValue({ crud: true }), + useGetUserSavedObjectPermissions: jest.fn(), }; }); +let mockedWidth = 1000; +jest.mock('../../../../common/components/utils'); +(useThrottledResizeObserver as jest.Mock).mockImplementation(() => ({ + width: mockedWidth, +})); jest.mock('react-redux', () => { const originalModule = jest.requireActual('react-redux'); @@ -30,7 +36,6 @@ jest.mock('react-redux', () => { return { ...originalModule, useSelector: jest.fn().mockReturnValue({ savedObjectId: '1' }), - useHistory: jest.fn(), }; }); @@ -42,19 +47,28 @@ jest.mock('react-router-dom', () => { useHistory: jest.fn(), }; }); -let mockedWidth = 1000; -jest.mock('../../../../common/components/utils', () => { - const originalModule = jest.requireActual('../../../../common/components/utils'); - - return { - ...originalModule, - useThrottledResizeObserver: jest.fn().mockReturnValue({ width: mockedWidth }), - }; -}); +const usersViewing = ['elastic']; +const defaultProps = { + associateNote: jest.fn(), + createTimeline: jest.fn(), + isDataInTimeline: false, + isDatepickerLocked: false, + isFavorite: false, + title: '', + description: '', + getNotesByIds: jest.fn(), + noteIds: [], + status: TimelineStatus.active, + timelineId: 'abc', + toggleLock: jest.fn(), + updateDescription: jest.fn(), + updateIsFavorite: jest.fn(), + updateTitle: jest.fn(), + updateNote: jest.fn(), + usersViewing, +}; describe('Properties', () => { - const usersViewing = ['elastic']; - const state: State = mockGlobalState; let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); @@ -67,25 +81,7 @@ describe('Properties', () => { test('renders correctly', () => { const wrapper = mount( - + ); @@ -100,25 +96,7 @@ describe('Properties', () => { test('renders correctly draft timeline', () => { const wrapper = mount( - + ); @@ -132,25 +110,7 @@ describe('Properties', () => { test('it renders an empty star icon when it is NOT a favorite', () => { const wrapper = mount( - + ); @@ -160,25 +120,7 @@ describe('Properties', () => { test('it renders a filled star icon when it is a favorite', () => { const wrapper = mount( - + ); @@ -190,25 +132,7 @@ describe('Properties', () => { const wrapper = mount( - + ); @@ -218,25 +142,7 @@ describe('Properties', () => { test('it renders the date picker with the lock icon', () => { const wrapper = mount( - + ); @@ -251,25 +157,7 @@ describe('Properties', () => { test('it renders the lock icon when isDatepickerLocked is true', () => { const wrapper = mount( - + ); expect( @@ -283,25 +171,7 @@ describe('Properties', () => { test('it renders the unlock icon when isDatepickerLocked is false', () => { const wrapper = mount( - + ); expect( @@ -318,25 +188,7 @@ describe('Properties', () => { const wrapper = mount( - + ); @@ -355,25 +207,7 @@ describe('Properties', () => { const wrapper = mount( - + ); @@ -390,25 +224,7 @@ describe('Properties', () => { const wrapper = mount( - + ); @@ -425,25 +241,7 @@ describe('Properties', () => { const wrapper = mount( - + ); @@ -458,25 +256,7 @@ describe('Properties', () => { test('it renders a settings icon', () => { const wrapper = mount( - + ); @@ -488,25 +268,7 @@ describe('Properties', () => { const wrapper = mount( - + ); @@ -516,25 +278,7 @@ describe('Properties', () => { test('it does NOT render an avatar for the current user viewing the timeline when it does NOT have a title', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index b07be4a471a70c..a09b64b7e8726e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -24,13 +24,36 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; - -jest.mock('../../../common/lib/kibana'); - +// import * as all from '../../../common/lib/kibana'; const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); +// jest.spyOn(all, 'useGetUserSavedObjectPermissions').mockImplementation(() => ({})); +jest.mock('../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: jest.fn().mockReturnValue({ + services: { + uiSettings: { + get: jest.fn(), + }, + savedObjects: { + client: {}, + }, + }, + }), + useGetUserSavedObjectPermissions: jest.fn(), + }; +}); describe('Timeline', () => { let props = {} as TimelineComponentProps; const sort: Sort = { From ed20f6414f65159b21589bbe2a0bfdeecc9bb8a0 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 8 Jun 2020 13:09:52 -0600 Subject: [PATCH 5/8] type and test fixing --- .../cases/components/all_cases/index.test.tsx | 2 +- .../public/common/mock/global_state.ts | 1 + .../timelines/components/flyout/header/index.tsx | 3 --- .../flyout/header_with_close_button/index.tsx | 2 +- .../timelines/components/timeline/index.test.tsx | 15 ++++++++++++++- .../components/timeline/timeline.test.tsx | 1 - .../public/timelines/store/timeline/helpers.ts | 1 + 7 files changed, 18 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index e3f4fee15ce681..f26accea4792c6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -151,7 +151,7 @@ describe('AllCases', () => { expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); expect(column.find('span').text()).toEqual(emptyTag); }; - getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key)); + getCasesColumns([], 'open', false).map((i, key) => i.name != null && checkIt(`${i.name}`, key)); }); it('should tableHeaderSortButton AllCases', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 4af39ade70d255..3e84e4035e15ed 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -229,6 +229,7 @@ export const mockGlobalState: State = { status: TimelineStatus.active, }, }, + insertTimeline: null, }, /** * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 9451f92d046cd2..8ad32d6e2cad01 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -22,7 +22,6 @@ import { timelineDefaults } from '../../../../timelines/store/timeline/defaults' import { InputsModelId } from '../../../../common/store/inputs/constants'; interface OwnProps { - onClose: () => void; timelineId: string; usersViewing: string[]; } @@ -39,7 +38,6 @@ const StatefulFlyoutHeader = React.memo( isFavorite, noteIds, notesById, - onClose, status, timelineId, title, @@ -64,7 +62,6 @@ const StatefulFlyoutHeader = React.memo( isDatepickerLocked={isDatepickerLocked} isFavorite={isFavorite} noteIds={noteIds} - onClose={onClose} status={status} timelineId={timelineId} title={title} diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx index a1bfac8c80fcc8..a4d9f0e8293dfc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.tsx @@ -40,7 +40,7 @@ const FlyoutHeaderWithCloseButtonComponent: React.FC<{ /> - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 581fa125d21e2f..99a8344ea61a7c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -26,7 +26,13 @@ import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { StatefulTimeline, Props as StatefulTimelineProps } from './index'; import { Timeline } from './timeline'; -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useGetUserSavedObjectPermissions: jest.fn(), + }; +}); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); @@ -34,7 +40,14 @@ mockUseResizeObserver.mockImplementation(() => ({})); const mockUseSignalIndex: jest.Mock = useSignalIndex as jest.Mock; jest.mock('../../../alerts/containers/detection_engine/alerts/use_signal_index'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); describe('StatefulTimeline', () => { let props = {} as StatefulTimelineProps; const sort: Sort = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index a09b64b7e8726e..ea4cb9198d02dd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -36,7 +36,6 @@ jest.mock('react-router-dom', () => { useHistory: jest.fn(), }; }); -// jest.spyOn(all, 'useGetUserSavedObjectPermissions').mockImplementation(() => ({})); jest.mock('../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../common/lib/kibana'); return { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index b2472cbe89a50c..5fb9b2e720e807 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -32,6 +32,7 @@ export const initialTimelineState: TimelineState = { newTimelineModel: null, }, showCallOutUnauthorizedMsg: false, + insertTimeline: null, }; interface AddTimelineHistoryParams { From db6ee6ec0c9617fc2ffe71f19126cad56f3ca38d Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 8 Jun 2020 15:13:45 -0600 Subject: [PATCH 6/8] more tests --- .../cases/components/all_cases/index.test.tsx | 14 ++ .../cases/components/all_cases/index.tsx | 2 +- .../components/all_cases_modal/index.test.tsx | 140 ++++++++++++++++++ .../components/all_cases_modal/index.tsx | 2 +- .../common/components/url_state/helpers.ts | 5 - .../timeline/properties/index.test.tsx | 76 +++++++--- .../components/timeline/timeline.test.tsx | 1 - .../timelines/store/timeline/reducer.ts | 17 ++- 8 files changed, 224 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index f26accea4792c6..bbb96f433d3c8f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -153,6 +153,20 @@ describe('AllCases', () => { }; getCasesColumns([], 'open', false).map((i, key) => i.name != null && checkIt(`${i.name}`, key)); }); + + it('should not render case link or actions on modal=true', () => { + const wrapper = mount( + + + + ); + const checkIt = (columnName: string) => { + expect(columnName).not.toEqual(i18n.ACTIONS); + }; + getCasesColumns([], 'open', true).map((i, key) => i.name != null && checkIt(`${i.name}`)); + expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy(); + }); + it('should tableHeaderSortButton AllCases', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 7b8a0c503aecba..d27f383fb94e33 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -358,7 +358,7 @@ export const AllCases = React.memo( {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( )} - + { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useGetUserSavedObjectPermissions: jest.fn(), + }; +}); + +const onCloseCaseModal = jest.fn(); +const onRowClick = jest.fn(); +const defaultProps = { + onCloseCaseModal, + onRowClick, + showCaseModal: true, +}; +describe('AllCasesModal', () => { + const dispatchResetIsDeleted = jest.fn(); + const dispatchResetIsUpdated = jest.fn(); + const dispatchUpdateCaseProperty = jest.fn(); + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); + const refetchCases = jest.fn(); + const setFilters = jest.fn(); + const setQueryParams = jest.fn(); + const setSelectedCases = jest.fn(); + const updateBulkStatus = jest.fn(); + const fetchCasesStatus = jest.fn(); + + const defaultGetCases = { + ...useGetCasesMockState, + dispatchUpdateCaseProperty, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + }; + const defaultDeleteCases = { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isDeleted: false, + isDisplayConfirmDeleteModal: false, + isLoading: false, + }; + const defaultCasesStatus = { + countClosedCases: 0, + countOpenCases: 5, + fetchCasesStatus, + isError: false, + isLoading: true, + }; + const defaultUpdateCases = { + isUpdated: false, + isLoading: false, + isError: false, + dispatchResetIsUpdated, + updateBulkStatus, + }; + /* eslint-disable no-console */ + // Silence until enzyme fixed to use ReactTestUtils.act() + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + beforeEach(() => { + jest.resetAllMocks(); + useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); + useGetCasesMock.mockImplementation(() => defaultGetCases); + useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); + useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); + }); + + it('renders with unselectable rows', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy(); + expect(wrapper.find(EuiTableRow).first().prop('isSelectable')).toBeFalsy(); + }); + it('does not render modal if showCaseModal: false', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy(); + }); + it('onRowClick called when row is clicked', () => { + const wrapper = mount( + + + + ); + const firstRow = wrapper.find(EuiTableRow).first(); + firstRow.simulate('click'); + expect(onRowClick.mock.calls[0][0]).toEqual(basicCaseId); + }); + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + + + + ); + const modalClose = wrapper.find('.euiModal__closeIcon').first(); + modalClose.simulate('click'); + expect(onCloseCaseModal).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx index 9b2dfd1a5dd0b5..d2ca0f0cd02ee2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.tsx @@ -31,7 +31,7 @@ export const AllCasesModalComponent = ({ let modal; if (showCaseModal) { modal = ( - + {i18n.SELECT_CASE_TITLE} diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index ef39a522133366..8f13e4dd0cdcf7 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -193,7 +193,6 @@ export const updateUrlStateString = ({ if (urlKey === CONSTANTS.appQuery) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.query === '') { - console.log('1', history); return replaceStateInLocation({ history, pathName, @@ -205,7 +204,6 @@ export const updateUrlStateString = ({ } else if (urlKey === CONSTANTS.timerange && updateTimerange) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.global != null) { - console.log('2', history); return replaceStateInLocation({ history, pathName, @@ -217,7 +215,6 @@ export const updateUrlStateString = ({ } else if (urlKey === CONSTANTS.filters) { const queryState = decodeRisonUrlState(newUrlStateString); if (isEmpty(queryState)) { - console.log('3', history); return replaceStateInLocation({ history, pathName, @@ -229,7 +226,6 @@ export const updateUrlStateString = ({ } else if (urlKey === CONSTANTS.timeline) { const queryState = decodeRisonUrlState(newUrlStateString); if (queryState != null && queryState.id === '') { - console.log('4', history); return replaceStateInLocation({ history, pathName, @@ -239,7 +235,6 @@ export const updateUrlStateString = ({ }); } } - console.log('5', history); return search; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 089c4a4939a5b7..3649b5486d288e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -6,16 +6,21 @@ import { mount } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../common/mock'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER, + TestProviders, } from '../../../../common/mock'; import { createStore, State } from '../../../../common/store'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; +import { SiemPageName } from '../../../../app/types'; +import { setInsertTimeline } from '../../../store/timeline/actions'; +export { nextTick } from '../../../../../../../test_utils'; + +import { act } from 'react-dom/test-utils'; jest.mock('../../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../../common/lib/kibana'); @@ -30,24 +35,21 @@ jest.mock('../../../../common/components/utils'); width: mockedWidth, })); -jest.mock('react-redux', () => { - const originalModule = jest.requireActual('react-redux'); +const mockDispatch = jest.fn(); - return { - ...originalModule, - useSelector: jest.fn().mockReturnValue({ savedObjectId: '1' }), - }; -}); - -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useHistory: jest.fn(), - }; -}); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, + useSelector: jest.fn().mockReturnValue({ savedObjectId: '1', urlState: {} }), +})); +const mockHistoryPush = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); const usersViewing = ['elastic']; const defaultProps = { associateNote: jest.fn(), @@ -91,6 +93,9 @@ describe('Properties', () => { expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( false ); + expect( + wrapper.find('button[data-test-subj="attach-timeline-existing-case"]').prop('disabled') + ).toEqual(false); }); test('renders correctly draft timeline', () => { @@ -105,6 +110,9 @@ describe('Properties', () => { expect(wrapper.find('button[data-test-subj="attach-timeline-case"]').prop('disabled')).toEqual( true ); + expect( + wrapper.find('button[data-test-subj="attach-timeline-existing-case"]').prop('disabled') + ).toEqual(true); }); test('it renders an empty star icon when it is NOT a favorite', () => { @@ -284,4 +292,38 @@ describe('Properties', () => { expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(false); }); + + test('insert timeline - new case', () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); + wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click'); + + expect(mockHistoryPush).toBeCalledWith({ pathname: `/${SiemPageName.case}/create` }); + expect(mockDispatch).toBeCalledWith( + setInsertTimeline({ + timelineId: defaultProps.timelineId, + timelineSavedObjectId: '1', + timelineTitle: 'coolness', + }) + ); + }); + + test('insert timeline - existing case', async () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); + wrapper.find('[data-test-subj="attach-timeline-existing-case"]').first().simulate('click'); + + await act(async () => { + await Promise.resolve({}); + }); + expect(wrapper.find('[data-test-subj="all-cases-modal"]').exists()).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index ea4cb9198d02dd..8b29b10fd7e55a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -24,7 +24,6 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; -// import * as all from '../../../common/lib/kibana'; const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index 33b43cb9bb71b3..083c1605fc6b18 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -6,14 +6,17 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers'; import { - addTimeline, addHistory, addNote, addNoteToEvent, addProvider, + addTimeline, applyDeltaToColumnWidth, applyDeltaToWidth, applyKqlFilterQuery, + clearEventsDeleted, + clearEventsLoading, + clearSelected, createTimeline, dataProviderEdited, endTimelineSaving, @@ -21,12 +24,12 @@ import { removeColumn, removeProvider, setEventsDeleted, - clearEventsDeleted, setEventsLoading, - clearEventsLoading, + setFilters, + setInsertTimeline, setKqlFilterQueryDraft, + setSavedQueryId, setSelected, - clearSelected, showCallOutUnauthorizedMsg, showTimeline, startTimelineSaving, @@ -37,9 +40,11 @@ import { updateDataProviderExcluded, updateDataProviderKqlQuery, updateDescription, + updateEventType, updateHighlightedDropAndProviderId, updateIsFavorite, updateIsLive, + updateIsLoading, updateItemsPerPage, updateItemsPerPageOptions, updateKqlMode, @@ -50,10 +55,6 @@ import { updateTimeline, updateTitle, upsertColumn, - updateIsLoading, - setSavedQueryId, - setFilters, - updateEventType, setInsertTimeline, } from './actions'; import { addNewTimeline, From 22e9dca66226ca591cccc5c693c342645df8b986 Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Mon, 8 Jun 2020 16:03:47 -0600 Subject: [PATCH 7/8] fix add comment preview --- .../cases/components/add_comment/index.tsx | 32 +++++++++++++++++++ .../use_insert_timeline.tsx | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index a57fae8081bea4..a830b299d655b7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; import { CommentRequest } from '../../../../../case/common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; @@ -18,6 +19,12 @@ import { Form, useForm, UseField } from '../../../shared_imports'; import * as i18n from './translations'; import { schema } from './schema'; +import { + dispatchUpdateTimeline, + queryTimelineById, +} from '../../../timelines/components/open_timeline/helpers'; +import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions'; +import { useApolloClient } from '../../../common/utils/apollo_context'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -46,6 +53,8 @@ export const AddComment = React.memo( options: { stripEmptyFields: false }, schema, }); + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( form, 'comment' @@ -62,6 +71,28 @@ export const AddComment = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, [insertQuote]); + const handleTimelineClick = useCallback( + (timelineId: string) => { + queryTimelineById({ + apolloClient, + timelineId, + updateIsLoading: ({ + id: currentTimelineId, + isLoading: isLoadingTimeline, + }: { + id: string; + isLoading: boolean; + }) => + dispatch( + dispatchUpdateIsLoading({ id: currentTimelineId, isLoading: isLoadingTimeline }) + ), + updateTimeline: dispatchUpdateTimeline(dispatch), + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [apolloClient] + ); + const onSubmit = useCallback(async () => { const { isValid, data } = await form.submit(); if (isValid) { @@ -86,6 +117,7 @@ export const AddComment = React.memo( dataTestSubj: 'add-comment', placeholder: i18n.ADD_COMMENT_HELP_TEXT, onCursorPositionUpdate: handleCursorChange, + onClickTimeline: handleTimelineClick, bottomRightContent: ( (form: FormHook, fieldNa }); const handleOnTimelineChange = useCallback( (title: string, id: string | null) => { - const builtLink = `${basePath}/app/siem#/timelines?timeline=(id:'${id}',isOpen:!t)`; + const builtLink = `${basePath}/app/security_solution#/timelines?timeline=(id:'${id}',isOpen:!t)`; const currentValue = form.getFormData()[fieldName]; const newValue: string = [ currentValue.slice(0, cursorPosition.start), From 2a01edb56410052113052003c71f0d8a8e17791d Mon Sep 17 00:00:00 2001 From: stephmilovic Date: Tue, 9 Jun 2020 07:55:10 -0600 Subject: [PATCH 8/8] fix url --- .../timeline/insert_timeline_popover/use_insert_timeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx index 8101982f96210a..6269bc1b4a1a33 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx @@ -17,7 +17,7 @@ export const useInsertTimeline = (form: FormHook, fieldNa }); const handleOnTimelineChange = useCallback( (title: string, id: string | null) => { - const builtLink = `${basePath}/app/security_solution#/timelines?timeline=(id:'${id}',isOpen:!t)`; + const builtLink = `${basePath}/app/security#/timelines?timeline=(id:'${id}',isOpen:!t)`; const currentValue = form.getFormData()[fieldName]; const newValue: string = [ currentValue.slice(0, cursorPosition.start),