[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 React, { useMemo, useState, useCallback } from 'react';
import { find } from 'lodash/fp'; import { find } from 'lodash/fp';
import { import { EuiText, EuiSpacer } from '@elastic/eui';
EuiCallOut,
EuiTitle,
EuiText,
EuiTextArea,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation'; import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation';
import { import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations';
CANCEL,
CASES_ASSOCIATED_WITH_ALERT,
COMMENT,
COMMENT_PLACEHOLDER,
CONFIRM,
RETURN_TO_ALERT_DETAILS,
} from './translations';
import { Maybe } from '../../../../../observability/common/typings'; import { Maybe } from '../../../../../observability/common/typings';
import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts'; import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts';
import { CaseDetailsLink } from '../../../common/components/links'; import { CaseDetailsLink } from '../../../common/components/links';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import {
EndpointIsolatedFormProps,
EndpointIsolateForm,
EndpointIsolateSuccess,
} from '../../../common/components/endpoint/host_isolation';
export const HostIsolationPanel = React.memo( export const HostIsolationPanel = React.memo(
({ ({
@ -76,6 +63,11 @@ export const HostIsolationPanel = React.memo(
const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]);
const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback(
({ comment: newComment }) => setComment(newComment),
[]
);
const casesList = useMemo( const casesList = useMemo(
() => () =>
caseIds.map((id, index) => { caseIds.map((id, index) => {
@ -100,43 +92,29 @@ export const HostIsolationPanel = React.memo(
return ( return (
<> <>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<EuiCallOut <EndpointIsolateSuccess
iconType="check" hostName={hostName}
color="success" completeButtonLabel={RETURN_TO_ALERT_DETAILS}
title={i18n.translate( onComplete={backToAlertDetails}
'xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.title', additionalInfo={
{ caseCount > 0 && (
defaultMessage: 'Host Isolation on {hostname} successfully submitted', <>
values: { hostname: hostName }, <EuiText size="s">
} <p>
)} <FormattedMessage
> id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases"
{caseCount > 0 && ( defaultMessage="This action has been attached to the following {caseCount, plural, one {case} other {cases}}:"
<> values={{ caseCount }}
<EuiText size="s"> />
<p> </p>
<FormattedMessage </EuiText>
id="xpack.securitySolution.endpoint.hostIsolation.successfulIsolation.cases" <EuiText size="s">
defaultMessage="This action has been attached to the following {caseCount, plural, one {case} other {cases}}:" <ul>{casesList}</ul>
values={{ caseCount }} </EuiText>
/> </>
</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>
</> </>
); );
}, [backToAlertDetails, hostName, caseCount, casesList]); }, [backToAlertDetails, hostName, caseCount, casesList]);
@ -145,13 +123,18 @@ export const HostIsolationPanel = React.memo(
return ( return (
<> <>
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<EuiText size="s"> <EndpointIsolateForm
<p> hostName={hostName}
onCancel={backToAlertDetails}
onConfirm={confirmHostIsolation}
onChange={handleIsolateFormChange}
comment={comment}
isLoading={loading}
messageAppend={
<FormattedMessage <FormattedMessage
id="xpack.securitySolution.endpoint.hostIsolation.isolateThisHost" id="xpack.securitySolution.detections.hostIsolation.impactedCases"
defaultMessage="Isolate host {hostname} from network. This action will be added to the {cases}." defaultMessage="This action will be added to the {cases}."
values={{ values={{
hostname: <b>{hostName}</b>,
cases: ( cases: (
<b> <b>
{caseCount} {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, hostName,
backToAlertDetails,
confirmHostIsolation,
handleIsolateFormChange,
comment,
loading, loading,
caseCount, caseCount,
alertRule,
]); ]);
return isIsolated ? hostIsolated : hostNotIsolated; 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 => export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string =>
i18n.translate( i18n.translate(
'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert', 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert',