Skip to content

Commit

Permalink
[Security Solution] Fix UX for Success banner for Host Isolation (#10…
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinlog authored Jul 14, 2021
1 parent 509026d commit dd081ba
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="right"
onClick={onClickCallback}
data-test-subj="hostIsolateSuccessCompleteButton"
>
<EuiText size="s">
<p>{buttonText}</p>
</EuiText>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
);

ActionCompletionReturnButton.displayName = 'ActionCompletionReturnButton';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<li key={caseInfo.id}>
<CaseDetailsLink detailName={caseInfo.id}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.placeholderCase"
defaultMessage="{caseName}"
values={{ caseName: caseInfo.title }}
/>
</CaseDetailsLink>
</li>
);
}),
[casesInfo]
);

return (
<>
{caseCount > 0 && (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases"
defaultMessage="This action has been attached to the following {caseCount, plural, one {case} other {cases}}:"
values={{ caseCount }}
/>
</p>
</EuiText>
<EuiText size="s">
<ul>{casesList}</ul>
</EuiText>
</>
)}
</>
);
};

export const EndpointIsolateSuccess = memo<EndpointIsolateSuccessProps>(
({
hostName,
isolateAction = 'isolateHost',
onComplete,
completeButtonLabel,
additionalInfo,
}) => {
({ hostName, alertId, isolateAction = 'isolateHost', additionalInfo }) => {
return (
<>
<EuiCallOut
iconType="check"
color="success"
title={
isolateAction === 'isolateHost'
? GET_ISOLATION_SUCCESS_MESSAGE(hostName)
: GET_UNISOLATION_SUCCESS_MESSAGE(hostName)
}
data-test-subj={
isolateAction === 'isolateHost'
? 'hostIsolateSuccessMessage'
: 'hostUnisolateSuccessMessage'
}
>
{additionalInfo}
</EuiCallOut>
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
flush="right"
onClick={onComplete}
data-test-subj="hostIsolateSuccessCompleteButton"
>
<EuiText size="s">
<p>{completeButtonLabel}</p>
</EuiText>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</>
<EuiCallOut
iconType="check"
color="success"
title={
isolateAction === 'isolateHost'
? GET_ISOLATION_SUCCESS_MESSAGE(hostName)
: GET_UNISOLATION_SUCCESS_MESSAGE(hostName)
}
data-test-subj={
isolateAction === 'isolateHost'
? 'hostIsolateSuccessMessage'
: 'hostUnisolateSuccessMessage'
}
>
{alertId !== undefined ? CasesAdditionalInfo({ alertIdForCase: alertId }) : additionalInfo}
</EuiCallOut>
);
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<li key={caseInfo.id}>
<CaseDetailsLink detailName={caseInfo.id}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.placeholderCase"
defaultMessage="{caseName}"
values={{ caseName: caseInfo.title }}
/>
</CaseDetailsLink>
</li>
);
}),
[casesInfo]
);

const associatedCases = useMemo(() => {
if (caseCount > 0) {
return (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases"
defaultMessage="This action has been attached to the following {caseCount, plural, one {case} other {cases}}:"
values={{ caseCount }}
/>
</p>
</EuiText>
<EuiText size="s">
<ul>{casesList}</ul>
</EuiText>
</>
);
}
}, [caseCount, casesList]);

return isolateAction === 'isolateHost' ? (
<IsolateHost
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
casesInfo={casesInfo}
cancelCallback={cancelCallback}
successCallback={successCallback}
Expand All @@ -100,7 +54,6 @@ export const HostIsolationPanel = React.memo(
<UnisolateHost
endpointId={endpointId}
hostName={hostName}
cases={associatedCases}
casesInfo={casesInfo}
cancelCallback={cancelCallback}
successCallback={successCallback}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,28 @@
* 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 { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations';
import {
EndpointIsolatedFormProps,
EndpointIsolateForm,
EndpointIsolateSuccess,
ActionCompletionReturnButton,
} from '../../../common/components/endpoint/host_isolation';
import { CasesFromAlertsResponse } from '../../containers/detection_engine/alerts/types';

export const IsolateHost = React.memo(
({
endpointId,
hostName,
cases,
casesInfo,
cancelCallback,
successCallback,
}: {
endpointId: string;
hostName: string;
cases: ReactNode;
casesInfo: CasesFromAlertsResponse;
cancelCallback: () => void;
successCallback?: () => void;
Expand Down Expand Up @@ -60,20 +58,14 @@ export const IsolateHost = React.memo(

const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);

const hostIsolatedSuccess = useMemo(() => {
const hostIsolatedSuccessButton = useMemo(() => {
return (
<>
<EuiSpacer size="m" />
<EndpointIsolateSuccess
hostName={hostName}
isolateAction="isolateHost"
completeButtonLabel={RETURN_TO_ALERT_DETAILS}
onComplete={backToAlertDetails}
additionalInfo={cases}
/>
</>
<ActionCompletionReturnButton
onClick={backToAlertDetails}
buttonText={RETURN_TO_ALERT_DETAILS}
/>
);
}, [backToAlertDetails, hostName, cases]);
}, [backToAlertDetails]);

const hostNotIsolated = useMemo(() => {
return (
Expand Down Expand Up @@ -108,7 +100,7 @@ export const IsolateHost = React.memo(
caseCount,
]);

return isIsolated ? hostIsolatedSuccess : hostNotIsolated;
return isIsolated ? hostIsolatedSuccessButton : hostNotIsolated;
}
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -60,20 +58,14 @@ export const UnisolateHost = React.memo(

const caseCount: number = useMemo(() => casesInfo.length, [casesInfo]);

const hostUnisolatedSuccess = useMemo(() => {
const hostUnisolatedSuccessButton = useMemo(() => {
return (
<>
<EuiSpacer size="m" />
<EndpointIsolateSuccess
hostName={hostName}
isolateAction="unisolateHost"
completeButtonLabel={RETURN_TO_ALERT_DETAILS}
onComplete={backToAlertDetails}
additionalInfo={cases}
/>
</>
<ActionCompletionReturnButton
onClick={backToAlertDetails}
buttonText={RETURN_TO_ALERT_DETAILS}
/>
);
}, [backToAlertDetails, hostName, cases]);
}, [backToAlertDetails]);

const hostNotUnisolated = useMemo(() => {
return (
Expand Down Expand Up @@ -108,7 +100,7 @@ export const UnisolateHost = React.memo(
caseCount,
]);

return isUnIsolated ? hostUnisolatedSuccess : hostNotUnisolated;
return isUnIsolated ? hostUnisolatedSuccessButton : hostNotUnisolated;
}
);

Expand Down
Loading

0 comments on commit dd081ba

Please sign in to comment.