mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Fleet][Integrations] Add callout to Microsoft Defender integrations showing support for bi-directional response actions (#207861)
## Summary - Add dismissible callout indicating support for bi-directional response actions to the following integration: Overview page: - Microsoft Defender for Endpoint - Microsoft M365 Defender
This commit is contained in:
parent
a69236d048
commit
cc38fbea29
3 changed files with 118 additions and 79 deletions
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { type RenderResult } from '@testing-library/react';
|
||||
import { type RenderResult, fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import type { FleetStartServices } from '../../../../../../..';
|
||||
|
||||
import { createFleetTestRendererMock } from '../../../../../../../mock';
|
||||
|
||||
|
@ -18,33 +20,61 @@ import {
|
|||
jest.mock('react-use/lib/useLocalStorage');
|
||||
|
||||
describe('BidirectionalIntegrationsBanner', () => {
|
||||
let formProps: BidirectionalIntegrationsBannerProps;
|
||||
let componentProps: BidirectionalIntegrationsBannerProps;
|
||||
let renderResult: RenderResult;
|
||||
let render: () => RenderResult;
|
||||
let storageMock: jest.Mocked<FleetStartServices['storage']>;
|
||||
|
||||
beforeEach(() => {
|
||||
formProps = {
|
||||
onDismiss: jest.fn(),
|
||||
componentProps = { integrationPackageName: 'sentinel_one' };
|
||||
|
||||
const testRunner = createFleetTestRendererMock();
|
||||
|
||||
storageMock = testRunner.startServices.storage;
|
||||
|
||||
render = () => {
|
||||
renderResult = testRunner.render(<BidirectionalIntegrationsBanner {...componentProps} />);
|
||||
return renderResult;
|
||||
};
|
||||
|
||||
const renderer = createFleetTestRendererMock();
|
||||
|
||||
renderResult = renderer.render(<BidirectionalIntegrationsBanner {...formProps} />);
|
||||
});
|
||||
|
||||
it('should render bidirectional integrations banner', () => {
|
||||
render();
|
||||
expect(renderResult.getByTestId('bidirectionalIntegrationsCallout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should contain a link to documentation', () => {
|
||||
render();
|
||||
const docLink = renderResult.getByTestId('bidirectionalIntegrationDocLink');
|
||||
|
||||
expect(docLink).toBeInTheDocument();
|
||||
expect(docLink.getAttribute('href')).toContain('third-party-actions.html');
|
||||
});
|
||||
|
||||
it('should call `onDismiss` callback when user clicks dismiss', () => {
|
||||
renderResult.getByTestId('euiDismissCalloutButton').click();
|
||||
it('should remove the callout when the dismiss button is clicked', async () => {
|
||||
render();
|
||||
fireEvent.click(renderResult.getByTestId('euiDismissCalloutButton'));
|
||||
|
||||
expect(formProps.onDismiss).toBeCalled();
|
||||
await waitFor(() => {
|
||||
expect(storageMock.store.setItem).toHaveBeenCalledWith(
|
||||
'fleet.showSOReponseSupportBanner',
|
||||
'false'
|
||||
);
|
||||
expect(renderResult.queryByTestId('bidirectionalIntegrationsCallout')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render nothing if integration is not supported', () => {
|
||||
componentProps.integrationPackageName = 'foo';
|
||||
render();
|
||||
|
||||
expect(renderResult.queryByTestId('bidirectionalIntegrationsCallout')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render nothing if user had dismissed the callout in the past', () => {
|
||||
(storageMock.store.getItem as jest.Mock).mockReturnValue('false');
|
||||
render();
|
||||
|
||||
expect(renderResult.queryByTestId('bidirectionalIntegrationsCallout')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,12 +5,26 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiCallOut, EuiLink, EuiTextColor } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
|
||||
import type { FleetStartServices } from '../../../../../../..';
|
||||
|
||||
/**
|
||||
* A list of Integration `package.name`'s that support security's bi-directional response actions
|
||||
* along with its corresponding storage (local storage) key for persisting the user's dismissal of
|
||||
* the callout
|
||||
*/
|
||||
const SUPPORTED_INTEGRATIONS_STORAGE_KEY: Readonly<Record<string, string>> = Object.freeze({
|
||||
sentinel_one: 'fleet.showSOReponseSupportBanner',
|
||||
crowdstrike: 'fleet.showCSResponseSupportBanner',
|
||||
microsoft_defender_endpoint: 'fleet.showMSDefenderResponseSupportBanner',
|
||||
m365_defender: 'fleet.showMSDefenderResponseSupportBanner', // Same key as the one above
|
||||
});
|
||||
|
||||
const AccentCallout = styled(EuiCallOut)`
|
||||
.euiCallOutHeader__title {
|
||||
color: ${(props) => props.theme.eui.euiColorAccent};
|
||||
|
@ -19,47 +33,69 @@ const AccentCallout = styled(EuiCallOut)`
|
|||
`;
|
||||
|
||||
export interface BidirectionalIntegrationsBannerProps {
|
||||
onDismiss: () => void;
|
||||
integrationPackageName: string;
|
||||
}
|
||||
export const BidirectionalIntegrationsBanner = memo<BidirectionalIntegrationsBannerProps>(
|
||||
({ onDismiss }) => {
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
const bannerTitle = (
|
||||
<EuiTextColor color="accent">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.bidirectionalIntegrationsBanner.title"
|
||||
defaultMessage={'NEW: Response enabled integration'}
|
||||
/>
|
||||
</EuiTextColor>
|
||||
({ integrationPackageName }) => {
|
||||
const { docLinks, storage } = useKibana<FleetStartServices>().services;
|
||||
const storageKey = SUPPORTED_INTEGRATIONS_STORAGE_KEY[integrationPackageName];
|
||||
const [showBanner, setShowBanner] = useState(
|
||||
storageKey ? storage.get(storageKey) ?? true : false
|
||||
);
|
||||
|
||||
const onDismissHandler = useCallback(() => {
|
||||
setShowBanner(false);
|
||||
|
||||
if (storageKey) {
|
||||
storage.set(storageKey, false);
|
||||
}
|
||||
}, [storage, storageKey]);
|
||||
|
||||
const bannerTitle = useMemo(
|
||||
() => (
|
||||
<EuiTextColor color="accent">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.bidirectionalIntegrationsBanner.title"
|
||||
defaultMessage={'NEW: Response enabled integration'}
|
||||
/>
|
||||
</EuiTextColor>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
if (!storageKey || !showBanner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccentCallout
|
||||
title={bannerTitle}
|
||||
iconType="cheer"
|
||||
onDismiss={onDismiss}
|
||||
data-test-subj={'bidirectionalIntegrationsCallout'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.bidirectionalIntegrationsBanner.body"
|
||||
defaultMessage="Orchestrate response actions across endpoint vendors with bidirectional integrations. {learnmore}."
|
||||
values={{
|
||||
learnmore: (
|
||||
<EuiLink
|
||||
href={docLinks?.links.securitySolution.bidirectionalIntegrations}
|
||||
target="_blank"
|
||||
data-test-subj="bidirectionalIntegrationDocLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.bidirectionalIntegrations.doc.link"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</AccentCallout>
|
||||
<>
|
||||
<AccentCallout
|
||||
title={bannerTitle}
|
||||
iconType="cheer"
|
||||
onDismiss={onDismissHandler}
|
||||
data-test-subj={'bidirectionalIntegrationsCallout'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.bidirectionalIntegrationsBanner.body"
|
||||
defaultMessage="Orchestrate response actions across endpoint vendors with bidirectional integrations. {learnmore}."
|
||||
values={{
|
||||
learnmore: (
|
||||
<EuiLink
|
||||
href={docLinks?.links.securitySolution.bidirectionalIntegrations}
|
||||
target="_blank"
|
||||
data-test-subj="bidirectionalIntegrationDocLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.bidirectionalIntegrations.doc.link"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</AccentCallout>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -175,8 +175,6 @@ export const OverviewPage: React.FC<Props> = memo(
|
|||
const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId);
|
||||
const isPrerelease = isPackagePrerelease(packageInfo.version);
|
||||
const isElasticDefend = packageInfo.name === 'endpoint';
|
||||
const isSentinelOne = packageInfo.name === 'sentinel_one';
|
||||
const isCrowdStrike = packageInfo.name === 'crowdstrike';
|
||||
const [markdown, setMarkdown] = useState<string | undefined>(undefined);
|
||||
const [selectedItemId, setSelectedItem] = useState<string | undefined>(undefined);
|
||||
const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false);
|
||||
|
@ -301,27 +299,11 @@ export const OverviewPage: React.FC<Props> = memo(
|
|||
const [showAVCBanner, setShowAVCBanner] = useState(
|
||||
storage.get('securitySolution.showAvcBanner') ?? true
|
||||
);
|
||||
const [showCSResponseSupportBanner, setShowCSResponseSupportBanner] = useState(
|
||||
storage.get('fleet.showCSResponseSupportBanner') ?? true
|
||||
);
|
||||
const [showSOReponseSupportBanner, setShowSOResponseSupportBanner] = useState(
|
||||
storage.get('fleet.showSOReponseSupportBanner') ?? true
|
||||
);
|
||||
const onAVCBannerDismiss = useCallback(() => {
|
||||
setShowAVCBanner(false);
|
||||
storage.set('securitySolution.showAvcBanner', false);
|
||||
}, [storage]);
|
||||
|
||||
const onCSResponseSupportBannerDismiss = useCallback(() => {
|
||||
setShowCSResponseSupportBanner(false);
|
||||
storage.set('fleet.showCSResponseSupportBanner', false);
|
||||
}, [storage]);
|
||||
|
||||
const onSOResponseSupportBannerDismiss = useCallback(() => {
|
||||
setShowSOResponseSupportBanner(false);
|
||||
storage.set('fleet.showSOReponseSupportBanner', false);
|
||||
}, [storage]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="flexStart" data-test-subj="epm.OverviewPage">
|
||||
<SideBar grow={2}>
|
||||
|
@ -342,18 +324,9 @@ export const OverviewPage: React.FC<Props> = memo(
|
|||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
{isCrowdStrike && showCSResponseSupportBanner && (
|
||||
<>
|
||||
<BidirectionalIntegrationsBanner onDismiss={onCSResponseSupportBannerDismiss} />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
{isSentinelOne && showSOReponseSupportBanner && (
|
||||
<>
|
||||
<BidirectionalIntegrationsBanner onDismiss={onSOResponseSupportBannerDismiss} />
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<BidirectionalIntegrationsBanner integrationPackageName={packageInfo.name} />
|
||||
|
||||
<CloudPostureThirdPartySupportCallout packageInfo={packageInfo} />
|
||||
{isPrerelease && (
|
||||
<PrereleaseCallout
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue