[Security Solution][Endpoint] Refactor Host Isolation component used in Isolate use case (#100159)

* EndpointHostIsolateForm component
* Refactor Detections Host isolation flyout to use isolateform
This commit is contained in:
Paul Tavares 2021-05-17 11:16:50 -04:00 committed by GitHub
parent 8d85d72fef
commit 532a33b051
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 219 additions and 107 deletions

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './isolate_success';
export * from './isolate_form';

View file

@ -0,0 +1,88 @@
/*
* 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, { ChangeEventHandler, memo, ReactNode, useCallback } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
EuiTextArea,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM } from './translations';
export interface EndpointIsolatedFormProps {
hostName: string;
onCancel: () => void;
onConfirm: () => void;
onChange: (changes: { comment: string }) => void;
comment?: string;
/** Any additional message to be appended to the default one */
messageAppend?: ReactNode;
/** If true, then `Confirm` and `Cancel` buttons will be disabled, and `Confirm` button will loading loading style */
isLoading?: boolean;
}
export const EndpointIsolateForm = memo<EndpointIsolatedFormProps>(
({ hostName, onCancel, onConfirm, onChange, comment = '', messageAppend, isLoading = false }) => {
const handleCommentChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
(event) => {
onChange({ comment: event.target.value });
},
[onChange]
);
return (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost"
defaultMessage="Isolate host {hostName} from network."
values={{ hostName: <b>{hostName}</b> }}
/>{' '}
{messageAppend}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiTitle size="xs">
<h4>{COMMENT}</h4>
</EuiTitle>
<EuiTextArea
data-test-subj="host_isolation_comment"
fullWidth
placeholder={COMMENT_PLACEHOLDER}
value={comment}
onChange={handleCommentChange}
/>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onCancel} disabled={isLoading}>
{CANCEL}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={onConfirm} disabled={isLoading} isLoading={isLoading}>
{CONFIRM}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
);
EndpointIsolateForm.displayName = 'EndpointIsolateForm';

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, ReactNode } from 'react';
import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { GET_SUCCESS_MESSAGE } from './translations';
export interface EndpointIsolateSuccessProps {
hostName: string;
completeButtonLabel: string;
onComplete: () => void;
additionalInfo?: ReactNode;
}
export const EndpointIsolateSuccess = memo<EndpointIsolateSuccessProps>(
({ hostName, onComplete, completeButtonLabel, additionalInfo }) => {
return (
<>
<EuiCallOut iconType="check" color="success" title={GET_SUCCESS_MESSAGE(hostName)}>
{additionalInfo}
</EuiCallOut>
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty flush="right" onClick={onComplete}>
<EuiText size="s">
<p>{completeButtonLabel}</p>
</EuiText>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
);
EndpointIsolateSuccess.displayName = 'EndpointIsolateSuccess';

View file

@ -0,0 +1,31 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const CANCEL = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.cancel', {
defaultMessage: 'Cancel',
});
export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.confirm', {
defaultMessage: 'Confirm',
});
export const COMMENT = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.comment', {
defaultMessage: 'Comment',
});
export const COMMENT_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.comment.placeholder',
{ defaultMessage: 'You may leave an optional note here.' }
);
export const GET_SUCCESS_MESSAGE = (hostName: string) =>
i18n.translate('xpack.securitySolution.endpoint.hostIsolation.successfulMessage', {
defaultMessage: 'Host Isolation on {hostName} successfully submitted',
values: { hostName },
});

View file

@ -7,32 +7,19 @@
import React, { useMemo, useState, useCallback } from 'react';
import { find } from 'lodash/fp';
import {
EuiCallOut,
EuiTitle,
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiText, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
import {
CANCEL,
CASES_ASSOCIATED_WITH_ALERT,
COMMENT,
COMMENT_PLACEHOLDER,
CONFIRM,
RETURN_TO_ALERT_DETAILS,
} from './translations';
import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations';
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 {
EndpointIsolatedFormProps,
EndpointIsolateForm,
EndpointIsolateSuccess,
} from '../../../common/components/endpoint/host_isolation';
export const HostIsolationPanel = React.memo(
({
@ -76,6 +63,11 @@ export const HostIsolationPanel = React.memo(
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback(
({ comment: newComment }) => setComment(newComment),
[]
);
const casesList = useMemo(
() =>
caseIds.map((id, index) => {
@ -100,43 +92,29 @@ export const HostIsolationPanel = React.memo(
return (
<>
<EuiSpacer size="m" />
<EuiCallOut
iconType="check"
color="success"
title={i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.title',
{
defaultMessage: 'Host Isolation on {hostname} successfully submitted',
values: { hostname: hostName },
}
)}
>
{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>
</>
)}
</EuiCallOut>
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty flush="right" onClick={backToAlertDetails}>
<EuiText size="s">
<p>{RETURN_TO_ALERT_DETAILS}</p>
</EuiText>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EndpointIsolateSuccess
hostName={hostName}
completeButtonLabel={RETURN_TO_ALERT_DETAILS}
onComplete={backToAlertDetails}
additionalInfo={
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>
</>
)
}
/>
</>
);
}, [backToAlertDetails, hostName, caseCount, casesList]);
@ -145,13 +123,18 @@ export const HostIsolationPanel = React.memo(
return (
<>
<EuiSpacer size="m" />
<EuiText size="s">
<p>
<EndpointIsolateForm
hostName={hostName}
onCancel={backToAlertDetails}
onConfirm={confirmHostIsolation}
onChange={handleIsolateFormChange}
comment={comment}
isLoading={loading}
messageAppend={
<FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost"
defaultMessage="Isolate host {hostname} from network. This action will be added to the {cases}."
id="xpack.securitySolution.detections.hostIsolation.impactedCases"
defaultMessage="This action will be added to the {cases}."
values={{
hostname: <b>{hostName}</b>,
cases: (
<b>
{caseCount}
@ -161,42 +144,19 @@ export const HostIsolationPanel = React.memo(
),
}}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiTitle size="xs">
<h4>{COMMENT}</h4>
</EuiTitle>
<EuiTextArea
data-test-subj="host_isolation_comment"
fullWidth={true}
placeholder={COMMENT_PLACEHOLDER}
value={comment}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
setComment(event.target.value)
}
/>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={backToAlertDetails}>{CANCEL}</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill onClick={confirmHostIsolation} isLoading={loading}>
{CONFIRM}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}, [
alertRule,
backToAlertDetails,
comment,
confirmHostIsolation,
hostName,
backToAlertDetails,
confirmHostIsolation,
handleIsolateFormChange,
comment,
loading,
caseCount,
alertRule,
]);
return isIsolated ? hostIsolated : hostNotIsolated;

View file

@ -14,23 +14,6 @@ export const ISOLATE_HOST = i18n.translate(
}
);
export const COMMENT = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.comment', {
defaultMessage: 'Comment',
});
export const COMMENT_PLACEHOLDER = i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.comment.placeholder',
{ defaultMessage: 'You may leave an optional note here.' }
);
export const CANCEL = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.cancel', {
defaultMessage: 'Cancel',
});
export const CONFIRM = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.confirm', {
defaultMessage: 'Confirm',
});
export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string =>
i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert',