[Security Solution][Endpoint Responder] CallOut for offline endpoints (#135965)

This commit is contained in:
Candace Park 2022-07-11 15:14:21 -04:00 committed by GitHub
parent 000f39b4a0
commit f3eedfb565
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 8 deletions

View file

@ -46,6 +46,7 @@ export const ConsolePageOverlay = memo<ConsolePageOverlayProps>(
return {
pageTitle,
pageBody: body,
headerHasBottomBorder: false,
'data-test-subj': getTestId('layout'),
headerBackComponent: (
@ -74,7 +75,7 @@ export const ConsolePageOverlay = memo<ConsolePageOverlayProps>(
...(actions ?? []),
],
};
}, [actions, getTestId, handleCloseOverlayOnClick, isHidden, pageTitle]);
}, [actions, getTestId, handleCloseOverlayOnClick, isHidden, pageTitle, body]);
return (
<PageOverlay
@ -84,11 +85,7 @@ export const ConsolePageOverlay = memo<ConsolePageOverlayProps>(
paddingSize="l"
enableScrolling={false}
>
<PageLayout {...layoutProps}>
{body}
{console}
</PageLayout>
<PageLayout {...layoutProps}>{console}</PageLayout>
</PageOverlay>
);
}

View file

@ -39,6 +39,7 @@ const EuiPanelStyled = styled(EuiPanel)`
export type PageLayoutProps = PropsWithChildren<{
pageTitle?: ReactNode;
pageDescription?: ReactNode;
pageBody?: ReactNode;
actions?: ReactNode | ReactNode[];
headerHasBottomBorder?: boolean;
restrictWidth?: boolean | number | string;
@ -52,6 +53,7 @@ export const PageLayout = memo<PageLayoutProps>(
({
pageTitle,
pageDescription,
pageBody,
actions,
headerHasBottomBorder,
restrictWidth,
@ -75,7 +77,7 @@ export const PageLayout = memo<PageLayoutProps>(
};
}, []);
const bodyClassName = useMemo(() => {
const consoleBodyClassName = useMemo(() => {
return classnames({
'is-scrollable': scrollableBody,
'is-not-scrollable': !scrollableBody,
@ -132,7 +134,12 @@ export const PageLayout = memo<PageLayoutProps>(
</EuiFlexItem>
)}
<EuiFlexItem grow className={bodyClassName} data-test-subj={getTestId('body')}>
<EuiFlexItem grow={false}>{pageBody}</EuiFlexItem>
<EuiFlexItem
grow
className={consoleBodyClassName}
data-test-subj={getTestId('consoleBody')}
>
<div role="main" className="full-height">
{children}
</div>

View file

@ -0,0 +1,54 @@
/*
* 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 from 'react';
import type { HostInfo } from '../../../../common/endpoint/types';
import { HostStatus } from '../../../../common/endpoint/types';
import type { AppContextTestRender } from '../../../common/mock/endpoint';
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
import { useGetEndpointDetails } from '../../hooks/endpoint/use_get_endpoint_details';
import { mockEndpointDetailsApiResult } from '../../pages/endpoint_hosts/store/mock_endpoint_result_list';
import { OfflineCallout } from './offline_callout';
jest.mock('../../hooks/endpoint/use_get_endpoint_details');
const getEndpointDetails = useGetEndpointDetails as jest.Mock;
describe('Responder offline callout', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
let endpointDetails: HostInfo;
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
render = () => (renderResult = mockedContext.render(<OfflineCallout endpointId={'1234'} />));
endpointDetails = mockEndpointDetailsApiResult();
getEndpointDetails.mockReturnValue({ data: endpointDetails });
render();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be visible when endpoint is offline', () => {
getEndpointDetails.mockReturnValue({
data: { ...endpointDetails, host_status: HostStatus.OFFLINE },
});
render();
const callout = renderResult.queryByTestId('offlineCallout');
expect(callout).toBeTruthy();
});
it('should not be visible when endpoint is online', () => {
getEndpointDetails.mockReturnValue({
data: { ...endpointDetails, host_status: HostStatus.HEALTHY },
});
render();
const callout = renderResult.queryByTestId('offlineCallout');
expect(callout).toBeFalsy();
});
});

View file

@ -0,0 +1,55 @@
/*
* 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 } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { useGetEndpointDetails } from '../../hooks';
import { HostStatus } from '../../../../common/endpoint/types';
interface OfflineCalloutProps {
endpointId: string;
}
export const OfflineCallout = memo<OfflineCalloutProps>(({ endpointId }) => {
const { data: endpointDetails } = useGetEndpointDetails(endpointId, {
refetchInterval: 10000,
});
if (!endpointDetails) {
return null;
}
if (endpointDetails.host_status === HostStatus.OFFLINE) {
return (
<>
<EuiCallOut
iconType="offline"
color="warning"
data-test-subj="offlineCallout"
title={i18n.translate('xpack.securitySolution.responder.hostOffline.callout.title', {
defaultMessage: 'Host Offline',
})}
>
<p>
<FormattedMessage
id="xpack.securitySolution.responder.hostOffline.callout.body"
defaultMessage="The host {name} is offline, so its responses may be delayed. Pending commands will execute when the host reconnects."
values={{ name: <strong>{endpointDetails.metadata.host.name}</strong> }}
/>
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</>
);
}
return null;
});
OfflineCallout.displayName = 'OfflineCallout';

View file

@ -15,6 +15,7 @@ import {
import { useConsoleManager } from '../../components/console';
import type { HostMetadata } from '../../../../common/endpoint/types';
import { HeaderEndpointInfo } from '../../components/endpoint_responder/header_endpoint_info';
import { OfflineCallout } from '../../components/endpoint_responder/offline_callout';
type ShowEndpointResponseActionsConsole = (endpointMetadata: HostMetadata) => void;
@ -52,6 +53,7 @@ export const useWithShowEndpointResponder = (): ShowEndpointResponseActionsConso
TitleComponent: () => <HeaderEndpointInfo endpointId={endpointAgentId} />,
},
PageTitleComponent: () => <>{RESPONDER_PAGE_TITLE}</>,
PageBodyComponent: () => <OfflineCallout endpointId={endpointAgentId} />,
ActionComponents: [ActionLogButton],
})
.show();