diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 951bdbb531d60..ba74061778b6d 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -462,6 +462,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D }, securitySolution: { artifactControl: `${SECURITY_SOLUTION_DOCS}artifact-control.html`, + avcResults: `${ELASTIC_WEBSITE_URL}blog/elastic-security-malware-protection-test-av-comparatives`, trustedApps: `${SECURITY_SOLUTION_DOCS}trusted-apps-ov.html`, eventFilters: `${SECURITY_SOLUTION_DOCS}event-filters.html`, blocklist: `${SECURITY_SOLUTION_DOCS}blocklist.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 7a296aac6d8ba..5586f0f8f201f 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -334,6 +334,7 @@ export interface DocLinks { }; readonly securitySolution: { readonly artifactControl: string; + readonly avcResults: string; readonly trustedApps: string; readonly eventFilters: string; readonly blocklist: string; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/avc_banner/avc_banner_background.svg b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/avc_banner/avc_banner_background.svg new file mode 100644 index 0000000000000..cd37f26c95f7b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/avc_banner/avc_banner_background.svg @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/avc_banner/avc_results_banner_2024.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/avc_banner/avc_results_banner_2024.tsx new file mode 100644 index 0000000000000..63a3f68254c6c --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/avc_banner/avc_results_banner_2024.tsx @@ -0,0 +1,56 @@ +/* + * 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 { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiCallOut, EuiSpacer, useEuiTheme } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +import avcBannerBackground from './avc_banner_background.svg'; + +export const AVCResultsBanner2024: React.FC<{ onDismiss: () => void }> = ({ onDismiss }) => { + const { docLinks } = useKibana().services; + const { euiTheme } = useEuiTheme(); + const bannerTitle = i18n.translate( + 'xpack.fleet.integrations.epm.elasticDefend.avcResultsBanner.title', + { + defaultMessage: '100% protection with zero false positives.', + } + ); + + const calloutStyles = css({ + paddingLeft: `${euiTheme.size.xl}`, + backgroundImage: `url(${avcBannerBackground})`, + backgroundRepeat: 'no-repeat', + backgroundPositionX: 'right', + backgroundPositionY: 'bottom', + }); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx index 85899dc8d86c3..f9c2d80d3336e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx @@ -20,6 +20,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + import { isIntegrationPolicyTemplate, isPackagePrerelease, @@ -36,6 +38,10 @@ import { isPackageUnverified } from '../../../../../../../services'; import type { PackageInfo, RegistryPolicyTemplate } from '../../../../../types'; import { SideBarColumn } from '../../../components/side_bar_column'; +import type { FleetStartServices } from '../../../../../../../plugin'; + +import { AVCResultsBanner2024 } from './avc_banner/avc_results_banner_2024'; + import { Screenshots } from './screenshots'; import { Readme } from './readme'; import { Details } from './details'; @@ -159,9 +165,11 @@ export const OverviewPage: React.FC = memo( () => integrationInfo?.screenshots || packageInfo.screenshots || [], [integrationInfo, packageInfo.screenshots] ); + const { storage } = useKibana().services; const { packageVerificationKeyId } = useGetPackageVerificationKeyId(); const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId); const isPrerelease = isPackagePrerelease(packageInfo.version); + const isElasticDefend = packageInfo.name === 'endpoint'; const [markdown, setMarkdown] = useState(undefined); const [selectedItemId, setSelectedItem] = useState(undefined); const [isSideNavOpenOnMobile, setIsSideNavOpenOnMobile] = useState(false); @@ -283,6 +291,14 @@ export const OverviewPage: React.FC = memo( const requireAgentRootPrivileges = isRootPrivilegesRequired(packageInfo); + const [showAVCBanner, setShowAVCBanner] = useState( + storage.get('securitySolution.showAvcBanner') ?? true + ); + const onBannerDismiss = useCallback(() => { + setShowAVCBanner(false); + storage.set('securitySolution.showAvcBanner', false); + }, [storage]); + return ( @@ -297,6 +313,12 @@ export const OverviewPage: React.FC = memo( {isUnverified && } + {isElasticDefend && showAVCBanner && ( + <> + + + + )} {isPrerelease && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/security_solution/public/common/components/avc_banner/avc_results_banner_2024.tsx b/x-pack/plugins/security_solution/public/common/components/avc_banner/avc_results_banner_2024.tsx new file mode 100644 index 0000000000000..0c73af1ef4861 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/avc_banner/avc_results_banner_2024.tsx @@ -0,0 +1,58 @@ +/* + * 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 { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiCallOut, EuiSpacer, useEuiTheme } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../lib/kibana'; +import avcBannerBackground from './avc_banner_background.svg'; + +export const AVCResultsBanner2024: React.FC<{ onDismiss: () => void }> = ({ onDismiss }) => { + const { docLinks } = useKibana().services; + const { euiTheme } = useEuiTheme(); + const bannerTitle = i18n.translate('xpack.securitySolution.common.avcResultsBanner.title', { + defaultMessage: '100% protection with zero false positives.', + }); + + const calloutStyles = css({ + paddingLeft: `${euiTheme.size.xl}`, + backgroundImage: `url(${avcBannerBackground})`, + backgroundRepeat: 'no-repeat', + backgroundPositionX: 'right', + backgroundPositionY: 'bottom', + }); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx index f3e6dfdd644c3..fb7b6b4c9ec6b 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ import React from 'react'; -import { render } from '@testing-library/react'; import { OnboardingComponent } from './onboarding'; import { AddIntegrationsSteps, @@ -15,44 +14,19 @@ import { ViewDashboardSteps, } from './types'; import { ProductLine, ProductTier } from './configs'; +import { useCurrentUser, useKibana } from '../../../lib/kibana'; +import type { AppContextTestRender } from '../../../mock/endpoint'; +import { createAppRootMockRenderer } from '../../../mock/endpoint'; + jest.mock('./toggle_panel'); -jest.mock('./hooks/use_project_features_url'); -jest.mock('./hooks/use_projects_url'); -jest.mock('../../../lib/kibana', () => { - const original = jest.requireActual('../../../lib/kibana'); - return { - ...original, - useCurrentUser: jest.fn().mockReturnValue({ fullName: 'UserFullName' }), - useAppUrl: jest.fn().mockReturnValue({ getAppUrl: jest.fn().mockReturnValue('mock url') }), - }; -}); -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - useEuiTheme: jest.fn().mockReturnValue({ - euiTheme: { - base: 16, - size: { xs: '4px', m: '12px', l: '24px', xl: '32px', xxl: '40px' }, - colors: { lightestShade: '' }, - font: { - weight: { bold: 700 }, - }, - }, - }), - }; -}); -jest.mock('react-router-dom', () => ({ - useLocation: jest.fn().mockReturnValue({ hash: '#watch_the_overview_video' }), -})); -jest.mock('@kbn/security-solution-navigation', () => ({ - useNavigateTo: jest.fn().mockReturnValue({ navigateTo: jest.fn() }), - SecurityPageName: { - landing: 'landing', - }, -})); +jest.mock('../../../lib/kibana'); + +(useCurrentUser as jest.Mock).mockReturnValue({ fullName: 'UserFullName' }); describe('OnboardingComponent', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; const props = { indicesExist: true, productTypes: [{ product_line: ProductLine.security, product_tier: ProductTier.complete }], @@ -65,16 +39,22 @@ describe('OnboardingComponent', () => { ], spaceId: 'spaceId', }; + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + render = () => (renderResult = mockedContext.render()); + }); + + afterEach(() => { jest.clearAllMocks(); }); it('should render page title, subtitle, and description', () => { - const { getByText } = render(); + render(); - const pageTitle = getByText('Hi UserFullName!'); - const subtitle = getByText(`Get started with Security`); - const description = getByText( + const pageTitle = renderResult.getByText('Hi UserFullName!'); + const subtitle = renderResult.getByText(`Get started with Security`); + const description = renderResult.getByText( `This area shows you everything you need to know. Feel free to explore all content. You can always come back here at any time.` ); @@ -84,12 +64,41 @@ describe('OnboardingComponent', () => { }); it('should render welcomeHeader and TogglePanel', () => { - const { getByTestId } = render(); + render(); - const welcomeHeader = getByTestId('welcome-header'); - const togglePanel = getByTestId('toggle-panel'); + const welcomeHeader = renderResult.getByTestId('welcome-header'); + const togglePanel = renderResult.getByTestId('toggle-panel'); expect(welcomeHeader).toBeInTheDocument(); expect(togglePanel).toBeInTheDocument(); }); + describe('AVC 2024 Results banner', () => { + it('should render on the page', () => { + render(); + expect(renderResult.getByTestId('avcResultsBanner')).toBeTruthy(); + }); + + it('should link to the blog post', () => { + render(); + expect(renderResult.getByTestId('avcReadTheBlog')).toHaveAttribute( + 'href', + 'https://www.elastic.co/blog/elastic-security-malware-protection-test-av-comparatives' + ); + }); + + it('on closing the callout should store dismissal state in local storage', () => { + render(); + renderResult.getByTestId('euiDismissCalloutButton').click(); + expect(renderResult.queryByTestId('avcResultsBanner')).toBeNull(); + expect(useKibana().services.storage.set).toHaveBeenCalledWith( + 'securitySolution.showAvcBanner', + false + ); + }); + it('should stay dismissed if it has been closed once', () => { + (useKibana().services.storage.get as jest.Mock).mockReturnValue(false); + render(); + expect(renderResult.queryByTestId('avcResultsBanner')).toBeNull(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.tsx index 624070c812ed7..c19f03f63461d 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.tsx +++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { TogglePanel } from './toggle_panel'; @@ -24,6 +24,7 @@ import type { StepId } from './types'; import { useOnboardingStyles } from './styles/onboarding.styles'; import { useKibana } from '../../../lib/kibana'; import type { OnboardingHubStepLinkClickedParams } from '../../../lib/telemetry/events/onboarding/types'; +import { AVCResultsBanner2024 } from '../../avc_banner/avc_results_banner_2024'; interface OnboardingProps { indicesExist?: boolean; @@ -55,8 +56,9 @@ export const OnboardingComponent: React.FC = ({ productTypes?.find((product) => product.product_line === ProductLine.security)?.product_tier, [productTypes] ); - const { wrapperStyles, progressSectionStyles, stepsSectionStyles } = useOnboardingStyles(); - const { telemetry } = useKibana().services; + const { wrapperStyles, progressSectionStyles, stepsSectionStyles, bannerStyles } = + useOnboardingStyles(); + const { telemetry, storage } = useKibana().services; const onStepLinkClicked = useCallback( (params: OnboardingHubStepLinkClickedParams) => { telemetry.reportOnboardingHubStepLinkClicked(params); @@ -64,10 +66,23 @@ export const OnboardingComponent: React.FC = ({ [telemetry] ); + const [showAVCBanner, setShowAVCBanner] = useState( + storage.get('securitySolution.showAvcBanner') ?? true + ); + const onBannerDismiss = useCallback(() => { + setShowAVCBanner(false); + storage.set('securitySolution.showAvcBanner', false); + }, [storage]); + useScrollToHash(); return (
+ {showAVCBanner && ( + + + + )} diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/onboarding.styles.ts b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/onboarding.styles.ts index f4a3901f4e2c0..4f2eb59f06bf2 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/onboarding.styles.ts +++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/onboarding.styles.ts @@ -25,6 +25,9 @@ export const useOnboardingStyles = () => { padding: `0 ${euiTheme.size.xxl} ${euiTheme.size.xxxl}`, backgroundColor: euiTheme.colors.lightestShade, }), + bannerStyles: css({ + margin: `-${euiTheme.size.l} 0`, + }), }), [ euiTheme.colors.lightestShade, diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts b/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts index 36466231b64a4..24be4de96cf9b 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_local_storage.ts @@ -27,10 +27,21 @@ export const localStorageMock = (): IStorage => { }; }; +const createStorageMock = (storeMock: IStorage): Storage => { + const storage = new Storage(storeMock); + return { + store: storeMock, + get: jest.fn((...args) => storage.get(...args)), + clear: jest.fn((...args) => storage.clear(...args)), + set: jest.fn((...args) => storage.set(...args)), + remove: jest.fn((...args) => storage.remove(...args)), + } as Storage; +}; + export const createSecuritySolutionStorageMock = () => { const localStorage = localStorageMock(); return { localStorage, - storage: new Storage(localStorage), + storage: createStorageMock(localStorage), }; };