From 1cd7e948ca718cfe9a8c57602d442f5973b8ed7a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 14 Jul 2021 12:10:52 -0400 Subject: [PATCH] [Security Solution] Fix UX for Success banner for Host Isolation (#105083) (#105607) Co-authored-by: Kevin Logan <56395104+kevinlog@users.noreply.github.com> --- .../action_completion_return_button.tsx | 36 ++++++ .../endpoint/host_isolation/index.ts | 1 + .../host_isolation/isolate_success.tsx | 110 +++++++++++------- .../components/host_isolation/index.tsx | 47 -------- .../components/host_isolation/isolate.tsx | 26 ++--- .../components/host_isolation/unisolate.tsx | 26 ++--- .../endpoint_isolate_flyout_panel.tsx | 17 ++- .../side_panel/event_details/index.tsx | 28 ++++- 8 files changed, 161 insertions(+), 130 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/action_completion_return_button.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/action_completion_return_button.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/action_completion_return_button.tsx new file mode 100644 index 00000000000000..af05999b2de265 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/action_completion_return_button.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiText } from '@elastic/eui'; + +export const ActionCompletionReturnButton = React.memo( + ({ onClick, buttonText }: { onClick: () => void; buttonText: string }) => { + const onClickCallback = useCallback(() => onClick(), [onClick]); + + return ( + <> + + + + + +

{buttonText}

+
+
+
+
+ + ); + } +); + +ActionCompletionReturnButton.displayName = 'ActionCompletionReturnButton'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts index bd8e23e3a4559f..24b94cd6212b7e 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts @@ -9,3 +9,4 @@ export * from './isolate_success'; export * from './isolate_form'; export * from './unisolate_form'; export * from './endpoint_host_isolation_status'; +export * from './action_completion_return_button'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx index 3459da068b2824..7f05e8eafac872 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx @@ -5,58 +5,84 @@ * 2.0. */ -import React, { memo, ReactNode } from 'react'; -import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import React, { memo, ReactNode, useMemo } from 'react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { GET_ISOLATION_SUCCESS_MESSAGE, GET_UNISOLATION_SUCCESS_MESSAGE } from './translations'; +import { useCasesFromAlerts } from '../../../../detections/containers/detection_engine/alerts/use_cases_from_alerts'; +import { CaseDetailsLink } from '../../../../common/components/links'; export interface EndpointIsolateSuccessProps { hostName: string; + alertId?: string; isolateAction?: 'isolateHost' | 'unisolateHost'; - completeButtonLabel: string; - onComplete: () => void; additionalInfo?: ReactNode; } +const CasesAdditionalInfo: React.FC<{ alertIdForCase: string }> = ({ alertIdForCase }) => { + const { casesInfo } = useCasesFromAlerts({ alertId: alertIdForCase }); + + const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); + + const casesList = useMemo( + () => + casesInfo.map((caseInfo, index) => { + return ( +
  • + + + +
  • + ); + }), + [casesInfo] + ); + + return ( + <> + {caseCount > 0 && ( + <> + +

    + +

    +
    + + + + + )} + + ); +}; + export const EndpointIsolateSuccess = memo( - ({ - hostName, - isolateAction = 'isolateHost', - onComplete, - completeButtonLabel, - additionalInfo, - }) => { + ({ hostName, alertId, isolateAction = 'isolateHost', additionalInfo }) => { return ( - <> - - {additionalInfo} - - - - - -

    {completeButtonLabel}

    -
    -
    -
    -
    - + + {alertId !== undefined ? CasesAdditionalInfo({ alertIdForCase: alertId }) : additionalInfo} + ); } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx index 36443cc91f4e84..acbae367fe75e2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx @@ -7,11 +7,8 @@ import React, { useMemo } from 'react'; import { find } from 'lodash/fp'; -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { Maybe } from '../../../../../observability/common/typings'; import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts'; -import { CaseDetailsLink } from '../../../common/components/links'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { IsolateHost } from './isolate'; import { UnisolateHost } from './unisolate'; @@ -45,53 +42,10 @@ export const HostIsolationPanel = React.memo( const { casesInfo } = useCasesFromAlerts({ alertId }); - // Cases related components to be used in both isolate and unisolate actions from the alert details flyout entry point - const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); - - const casesList = useMemo( - () => - casesInfo.map((caseInfo, index) => { - return ( -
  • - - - -
  • - ); - }), - [casesInfo] - ); - - const associatedCases = useMemo(() => { - if (caseCount > 0) { - return ( - <> - -

    - -

    -
    - -
      {casesList}
    -
    - - ); - } - }, [caseCount, casesList]); - return isolateAction === 'isolateHost' ? ( void; successCallback?: () => void; @@ -60,20 +58,14 @@ export const IsolateHost = React.memo( const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); - const hostIsolatedSuccess = useMemo(() => { + const hostIsolatedSuccessButton = useMemo(() => { return ( - <> - - - + ); - }, [backToAlertDetails, hostName, cases]); + }, [backToAlertDetails]); const hostNotIsolated = useMemo(() => { return ( @@ -108,7 +100,7 @@ export const IsolateHost = React.memo( caseCount, ]); - return isIsolated ? hostIsolatedSuccess : hostNotIsolated; + return isIsolated ? hostIsolatedSuccessButton : hostNotIsolated; } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx index 2b810dc16eec80..5cc862694bef83 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx @@ -5,14 +5,14 @@ * 2.0. */ -import React, { useMemo, useState, useCallback, ReactNode } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations'; import { EndpointIsolatedFormProps, - EndpointIsolateSuccess, EndpointUnisolateForm, + ActionCompletionReturnButton, } from '../../../common/components/endpoint/host_isolation'; import { useHostUnisolation } from '../../containers/detection_engine/alerts/use_host_unisolation'; import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types'; @@ -21,14 +21,12 @@ export const UnisolateHost = React.memo( ({ endpointId, hostName, - cases, casesInfo, cancelCallback, successCallback, }: { endpointId: string; hostName: string; - cases: ReactNode; casesInfo: CasesFromAlertsResponse; cancelCallback: () => void; successCallback?: () => void; @@ -60,20 +58,14 @@ export const UnisolateHost = React.memo( const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]); - const hostUnisolatedSuccess = useMemo(() => { + const hostUnisolatedSuccessButton = useMemo(() => { return ( - <> - - - + ); - }, [backToAlertDetails, hostName, cases]); + }, [backToAlertDetails]); const hostNotUnisolated = useMemo(() => { return ( @@ -108,7 +100,7 @@ export const UnisolateHost = React.memo( caseCount, ]); - return isUnIsolated ? hostUnisolatedSuccess : hostNotUnisolated; + return isUnIsolated ? hostUnisolatedSuccessButton : hostNotUnisolated; } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_isolate_flyout_panel.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_isolate_flyout_panel.tsx index 289c1efeab0411..527189a3ef394c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_isolate_flyout_panel.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_isolate_flyout_panel.tsx @@ -9,8 +9,8 @@ import React, { memo, useCallback, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; -import { i18n } from '@kbn/i18n'; import { EuiForm } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { HostMetadata } from '../../../../../../../common/endpoint/types'; import { BackToEndpointDetailsFlyoutSubHeader } from './back_to_endpoint_details_flyout_subheader'; import { @@ -18,6 +18,7 @@ import { EndpointIsolateForm, EndpointIsolateSuccess, EndpointUnisolateForm, + ActionCompletionReturnButton, } from '../../../../../../common/components/endpoint/host_isolation'; import { FlyoutBodyNoTopPadding } from './flyout_body_no_top_padding'; import { getEndpointDetailsPath } from '../../../../../common/routing'; @@ -88,16 +89,20 @@ export const EndpointIsolationFlyoutPanel = memo<{ <> + {wasSuccessful && ( + + )} {wasSuccessful ? ( - ) : ( = ({ const [isHostIsolationPanelOpen, setIsHostIsolationPanel] = useState(false); - const [isolateAction, setIsolateAction] = useState('isolateHost'); + const [isolateAction, setIsolateAction] = useState<'isolateHost' | 'unisolateHost'>( + 'isolateHost' + ); + + const [isIsolateActionSuccessBannerVisible, setIsIsolateActionSuccessBannerVisible] = useState( + false + ); const showAlertDetails = useCallback(() => { setIsHostIsolationPanel(false); + setIsIsolateActionSuccessBannerVisible(false); }, []); const { isAllowed: isIsolationAllowed } = useIsolationPrivileges(); @@ -114,6 +122,16 @@ const EventDetailsPanelComponent: React.FC = ({ return findAgentVersion ? findAgentVersion[0] : ''; }, [detailsData]); + const alertId = useMemo(() => { + const findAlertId = find({ category: '_id', field: '_id' }, detailsData)?.values; + return findAlertId ? findAlertId[0] : ''; + }, [detailsData]); + + const hostName = useMemo(() => { + const findHostName = find({ category: 'host', field: 'host.name' }, detailsData)?.values; + return findHostName ? findHostName[0] : ''; + }, [detailsData]); + const isolationSupported = isIsolationSupported({ osName: hostOsFamily, version: agentVersion, @@ -142,6 +160,7 @@ const EventDetailsPanelComponent: React.FC = ({ const caseDetailsRefresh = useWithCaseDetailsRefresh(); const handleIsolationActionSuccess = useCallback(() => { + setIsIsolateActionSuccessBannerVisible(true); // If a case details refresh ref is defined, then refresh actions and comments if (caseDetailsRefresh) { caseDetailsRefresh.refreshUserActionsAndComments(); @@ -161,6 +180,13 @@ const EventDetailsPanelComponent: React.FC = ({ )} + {isIsolateActionSuccessBannerVisible && ( + + )} {isHostIsolationPanelOpen ? (