From 0c575b78c64f3b3f81c6ee5bb497461e69294954 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 6 Sep 2024 12:38:20 +0200 Subject: [PATCH 01/74] basic architecture implemented --- .../security_solution/common/constants.ts | 2 +- .../public/app/app_routes.tsx | 4 +- .../security_solution/public/app_links.ts | 6 +-- .../security_route_page_wrapper/index.tsx | 46 ++++++++++++++----- .../public/lazy_sub_plugins.tsx | 2 + .../public/overview/links.ts | 25 +--------- .../public/overview/pages/landing.test.tsx | 31 ------------- .../public/overview/pages/landing.tsx | 32 ------------- .../public/overview/routes.tsx | 14 ------ .../security_solution/public/plugin.tsx | 2 + .../plugins/security_solution/public/types.ts | 3 ++ 11 files changed, 49 insertions(+), 118 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/pages/landing.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5da9b87a4e267..23ec7c128d668 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -85,7 +85,7 @@ export const MANAGE_PATH = '/manage' as const; export const TIMELINES_PATH = '/timelines' as const; export const CASES_PATH = '/cases' as const; export const OVERVIEW_PATH = '/overview' as const; -export const LANDING_PATH = '/get_started' as const; +export const ONBOARDING_PATH = '/get_started' as const; export const DATA_QUALITY_PATH = '/data_quality' as const; export const DETECTION_RESPONSE_PATH = '/detection_response' as const; export const DETECTIONS_PATH = '/detections' as const; diff --git a/x-pack/plugins/security_solution/public/app/app_routes.tsx b/x-pack/plugins/security_solution/public/app/app_routes.tsx index 07a17b4f0f00b..9703f05228314 100644 --- a/x-pack/plugins/security_solution/public/app/app_routes.tsx +++ b/x-pack/plugins/security_solution/public/app/app_routes.tsx @@ -10,7 +10,7 @@ import type { RouteProps } from 'react-router-dom'; import { Redirect } from 'react-router-dom'; import { Routes, Route } from '@kbn/shared-ux-router'; import type { Capabilities } from '@kbn/core/public'; -import { CASES_FEATURE_ID, CASES_PATH, LANDING_PATH, SERVER_APP_ID } from '../../common/constants'; +import { CASES_FEATURE_ID, CASES_PATH, ONBOARDING_PATH, SERVER_APP_ID } from '../../common/constants'; import { NotFoundPage } from './404'; import type { StartServices } from '../types'; @@ -33,7 +33,7 @@ AppRoutes.displayName = 'AppRoutes'; export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(({ capabilities }) => { if (capabilities[SERVER_APP_ID].show === true) { - return ; + return ; } if (capabilities[CASES_FEATURE_ID].read_cases === true) { return ; diff --git a/x-pack/plugins/security_solution/public/app_links.ts b/x-pack/plugins/security_solution/public/app_links.ts index 4140f6bfcd322..dca76b1c37f70 100644 --- a/x-pack/plugins/security_solution/public/app_links.ts +++ b/x-pack/plugins/security_solution/public/app_links.ts @@ -15,7 +15,7 @@ import { links as timelinesLinks } from './timelines/links'; import { links as casesLinks } from './cases/links'; import { links as managementLinks, getManagementFilteredLinks } from './management/links'; import { exploreLinks } from './explore/links'; -import { gettingStartedLinks } from './overview/links'; +import { onboardingLinks } from './onboarding/links'; import { findingsLinks } from './cloud_security_posture/links'; import type { StartPlugins } from './types'; import { dashboardsLinks } from './dashboards/links'; @@ -33,7 +33,7 @@ export const appLinks: AppLinkItems = Object.freeze([ indicatorsLinks, exploreLinks, rulesLinks, - gettingStartedLinks, + onboardingLinks, managementLinks, ]); @@ -53,7 +53,7 @@ export const getFilteredLinks = async ( indicatorsLinks, exploreLinks, rulesLinks, - gettingStartedLinks, + onboardingLinks, managementFilteredLinks, ]); }; diff --git a/x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx b/x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx index 265e921f67f45..3351666c64529 100644 --- a/x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx @@ -43,26 +43,33 @@ export const SecurityRoutePageWrapper: FC { const link = useLinkInfo(pageName); - const UpsellPage = useUpsellingPage(pageName); + const UpsellingPage = useUpsellingPage(pageName); + + // The upselling page is only returned when the license/product requirements are not met, + // When it is defined it must be rendered, no need to check anything else. + if (UpsellingPage) { + return ( + <> + + + + ); + } const isAuthorized = link != null && !link.unauthorized; if (isAuthorized) { - return {children}; + return ( + + {children} + + + ); } if (redirectOnMissing && link == null) { return ; // redirects to the home page } - if (UpsellPage) { - return ( - <> - - - - ); - } - return ( <> @@ -73,3 +80,20 @@ export const SecurityRoutePageWrapper: FC ); }; + +/** + * HOC to wrap a component with the `SecurityRoutePageWrapper`. + */ +export const withSecurityRoutePageWrapper = ( + Component: React.ComponentType, + pageName: SecurityPageName, + redirectOnMissing?: boolean +) => { + return function WithSecurityRoutePageWrapper(props: T) { + return ( + + + + ); + }; +}; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 166d14b01c1a1..8bbba3885a2ab 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -16,6 +16,7 @@ import { Detections } from './detections'; import { Exceptions } from './exceptions'; import { Explore } from './explore'; import { Kubernetes } from './kubernetes'; +import { Onboarding } from './onboarding'; import { Overview } from './overview'; import { Rules } from './rules'; import { Timelines } from './timelines'; @@ -39,6 +40,7 @@ const subPluginClasses = { Exceptions, Explore, Kubernetes, + Onboarding, Overview, Rules, Timelines, diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index 0f3bdac0786ec..ab2f64bf1b98a 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -10,18 +10,11 @@ import { DATA_QUALITY_PATH, DETECTION_RESPONSE_PATH, ENTITY_ANALYTICS_PATH, - LANDING_PATH, OVERVIEW_PATH, SecurityPageName, SERVER_APP_ID, } from '../../common/constants'; -import { - DATA_QUALITY, - DETECTION_RESPONSE, - GETTING_STARTED, - OVERVIEW, - ENTITY_ANALYTICS, -} from '../app/translations'; +import { DATA_QUALITY, DETECTION_RESPONSE, OVERVIEW, ENTITY_ANALYTICS } from '../app/translations'; import type { LinkItem } from '../common/links/types'; import overviewPageImg from '../common/images/overview_page.png'; import dataQualityDashboardPageImg from '../common/images/data_quality_dashboard_page.png'; @@ -45,22 +38,6 @@ export const overviewLinks: LinkItem = { ], }; -export const gettingStartedLinks: LinkItem = { - id: SecurityPageName.landing, - title: GETTING_STARTED, - path: LANDING_PATH, - capabilities: [`${SERVER_APP_ID}.show`], - globalSearchKeywords: [ - i18n.translate('xpack.securitySolution.appLinks.getStarted', { - defaultMessage: 'Getting started', - }), - ], - sideNavIcon: 'launch', - sideNavFooter: true, - skipUrlState: true, - hideTimeline: true, -}; - export const detectionResponseLinks: LinkItem = { id: SecurityPageName.detectionAndResponse, title: DETECTION_RESPONSE, diff --git a/x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx deleted file mode 100644 index e40a2b383196a..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 { render } from '@testing-library/react'; -import { LandingPage } from './landing'; -import { Router } from '@kbn/shared-ux-router'; -import { createBrowserHistory } from 'history'; -import type { PropsWithChildren } from 'react'; -import React from 'react'; - -jest.mock('../../common/components/landing_page'); -jest.mock('../../common/components/page_wrapper', () => ({ - SecuritySolutionPageWrapper: jest - .fn() - .mockImplementation(({ children }: PropsWithChildren) =>
{children}
), -})); -const history = createBrowserHistory(); -describe('LandingPage', () => { - it('renders page properly', () => { - const { queryByTestId } = render( - - - - ); - expect(queryByTestId('siem-landing-page')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/pages/landing.tsx b/x-pack/plugins/security_solution/public/overview/pages/landing.tsx deleted file mode 100644 index 5ce2bc36afb2e..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/pages/landing.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { css } from '@emotion/react'; -import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { SecurityPageName } from '../../../common/constants'; -import { LandingPageComponent } from '../../common/components/landing_page'; -import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; - -export const LandingPage = memo(() => { - return ( -
- - - - -
- ); -}); - -LandingPage.displayName = 'LandingPage'; diff --git a/x-pack/plugins/security_solution/public/overview/routes.tsx b/x-pack/plugins/security_solution/public/overview/routes.tsx index 82214b2463bdf..3b1e9ca0e8046 100644 --- a/x-pack/plugins/security_solution/public/overview/routes.tsx +++ b/x-pack/plugins/security_solution/public/overview/routes.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { - LANDING_PATH, OVERVIEW_PATH, DATA_QUALITY_PATH, DETECTION_RESPONSE_PATH, @@ -23,7 +22,6 @@ import { DetectionResponse } from './pages/detection_response'; import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper'; import { EntityAnalyticsPage } from '../entity_analytics/pages/entity_analytics_dashboard'; import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; -import { LandingPage } from './pages/landing'; const OverviewRoutes = () => ( @@ -41,14 +39,6 @@ const DetectionResponseRoutes = () => ( ); -const LandingRoutes = () => ( - - - - - -); - const EntityAnalyticsRoutes = () => ( @@ -74,10 +64,6 @@ export const routes: SecuritySubPluginRoutes = [ path: DETECTION_RESPONSE_PATH, component: DetectionResponseRoutes, }, - { - path: LANDING_PATH, - render: LandingRoutes, - }, { path: ENTITY_ANALYTICS_PATH, render: EntityAnalyticsRoutes, diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 991f591773aa7..18a7c238ba614 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -298,6 +298,7 @@ export class Plugin implements IPlugin; kubernetes: ReturnType; management: ReturnType; + onboarding: ReturnType; overview: ReturnType; rules: ReturnType; threatIntelligence: ReturnType; From e10f1da6fc8f16fcc7e93acf08388551c7178d04 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 9 Sep 2024 12:18:17 +0200 Subject: [PATCH 02/74] onboarding new directory in public --- .../centered_loading_spinner.tsx | 18 ++ .../centered_loading_spinner/index.ts | 8 + .../onboarding/components/onboarding.tsx | 39 +++ .../components/onboarding_avc_banner/index.ts | 8 + .../onboarding_avc_banner.tsx | 27 ++ .../onboarding_body/card_groups_config.ts | 20 ++ .../cards/integrations/index.ts | 23 ++ .../cards/integrations/integrations_card.tsx | 17 ++ .../integrations_check_complete.ts | 13 + .../components/onboarding_body/index.ts | 8 + .../onboarding_body/onboarding_body.tsx | 114 +++++++ .../onboarding_body/onboarding_card_group.tsx | 43 +++ .../onboarding_card_panel.styles.ts | 44 +++ .../onboarding_body/onboarding_card_panel.tsx | 95 ++++++ .../onboarding_body/translations.ts | 21 ++ .../onboarding_body/use_card_groups_config.ts | 46 +++ .../components/onboarding_context.tsx | 30 ++ .../components/onboarding_header/index.ts | 8 + .../onboarding_header/onboarding_header.tsx | 52 ++++ .../onboarding/components/translations.ts | 279 ++++++++++++++++++ .../onboarding/components/use_stored_state.ts | 35 +++ .../public/onboarding/constants.ts | 14 + .../public/onboarding/index.ts | 19 ++ .../public/onboarding/jest.config.js | 19 ++ .../public/onboarding/links.ts | 27 ++ .../public/onboarding/routes.tsx | 18 ++ .../public/onboarding/types.ts | 45 +++ 27 files changed, 1090 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/onboarding_avc_banner.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/constants.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/onboarding/links.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/routes.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/types.ts diff --git a/x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx b/x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx new file mode 100644 index 0000000000000..29466f79bcc41 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/centered_loading_spinner.tsx @@ -0,0 +1,18 @@ +/* + * 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 { EuiLoadingSpinner, type EuiLoadingSpinnerProps } from '@elastic/eui'; + +const centerSpinnerStyle = { display: 'flex', margin: 'auto', marginTop: '10em' }; + +export const CenteredLoadingSpinner = React.memo( + (euiLoadingSpinnerProps) => { + return ; + } +); +CenteredLoadingSpinner.displayName = 'CenteredLoadingSpinner'; diff --git a/x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/index.ts b/x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/index.ts new file mode 100644 index 0000000000000..2d263a0773cc7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/centered_loading_spinner/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { CenteredLoadingSpinner } from './centered_loading_spinner'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx new file mode 100644 index 0000000000000..a23340c8d051b --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -0,0 +1,39 @@ +/* + * 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 { PluginTemplateWrapper } from '../../common/components/plugin_template_wrapper'; +import { CenteredLoadingSpinner } from '../../common/components/centered_loading_spinner'; +import { useSpaceId } from '../../common/hooks/use_space_id'; +import { OnboardingContextProvider } from './onboarding_context'; +import { OnboardingHeader } from './onboarding_header'; +import { OnboardingBody } from './onboarding_body'; +import { OnboardingAVCBanner } from './onboarding_avc_banner'; + +export const OnboardingPage = React.memo(() => { + const spaceId = useSpaceId(); + + if (!spaceId) { + return ( + + + + ); + } + + return ( + + + + + + + + ); +}); +OnboardingPage.displayName = 'OnboardingPage'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/index.ts new file mode 100644 index 0000000000000..be8808fc8dc29 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { OnboardingAVCBanner } from './onboarding_avc_banner'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/onboarding_avc_banner.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/onboarding_avc_banner.tsx new file mode 100644 index 0000000000000..62e9d5a123284 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_avc_banner/onboarding_avc_banner.tsx @@ -0,0 +1,27 @@ +/* + * 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, { useCallback } from 'react'; + +import { AVCResultsBanner2024, useIsStillYear2024 } from '@kbn/avc-banner'; +import { useStoredIsAVCBannerDismissed } from '../use_stored_state'; + +export const OnboardingAVCBanner = React.memo(() => { + const [isAVCBannerDismissed, setIsAVCBannerDismissed] = useStoredIsAVCBannerDismissed(); + const isStillYear2024 = useIsStillYear2024(); + + const dismissAVCBanner = useCallback(() => { + setIsAVCBannerDismissed(true); + }, [setIsAVCBannerDismissed]); + + if (isAVCBannerDismissed || !isStillYear2024) { + return null; + } + + return ; +}); +OnboardingAVCBanner.displayName = 'OnboardingAVCBanner'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts new file mode 100644 index 0000000000000..466b31aa5dd10 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts @@ -0,0 +1,20 @@ +/* + * 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 { integrationsCardConfig } from './cards/integrations'; +import type { OnboardingHubGroupConfig } from '../../types'; + +export const cardGroupsConfig: OnboardingHubGroupConfig[] = [ + { + title: 'Add and validate your data', // TODO: i18n + cards: [ + integrationsCardConfig, + // dashboardsCardConfig, + ], + }, + // ... +]; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts new file mode 100644 index 0000000000000..088335cab5ed6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { OnboardingHubCardConfig } from '../../../../types'; +import { checkIntegrationsCardComplete } from './integrations_check_complete'; +import { OnboardingHubCardId } from '../../../../constants'; + +export const integrationsCardConfig: OnboardingHubCardConfig = { + id: OnboardingHubCardId.integrations, + title: 'Add data with integrations', // TODO: i18n + icon: 'fleetApp', + component: React.lazy( + () => import('./integrations_card' /* webpackChunkName: "onboarding_integrations_card" */) + ), + checkComplete: checkIntegrationsCardComplete, + capabilities: ['fleet.all', 'fleetv2.all'], + licenseType: 'basic', +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx new file mode 100644 index 0000000000000..7de1c420733e4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx @@ -0,0 +1,17 @@ +/* + * 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 { OnboardingHubCardComponent } from '../../../../types'; + +export const IntegrationsCard: OnboardingHubCardComponent = ({ setComplete }) => { + // implement this component + return
{'// TODO: integrations card'}
; +}; + +// eslint-disable-next-line import/no-default-export +export default IntegrationsCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts new file mode 100644 index 0000000000000..ce4467b164c2b --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts @@ -0,0 +1,13 @@ +/* + * 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 type { OnboardingHubCardCheckComplete } from '../../../../types'; + +export const checkIntegrationsCardComplete: OnboardingHubCardCheckComplete = async () => { + // implement this function + return new Promise((resolve) => setTimeout(() => resolve(true), 1000)); +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts new file mode 100644 index 0000000000000..ccbe527f38ba8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { OnboardingBody } from './onboarding_body'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx new file mode 100644 index 0000000000000..51ebe9e125d77 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -0,0 +1,114 @@ +/* + * 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, { Suspense, useCallback } from 'react'; +import { EuiLoadingSpinner, useEuiTheme } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { css } from '@emotion/react'; +import { PAGE_CONTENT_WIDTH, type OnboardingHubCardId } from '../../constants'; +import { useCardGroupsConfig } from './use_card_groups_config'; +import { useOnboardingContext } from '../onboarding_context'; +import { useStoredCompletedCardIds, useStoredExpandedCardId } from '../use_stored_state'; +import { OnboardingCardGroup } from './onboarding_card_group'; +import { OnboardingCardPanel } from './onboarding_card_panel'; + +export const OnboardingBody = React.memo(() => { + const { euiTheme } = useEuiTheme(); + const { spaceId } = useOnboardingContext(); + + const [completeCardIds, setCompleteCardIds] = useStoredCompletedCardIds(spaceId); + const [expandedCardId, setExpandedCardId] = useStoredExpandedCardId(spaceId); + + const cardGroupsConfig = useCardGroupsConfig(); + + const isCardComplete = useCallback( + (cardId: OnboardingHubCardId) => completeCardIds.includes(cardId), + [completeCardIds] + ); + + const setCompleteCard = useCallback( + (stepId: OnboardingHubCardId, complete: boolean) => { + if (complete) { + setCompleteCardIds((currentCompleteCards = []) => [ + ...new Set([...currentCompleteCards, stepId]), + ]); + } else { + setCompleteCardIds((currentCompleteCards = []) => + currentCompleteCards.filter((id) => id !== stepId) + ); + } + }, + [setCompleteCardIds] + ); + + // const {loading, completedCards, error} = useCheckCompleteCards(groupsConfig); + + const createCardSetComplete = useCallback( + (cardId: OnboardingHubCardId) => (complete: boolean) => { + setCompleteCard(cardId, complete); + }, + [setCompleteCard] + ); + + const createOnToggleExpanded = useCallback( + (cardId: OnboardingHubCardId) => (expanded: boolean) => { + setExpandedCardId(expanded ? cardId : null); + }, + [setExpandedCardId] + ); + + return ( + + {cardGroupsConfig.map((group) => { + return ( + + {group.cards.map((card) => { + const LazyCardComponent = card.component; + const onToggleExpanded = createOnToggleExpanded(card.id); + return ( + + }> + + + + ); + })} + + ); + })} + + ); +}); + +OnboardingBody.displayName = 'OnboardingBody'; + +/** + TODO: +- implement OnboardingCardBody component in separate file +- implement OnboardingCardPanel components in separate files, it should call +- implement useCheckCompleteCards hook to execute only the first render +- if the logic related to completed cards (including useCheckCompleteCards) becomes too big or complex, consider moving it to a custom hook: + const { isCardComplete, setCompleteCard } = useCompleteCards(groupsConfig); +- implement header and footer components +(most of the components are already implemented in the original code, so you can copy them and adjust them as needed) + */ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx new file mode 100644 index 0000000000000..7425e94476d58 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx @@ -0,0 +1,43 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import { useEuiTheme, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { css } from '@emotion/react'; + +export const OnboardingCardGroup = React.memo>( + ({ title, children }) => { + const { euiTheme } = useEuiTheme(); + return ( + +

+ {title} +

+ + {children} +
+ ); + } +); +OnboardingCardGroup.displayName = 'OnboardingCardGroup'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts new file mode 100644 index 0000000000000..8e5bbe50d6172 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts @@ -0,0 +1,44 @@ +/* + * 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 { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export const HEIGHT_ANIMATION_DURATION = 250; + +export const useCardPanelStyles = () => { + const { euiTheme } = useEuiTheme(); + const completeStepBackgroundColor = useEuiBackgroundColor('success'); + + return css` + .onboardingCardHeader { + padding: ${euiTheme.size.l}; + cursor: pointer; + } + .onboardingCardHeaderTitle { + font-weight: ${euiTheme.font.weight.semiBold}; + } + .onboardingCardHeaderCompleteBadge { + background-color: ${completeStepBackgroundColor}; + color: ${euiTheme.colors.successText}; + } + .onboardingCardContentWrapper { + display: grid; + grid-template-rows: 1fr; + visibility: visible; + transition: grid-template-rows ${HEIGHT_ANIMATION_DURATION}ms ease-in, + visibility ${euiTheme.animation.normal} ${euiTheme.animation.resistance}; + } + &.onboardingCardPanel-collapsed .onboardingCardContentWrapper { + visibility: collapse; + grid-template-rows: 0fr; + } + .onboardingCardContent { + overflow: hidden; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx new file mode 100644 index 0000000000000..883f29956d1e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx @@ -0,0 +1,95 @@ +/* + * 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, { useCallback, type PropsWithChildren } from 'react'; +import type { IconType } from '@elastic/eui'; +import { + EuiPanel, + EuiBadge, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTitle, +} from '@elastic/eui'; +import classnames from 'classnames'; +import type { OnboardingHubCardId } from '../../constants'; +import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from '../translations'; +import { useCardPanelStyles } from './onboarding_card_panel.styles'; + +interface OnboardingCardPanelProps { + id: OnboardingHubCardId; + title: string; + icon: IconType; + isExpanded: boolean; + isComplete: boolean; + onToggleExpanded: (expanded: boolean) => void; +} + +export const OnboardingCardPanel = React.memo>( + ({ id, title, icon, isExpanded, isComplete, onToggleExpanded, children }) => { + const onToggle = useCallback(() => { + onToggleExpanded(!isExpanded); + }, [onToggleExpanded, isExpanded]); + + const styles = useCardPanelStyles(); + const cardPanelClassName = classnames(styles, { + 'onboardingCardPanel-collapsed': !isExpanded, + }); + + return ( + + + + + + + + + +

{title}

+
+
+ {isComplete && ( + + + {CARD_COMPLETE_BADGE} + + + )} + + + +
+
+
{children}
+
+
+ ); + } +); +OnboardingCardPanel.displayName = 'OnboardingCardPanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts new file mode 100644 index 0000000000000..c70f592873cbf --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 CARD_COMPLETE_BADGE = i18n.translate( + 'xpack.securitySolution.onboarding.cardComplete', + { + defaultMessage: 'Completed', + } +); + +export const EXPAND_CARD_BUTTON_LABEL = (title: string) => + i18n.translate('xpack.securitySolution.onboarding.expandCardButtonAriaLabel', { + defaultMessage: 'Expand "{title}"', + values: { title }, + }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts new file mode 100644 index 0000000000000..e662b143f2263 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts @@ -0,0 +1,46 @@ +/* + * 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 { useObservable } from 'react-use'; +import { useMemo } from 'react'; +import { hasCapabilities } from '../../../common/lib/capabilities'; +import { useKibana } from '../../../common/lib/kibana/kibana_react'; +import { cardGroupsConfig } from './card_groups_config'; + +export const useCardGroupsConfig = () => { + const { application, licensing } = useKibana().services; + const license = useObservable(licensing.license$); + + const filteredCardGroupsConfig = useMemo(() => { + return cardGroupsConfig.filter((group) => { + const filteredCards = group.cards.filter((card) => { + if (card.capabilities) { + const cardHasCapabilities = hasCapabilities(application.capabilities, card.capabilities); + if (!cardHasCapabilities) { + return false; + } + } + + if (card.licenseType) { + const cardHasLicense = license?.hasAtLeast(card.licenseType); + if (!cardHasLicense) { + return false; + } + } + + return true; + }); + + if (filteredCards.length === 0) { + return false; + } + return true; + }); + }, [license, application.capabilities]); + + return filteredCardGroupsConfig; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx new file mode 100644 index 0000000000000..e6f1401ac69e6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx @@ -0,0 +1,30 @@ +/* + * 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 type { PropsWithChildren } from 'react'; +import React, { createContext, useContext } from 'react'; + +interface OnboardingContextValue { + spaceId: string; +} +const OnboardingContext = createContext(null); + +export const OnboardingContextProvider: React.FC> = + React.memo(({ children, spaceId }) => { + return {children}; + }); +OnboardingContextProvider.displayName = 'OnboardingContextProvider'; + +export const useOnboardingContext = () => { + const context = useContext(OnboardingContext); + if (!context) { + throw new Error( + 'No OnboardingContext found. Please wrap the application with OnboardingProvider' + ); + } + return context; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts new file mode 100644 index 0000000000000..d8777b6557468 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { OnboardingHeader } from './onboarding_header'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx new file mode 100644 index 0000000000000..364ee0ed9e9a3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx @@ -0,0 +1,52 @@ +/* + * 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. + */ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import React from 'react'; + +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { useCurrentUser } from '../../../common/lib/kibana/hooks'; +import { PAGE_CONTENT_WIDTH } from '../../constants'; + +export const OnboardingHeader = React.memo(() => { + const currentUser = useCurrentUser(); + + // Full name could be null, user name should always exist + const currentUserName = currentUser?.fullName || currentUser?.username; + + return ( + + + + {currentUserName && ( + + + {'Hi '} + {currentUserName} + + + )} + + {'// TODO: Header goes here'} + + + + ); +}); +OnboardingHeader.displayName = 'OnboardingHeader'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/translations.ts new file mode 100644 index 0000000000000..dd9176d973af1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/translations.ts @@ -0,0 +1,279 @@ +/* + * 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. + */ + +// TODO: remove this file once the translations are moved + +// import { i18n } from '@kbn/i18n'; + +// export const GET_STARTED_PAGE_TITLE = (userName: string) => +// i18n.translate('xpack.securitySolution.onboarding.Title', { +// defaultMessage: `Hi {userName}!`, +// values: { userName }, +// }); + +// export const GET_STARTED_PAGE_SUBTITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.subTitle', +// { +// defaultMessage: `Get started with Security`, +// } +// ); + +// export const GET_STARTED_PAGE_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.description', +// { +// defaultMessage: `This area shows you everything you need to know. Feel free to explore all content. You can always come back here at any time.`, +// } +// ); + +// export const GET_STARTED_DATA_INGESTION_HUB_SUBTITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.subTitle', +// { +// defaultMessage: `Welcome to Elastic Security`, +// } +// ); + +// export const GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.description', +// { +// defaultMessage: `Follow these steps to set up your workspace.`, +// } +// ); + +// export const CURRENT_PLAN_LABEL = i18n.translate( +// 'xpack.securitySolution.onboarding.currentPlan.label', +// { +// defaultMessage: 'Current plan:', +// } +// ); + +// export const CURRENT_TIER_LABEL = i18n.translate( +// 'xpack.securitySolution.onboarding.currentTier.label', +// { +// defaultMessage: 'Current tier:', +// } +// ); + +// export const PROGRESS_TRACKER_LABEL = i18n.translate( +// 'xpack.securitySolution.onboarding.progressTracker.progressBar.label', +// { defaultMessage: 'PROGRESS' } +// ); + +// export const SECTION_1_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.section1.title', +// { +// defaultMessage: 'Quick start', +// } +// ); + +// export const SECTION_2_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.section2.title', +// { +// defaultMessage: 'Add and validate your data', +// } +// ); + +// export const SECTION_3_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.section3.title', +// { +// defaultMessage: 'Get started with alerts', +// } +// ); + +// export const CREATE_PROJECT_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.createProject.title', +// { +// defaultMessage: 'Create your first project', +// } +// ); + +// export const CREATE_PROJECT_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.step.createProject.description', +// { +// defaultMessage: `Create Elastic Security project with our fully-managed serverless solutions that automatically manage nodes, shards, data tiers and scaling to maintain the health and performance so you can focus on your data and goals.`, +// } +// ); + +// export const WATCH_VIDEO_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.watchVideo.title', +// { +// defaultMessage: 'Watch the overview video', +// } +// ); + +// export const WATCH_VIDEO_BUTTON_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.watchVideo.button.title', +// { +// defaultMessage: 'Elastic Security', +// } +// ); + +// export const WATCH_VIDEO_DESCRIPTION1 = i18n.translate( +// 'xpack.securitySolution.onboarding.step.watchVideo.description1', +// { +// defaultMessage: `Elastic Security unifies analytics, EDR, cloud security capabilities, and more into a SaaS solution that helps you improve your organization’s security posture, defend against a wide range of threats, and prevent breaches. +// `, +// } +// ); + +// export const WATCH_VIDEO_DESCRIPTION2 = i18n.translate( +// 'xpack.securitySolution.onboarding.step.watchVideo.description2', +// { +// defaultMessage: `To explore the platform’s core features, watch the video:`, +// } +// ); + +// export const ADD_INTEGRATIONS_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.addIntegrations.title', +// { +// defaultMessage: 'Add data with integrations', +// } +// ); + +// export const ADD_INTEGRATIONS_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.step.addIntegrations.description', +// { +// defaultMessage: +// 'Use integrations to import data from common sources and help you gather relevant information in one place.', +// } +// ); + +// export const ADD_INTEGRATIONS_IMAGE_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.addIntegrations.image.title', +// { +// defaultMessage: 'Connect to existing data sources', +// } +// ); + +// export const VIEW_DASHBOARDS = i18n.translate( +// 'xpack.securitySolution.onboarding.step.viewDashboards.title', +// { +// defaultMessage: 'View and analyze your data using dashboards', +// } +// ); + +// export const VIEW_DASHBOARDS_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.step.viewDashboards.description', +// { +// defaultMessage: +// 'Use dashboards to visualize data and stay up-to-date with key information. Create your own, or use Elastic’s default dashboards — including alerts, user authentication events, known vulnerabilities, and more.', +// } +// ); + +// export const VIEW_DASHBOARDS_IMAGE_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.viewDashboards.image.title', +// { +// defaultMessage: 'Analyze data using dashboards', +// } +// ); + +// export const ENABLE_RULES = i18n.translate( +// 'xpack.securitySolution.onboarding.step.enableRules.title', +// { +// defaultMessage: 'Enable prebuilt rules', +// } +// ); + +// export const ENABLE_RULES_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.step.enableRules.description', +// { +// defaultMessage: +// 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met.', +// } +// ); + +// export const VIEW_ALERTS_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.step.viewAlerts.title', +// { +// defaultMessage: 'View alerts', +// } +// ); + +// export const VIEW_ALERTS_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.step.viewAlerts.description', +// { +// defaultMessage: +// 'Visualize, sort, filter, and investigate alerts from across your infrastructure. Examine individual alerts of interest, and discover general patterns in alert volume and severity.', +// } +// ); + +// export const PRODUCT_BADGE_ANALYTICS = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.productBadge.analytics', +// { +// defaultMessage: 'Analytics', +// } +// ); + +// export const PRODUCT_BADGE_CLOUD = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.productBadge.cloud', +// { +// defaultMessage: 'Cloud', +// } +// ); + +// export const PRODUCT_BADGE_EDR = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.productBadge.edr', +// { +// defaultMessage: 'EDR', +// } +// ); + +// export const TOGGLE_PANEL_TITLE = i18n.translate( +// 'xpack.securitySolution.onboardingProductLabel.title', +// { +// defaultMessage: `Curate your Get Started experience:`, +// } +// ); + +// export const ANALYTICS_SWITCH_LABEL = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.switch.analytics.label', +// { +// defaultMessage: 'Analytics', +// } +// ); + +// export const CLOUD_SWITCH_LABEL = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.switch.cloud.label', + +// { +// defaultMessage: 'Cloud Security', +// } +// ); + +// export const ENDPOINT_SWITCH_LABEL = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.switch.endpoint.label', +// { +// defaultMessage: 'Endpoint Security', +// } +// ); + +// export const MARK_AS_DONE_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.markAsDoneTitle', +// { +// defaultMessage: 'Mark as done', +// } +// ); + +// export const UNDO_MARK_AS_DONE_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.undoMarkAsDoneTitle', +// { +// defaultMessage: `Undo 'mark as done'`, +// } +// ); + +// export const TOGGLE_PANEL_EMPTY_TITLE = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.empty.title', +// { +// defaultMessage: `Hmm, there doesn't seem to be anything there`, +// } +// ); + +// export const TOGGLE_PANEL_EMPTY_DESCRIPTION = i18n.translate( +// 'xpack.securitySolution.onboarding.togglePanel.empty.description', +// { +// defaultMessage: `Switch on a toggle to continue your curated "Get Started" experience`, +// } +// ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts new file mode 100644 index 0000000000000..b64522538b519 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts @@ -0,0 +1,35 @@ +/* + * 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 { useLocalStorage } from 'react-use'; +import type { OnboardingHubCardId } from '../constants'; + +const LocalStorageKey = { + completeCards: 'ONBOARDING_HUB.COMPLETE_CARDS', + expandedCard: 'ONBOARDING_HUB.EXPANDED_CARD', + avcBannerDismissed: 'ONBOARDING_HUB.AVC_BANNER_DISMISSED', +} as const; + +/** + * Uses local storage to store a value, but if the value is not defined, it will return the default value + */ +const useDefinedLocalStorage = (key: string, defaultValue: T) => { + const [value, setValue] = useLocalStorage(key); + return [value ?? defaultValue, setValue] as const; +}; + +export const useStoredCompletedCardIds = (spaceId: string) => + useDefinedLocalStorage(`${LocalStorageKey.completeCards}.${spaceId}`, []); + +export const useStoredExpandedCardId = (spaceId: string) => + useDefinedLocalStorage( + `${LocalStorageKey.expandedCard}.${spaceId}`, + null + ); + +export const useStoredIsAVCBannerDismissed = () => + useDefinedLocalStorage(`${LocalStorageKey.avcBannerDismissed}`, false); diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts new file mode 100644 index 0000000000000..2888f41978412 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -0,0 +1,14 @@ +/* + * 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 const PAGE_CONTENT_WIDTH = '1150px'; + +export enum OnboardingHubCardId { + integrations = 'integrations', + dashboards = 'dashboards', + rules = 'rules', + alerts = 'alerts', +} diff --git a/x-pack/plugins/security_solution/public/onboarding/index.ts b/x-pack/plugins/security_solution/public/onboarding/index.ts new file mode 100644 index 0000000000000..b20a2777f6ee7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/index.ts @@ -0,0 +1,19 @@ +/* + * 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 type { SecuritySubPlugin } from '../app/types'; +import { routes } from './routes'; + +export class Onboarding { + public setup() {} + + public start(): SecuritySubPlugin { + return { + routes, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/onboarding/jest.config.js b/x-pack/plugins/security_solution/public/onboarding/jest.config.js new file mode 100644 index 0000000000000..f5c05b19b495b --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/onboarding'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/onboarding', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/onboarding/**/*.{ts,tsx}', + ], + moduleNameMapper: require('../../server/__mocks__/module_name_map'), +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/links.ts b/x-pack/plugins/security_solution/public/onboarding/links.ts new file mode 100644 index 0000000000000..ea9e603a36fac --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/links.ts @@ -0,0 +1,27 @@ +/* + * 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'; +import { ONBOARDING_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; +import { GETTING_STARTED } from '../app/translations'; +import type { LinkItem } from '../common/links/types'; + +export const onboardingLinks: LinkItem = { + id: SecurityPageName.landing, + title: GETTING_STARTED, + path: ONBOARDING_PATH, + capabilities: [`${SERVER_APP_ID}.show`], + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.getStarted', { + defaultMessage: 'Getting started', + }), + ], + sideNavIcon: 'launch', + sideNavFooter: true, + skipUrlState: true, + hideTimeline: true, +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/routes.tsx b/x-pack/plugins/security_solution/public/onboarding/routes.tsx new file mode 100644 index 0000000000000..b8ac8aba3e90e --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/routes.tsx @@ -0,0 +1,18 @@ +/* + * 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 { ONBOARDING_PATH, SecurityPageName } from '../../common/constants'; +import type { SecuritySubPluginRoutes } from '../app/types'; +import { withSecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper'; +import { OnboardingPage } from './components/onboarding'; + +export const routes: SecuritySubPluginRoutes = [ + { + path: ONBOARDING_PATH, + component: withSecurityRoutePageWrapper(OnboardingPage, SecurityPageName.landing), + }, +]; diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts new file mode 100644 index 0000000000000..14ac95a0e97fe --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/types.ts @@ -0,0 +1,45 @@ +/* + * 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 type { IconType } from '@elastic/eui'; +import type { LicenseType } from '@kbn/licensing-plugin/public'; +import type { OnboardingHubCardId } from './constants'; +import type { RequiredCapabilities } from '../common/lib/capabilities'; + +export type OnboardingHubCardComponent = React.ComponentType<{ + setComplete: (complete: boolean) => void; +}>; + +export type OnboardingHubCardCheckComplete = () => Promise; + +export interface OnboardingHubCardConfig { + id: OnboardingHubCardId; + title: string; // i18n + icon: IconType; + /** + * Component to render the card + */ + component: React.ComponentType<{ setComplete: (complete: boolean) => void }>; // use React.lazy() to load the component + /** + * for auto-checking completion + * @returns Promise for the complete status + */ + checkComplete?: () => Promise; + /** + * Capabilities strings (using object dot notation) to enable the link. + */ + capabilities?: RequiredCapabilities; // check `x-pack/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts` + /** + * Minimum license required to enable the card + */ + licenseType?: LicenseType; +} + +export interface OnboardingHubGroupConfig { + title: string; + cards: OnboardingHubCardConfig[]; +} From ea54df3c024a85b5ccb5d2f97eb4d1f5d29c653d Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 10 Sep 2024 10:43:08 +0200 Subject: [PATCH 03/74] adapt design --- .../onboarding/components/onboarding.tsx | 4 +- .../onboarding_body/card_groups_config.ts | 22 ++-- .../cards/common/card_content_panel.tsx | 33 +++++ .../cards/dashboards/dashboards_card.tsx | 22 ++++ .../onboarding_body/cards/dashboards/index.ts | 23 ++++ .../cards/integrations/index.ts | 11 +- .../cards/integrations/integrations_card.tsx | 13 +- .../integrations_check_complete.ts | 6 +- .../onboarding_body/onboarding_body.tsx | 122 ++++++++---------- .../onboarding_body/onboarding_card_group.tsx | 33 +---- .../onboarding_card_panel.styles.ts | 34 +++-- .../onboarding_body/onboarding_card_panel.tsx | 21 ++- .../onboarding_body/use_card_groups_config.ts | 10 +- .../use_check_complete_cards.ts | 53 ++++++++ .../onboarding_body/use_completed_cards.ts | 39 ++++++ .../onboarding_body/use_expanded_card.ts | 69 ++++++++++ .../onboarding_footer/footer_items.ts | 81 ++++++++++++ .../onboarding_footer/images/demo.png | Bin 0 -> 6642 bytes .../images/documentation.png | Bin 0 -> 7046 bytes .../onboarding_footer/images/forum.png | Bin 0 -> 9139 bytes .../onboarding_footer/images/labs.png | Bin 0 -> 9628 bytes .../components/onboarding_footer/index.ts | 8 ++ .../onboarding_footer.styles.ts | 20 +++ .../onboarding_footer/onboarding_footer.tsx | 46 +++++++ .../onboarding/components/use_stored_state.ts | 2 +- .../public/onboarding/types.ts | 40 ++++-- 26 files changed, 563 insertions(+), 149 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/demo.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/documentation.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/forum.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/labs.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx index a23340c8d051b..2705abdb112ef 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -11,9 +11,10 @@ import { PluginTemplateWrapper } from '../../common/components/plugin_template_w import { CenteredLoadingSpinner } from '../../common/components/centered_loading_spinner'; import { useSpaceId } from '../../common/hooks/use_space_id'; import { OnboardingContextProvider } from './onboarding_context'; +import { OnboardingAVCBanner } from './onboarding_avc_banner'; import { OnboardingHeader } from './onboarding_header'; import { OnboardingBody } from './onboarding_body'; -import { OnboardingAVCBanner } from './onboarding_avc_banner'; +import { OnboardingFooter } from './onboarding_footer'; export const OnboardingPage = React.memo(() => { const spaceId = useSpaceId(); @@ -32,6 +33,7 @@ export const OnboardingPage = React.memo(() => { +
); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts index 466b31aa5dd10..ccd1deb6a6a83 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/card_groups_config.ts @@ -5,16 +5,22 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import type { OnboardingGroupConfig } from '../../types'; import { integrationsCardConfig } from './cards/integrations'; -import type { OnboardingHubGroupConfig } from '../../types'; +import { dashboardsCardConfig } from './cards/dashboards'; -export const cardGroupsConfig: OnboardingHubGroupConfig[] = [ +export const cardGroupsConfig: OnboardingGroupConfig[] = [ { - title: 'Add and validate your data', // TODO: i18n - cards: [ - integrationsCardConfig, - // dashboardsCardConfig, - ], + title: i18n.translate('xpack.securitySolution.onboarding.dataGroup.title', { + defaultMessage: 'Ingest your data', + }), + cards: [integrationsCardConfig, dashboardsCardConfig], + }, + { + title: i18n.translate('xpack.securitySolution.onboarding.alertsGroup.title', { + defaultMessage: 'Configure rules and alerts', + }), + cards: [], }, - // ... ]; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx new file mode 100644 index 0000000000000..ef88aacc08679 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx @@ -0,0 +1,33 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; + +export const OnboardingCardContentPanel = React.memo>(({ children }) => { + return ( + + {children} + + ); +}); +OnboardingCardContentPanel.displayName = 'OnboardingCardContentPanel'; + +export const OnboardingCardContentImagePanel = React.memo< + PropsWithChildren<{ imageSrc: string; imageAlt: string }> +>(({ children, imageSrc, imageAlt }) => { + return ( + + + {children} + + {imageAlt} + + + + ); +}); +OnboardingCardContentImagePanel.displayName = 'OnboardingCardContentImagePanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx new file mode 100644 index 0000000000000..24918ebd7dbcf --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx @@ -0,0 +1,22 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import type { OnboardingCardComponent } from '../../../../types'; +import { OnboardingCardContentPanel } from '../common/card_content_panel'; + +export const IntegrationsCard: OnboardingCardComponent = ({ setComplete }) => { + return ( + + setComplete(false)}>{'Set not complete'} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default IntegrationsCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts new file mode 100644 index 0000000000000..9293e16af84b7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { OnboardingCardConfig } from '../../../../types'; +import { OnboardingHubCardId } from '../../../../constants'; + +export const dashboardsCardConfig: OnboardingCardConfig = { + id: OnboardingHubCardId.dashboards, + title: i18n.translate('xpack.securitySolution.onboarding.dashboardsCard.title', { + defaultMessage: 'View and analyze your data using dashboards', + }), + icon: 'dashboardApp', + Component: React.lazy( + () => import('./dashboards_card' /* webpackChunkName: "onboarding_dashboards_card" */) + ), + capabilities: ['dashboard.show'], +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts index 088335cab5ed6..37a42e35b4c19 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -6,15 +6,18 @@ */ import React from 'react'; -import type { OnboardingHubCardConfig } from '../../../../types'; +import { i18n } from '@kbn/i18n'; +import type { OnboardingCardConfig } from '../../../../types'; import { checkIntegrationsCardComplete } from './integrations_check_complete'; import { OnboardingHubCardId } from '../../../../constants'; -export const integrationsCardConfig: OnboardingHubCardConfig = { +export const integrationsCardConfig: OnboardingCardConfig = { id: OnboardingHubCardId.integrations, - title: 'Add data with integrations', // TODO: i18n + title: i18n.translate('xpack.securitySolution.onboarding.integrationsCard.title', { + defaultMessage: 'Add data with integrations', + }), icon: 'fleetApp', - component: React.lazy( + Component: React.lazy( () => import('./integrations_card' /* webpackChunkName: "onboarding_integrations_card" */) ), checkComplete: checkIntegrationsCardComplete, diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx index 7de1c420733e4..24918ebd7dbcf 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx @@ -6,11 +6,16 @@ */ import React from 'react'; -import type { OnboardingHubCardComponent } from '../../../../types'; +import { EuiButton } from '@elastic/eui'; +import type { OnboardingCardComponent } from '../../../../types'; +import { OnboardingCardContentPanel } from '../common/card_content_panel'; -export const IntegrationsCard: OnboardingHubCardComponent = ({ setComplete }) => { - // implement this component - return
{'// TODO: integrations card'}
; +export const IntegrationsCard: OnboardingCardComponent = ({ setComplete }) => { + return ( + + setComplete(false)}>{'Set not complete'} + + ); }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts index ce4467b164c2b..e3705c431c504 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type { OnboardingHubCardCheckComplete } from '../../../../types'; +import type { OnboardingCardCheckComplete } from '../../../../types'; -export const checkIntegrationsCardComplete: OnboardingHubCardCheckComplete = async () => { +export const checkIntegrationsCardComplete: OnboardingCardCheckComplete = async () => { // implement this function - return new Promise((resolve) => setTimeout(() => resolve(true), 1000)); + return new Promise((resolve) => setTimeout(() => resolve(true), 3000)); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index 51ebe9e125d77..6f4fb534d4bd2 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -5,60 +5,55 @@ * 2.0. */ -import React, { Suspense, useCallback } from 'react'; -import { EuiLoadingSpinner, useEuiTheme } from '@elastic/eui'; +import React, { Suspense, useCallback, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { css } from '@emotion/react'; import { PAGE_CONTENT_WIDTH, type OnboardingHubCardId } from '../../constants'; import { useCardGroupsConfig } from './use_card_groups_config'; import { useOnboardingContext } from '../onboarding_context'; -import { useStoredCompletedCardIds, useStoredExpandedCardId } from '../use_stored_state'; import { OnboardingCardGroup } from './onboarding_card_group'; import { OnboardingCardPanel } from './onboarding_card_panel'; +import { useCheckCompleteCards } from './use_check_complete_cards'; +import { useExpandedCard } from './use_expanded_card'; +import { useCompletedCards } from './use_completed_cards'; export const OnboardingBody = React.memo(() => { const { euiTheme } = useEuiTheme(); const { spaceId } = useOnboardingContext(); - - const [completeCardIds, setCompleteCardIds] = useStoredCompletedCardIds(spaceId); - const [expandedCardId, setExpandedCardId] = useStoredExpandedCardId(spaceId); - const cardGroupsConfig = useCardGroupsConfig(); - const isCardComplete = useCallback( - (cardId: OnboardingHubCardId) => completeCardIds.includes(cardId), - [completeCardIds] + const { expandedCardId, setExpandedCardId } = useExpandedCard(spaceId); + const { isCardComplete, setCardComplete } = useCompletedCards(spaceId); + + const { checkAllCardsComplete, checkCardComplete } = useCheckCompleteCards( + cardGroupsConfig, + setCardComplete ); - const setCompleteCard = useCallback( - (stepId: OnboardingHubCardId, complete: boolean) => { - if (complete) { - setCompleteCardIds((currentCompleteCards = []) => [ - ...new Set([...currentCompleteCards, stepId]), - ]); + useEffect(() => { + // initial auto-check for all cards + checkAllCardsComplete(); + }, [checkAllCardsComplete]); + + const createOnToggleExpanded = useCallback( + (cardId: OnboardingHubCardId) => () => { + if (expandedCardId === cardId) { + setExpandedCardId(null); } else { - setCompleteCardIds((currentCompleteCards = []) => - currentCompleteCards.filter((id) => id !== stepId) - ); + setExpandedCardId(cardId); + // execute the auto-check for the card when it's been expanded + checkCardComplete(cardId); } }, - [setCompleteCardIds] + [setExpandedCardId, expandedCardId, checkCardComplete] ); - // const {loading, completedCards, error} = useCheckCompleteCards(groupsConfig); - - const createCardSetComplete = useCallback( + const createSetCardComplete = useCallback( (cardId: OnboardingHubCardId) => (complete: boolean) => { - setCompleteCard(cardId, complete); + setCardComplete(cardId, complete); }, - [setCompleteCard] - ); - - const createOnToggleExpanded = useCallback( - (cardId: OnboardingHubCardId) => (expanded: boolean) => { - setExpandedCardId(expanded ? cardId : null); - }, - [setExpandedCardId] + [setCardComplete] ); return ( @@ -71,44 +66,35 @@ export const OnboardingBody = React.memo(() => { background-color: ${euiTheme.colors.lightestShade}; `} > - {cardGroupsConfig.map((group) => { - return ( - - {group.cards.map((card) => { - const LazyCardComponent = card.component; - const onToggleExpanded = createOnToggleExpanded(card.id); - return ( - - }> - - - - ); - })} - - ); - })} + + {cardGroupsConfig.map((group, index) => ( + + + + {group.cards.map(({ id, title, icon, Component: LazyCardComponent }) => ( + + + }> + + + + + ))} + + + + ))} + + ); }); OnboardingBody.displayName = 'OnboardingBody'; - -/** - TODO: -- implement OnboardingCardBody component in separate file -- implement OnboardingCardPanel components in separate files, it should call -- implement useCheckCompleteCards hook to execute only the first render -- if the logic related to completed cards (including useCheckCompleteCards) becomes too big or complex, consider moving it to a custom hook: - const { isCardComplete, setCompleteCard } = useCompleteCards(groupsConfig); -- implement header and footer components -(most of the components are already implemented in the original code, so you can copy them and adjust them as needed) - */ diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx index 7425e94476d58..801c8f2e2aa8f 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx @@ -6,37 +6,18 @@ */ import React, { type PropsWithChildren } from 'react'; -import { useEuiTheme, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiSpacer, EuiTitle } from '@elastic/eui'; export const OnboardingCardGroup = React.memo>( ({ title, children }) => { - const { euiTheme } = useEuiTheme(); return ( - -

- {title} -

- +
+ +

{title}

+
+ {children} - +
); } ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts index 8e5bbe50d6172..baa1add546b95 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts @@ -12,33 +12,49 @@ export const HEIGHT_ANIMATION_DURATION = 250; export const useCardPanelStyles = () => { const { euiTheme } = useEuiTheme(); - const completeStepBackgroundColor = useEuiBackgroundColor('success'); + const successBackgroundColor = useEuiBackgroundColor('success'); return css` .onboardingCardHeader { - padding: ${euiTheme.size.l}; + padding: calc(${euiTheme.size.s} * 2); cursor: pointer; } + .onboardingCardIcon { + padding: ${euiTheme.size.m}; + border-radius: 50%; + background-color: ${euiTheme.colors.lightestShade}; + } .onboardingCardHeaderTitle { font-weight: ${euiTheme.font.weight.semiBold}; } .onboardingCardHeaderCompleteBadge { - background-color: ${completeStepBackgroundColor}; + background-color: ${successBackgroundColor}; color: ${euiTheme.colors.successText}; } .onboardingCardContentWrapper { display: grid; - grid-template-rows: 1fr; - visibility: visible; - transition: grid-template-rows ${HEIGHT_ANIMATION_DURATION}ms ease-in, - visibility ${euiTheme.animation.normal} ${euiTheme.animation.resistance}; - } - &.onboardingCardPanel-collapsed .onboardingCardContentWrapper { visibility: collapse; grid-template-rows: 0fr; + transition: grid-template-rows ${HEIGHT_ANIMATION_DURATION}ms ease-in, + visibility ${euiTheme.animation.normal} ${euiTheme.animation.resistance}; } .onboardingCardContent { overflow: hidden; } + + &.onboardingCardPanel-expanded { + border: 2px solid ${euiTheme.colors.primary}; + + .onboardingCardContentWrapper { + visibility: visible; + grid-template-rows: 1fr; + } + } + + &.onboardingCardPanel-completed { + .onboardingCardIcon { + background-color: ${successBackgroundColor}; + } + } `; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx index 883f29956d1e8..3a56ccf7edf23 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, type PropsWithChildren } from 'react'; +import React, { type PropsWithChildren } from 'react'; import type { IconType } from '@elastic/eui'; import { EuiPanel, @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import classnames from 'classnames'; import type { OnboardingHubCardId } from '../../constants'; -import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from '../translations'; +import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from './translations'; import { useCardPanelStyles } from './onboarding_card_panel.styles'; interface OnboardingCardPanelProps { @@ -27,18 +27,15 @@ interface OnboardingCardPanelProps { icon: IconType; isExpanded: boolean; isComplete: boolean; - onToggleExpanded: (expanded: boolean) => void; + onToggleExpanded: () => void; } export const OnboardingCardPanel = React.memo>( ({ id, title, icon, isExpanded, isComplete, onToggleExpanded, children }) => { - const onToggle = useCallback(() => { - onToggleExpanded(!isExpanded); - }, [onToggleExpanded, isExpanded]); - const styles = useCardPanelStyles(); const cardPanelClassName = classnames(styles, { - 'onboardingCardPanel-collapsed': !isExpanded, + 'onboardingCardPanel-expanded': isExpanded, + 'onboardingCardPanel-completed': isComplete, }); return ( @@ -46,20 +43,20 @@ export const OnboardingCardPanel = React.memo - + diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts index e662b143f2263..f7cb67ba34365 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts @@ -11,11 +11,19 @@ import { hasCapabilities } from '../../../common/lib/capabilities'; import { useKibana } from '../../../common/lib/kibana/kibana_react'; import { cardGroupsConfig } from './card_groups_config'; +/** + * Hook that filters the card groups config based on the user's capabilities and license + */ export const useCardGroupsConfig = () => { const { application, licensing } = useKibana().services; const license = useObservable(licensing.license$); const filteredCardGroupsConfig = useMemo(() => { + // If the license is not defined, return an empty array. It always eventually becomes available. + // This exit case is just to prevent config-dependent code to run multiple times for each card. + if (!license) { + return []; + } return cardGroupsConfig.filter((group) => { const filteredCards = group.cards.filter((card) => { if (card.capabilities) { @@ -26,7 +34,7 @@ export const useCardGroupsConfig = () => { } if (card.licenseType) { - const cardHasLicense = license?.hasAtLeast(card.licenseType); + const cardHasLicense = license.hasAtLeast(card.licenseType); if (!cardHasLicense) { return false; } diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts new file mode 100644 index 0000000000000..410636225c4ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts @@ -0,0 +1,53 @@ +/* + * 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 { useCallback, useMemo } from 'react'; +import type { OnboardingHubCardId } from '../../constants'; +import type { OnboardingCardConfig, OnboardingGroupConfig } from '../../types'; + +/** + * Hook that implements the functions to call the `checkComplete` function for the cards that have it defined in the config + **/ +export const useCheckCompleteCards = ( + cardsGroupConfig: OnboardingGroupConfig[], + setCardComplete: (cardId: OnboardingHubCardId, complete: boolean) => void +) => { + // Stores all cards that have a checkComplete function in a flat array + const cardsWithCheckComplete = useMemo( + () => + cardsGroupConfig.reduce((acc, group) => { + acc.push(...group.cards.filter((card) => card.checkComplete)); + return acc; + }, []), + [cardsGroupConfig] + ); + + // Exported function to run the check for all cards the have a checkComplete function + const checkAllCardsComplete = useCallback(() => { + cardsWithCheckComplete.map((card) => + card.checkComplete?.().then((isComplete) => { + setCardComplete(card.id, isComplete); + }) + ); + }, [cardsWithCheckComplete, setCardComplete]); + + // Exported function to run the check for a specific card + const checkCardComplete = useCallback( + (cardId: OnboardingHubCardId) => { + const cardConfig = cardsWithCheckComplete.find(({ id }) => id === cardId); + + if (cardConfig) { + cardConfig.checkComplete?.().then((isComplete) => { + setCardComplete(cardId, isComplete); + }); + } + }, + [cardsWithCheckComplete, setCardComplete] + ); + + return { checkAllCardsComplete, checkCardComplete }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts new file mode 100644 index 0000000000000..b40bcfcaa6ad9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts @@ -0,0 +1,39 @@ +/* + * 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 { useCallback } from 'react'; +import { useStoredCompletedCardIds } from '../use_stored_state'; +import type { OnboardingHubCardId } from '../../constants'; + +/** + * This hook implements the logic for tracking which onboarding cards have been completed using Local Storage. + */ +export const useCompletedCards = (spaceId: string) => { + const [completeCardIds, setCompleteCardIds] = useStoredCompletedCardIds(spaceId); + + const isCardComplete = useCallback( + (cardId: OnboardingHubCardId) => completeCardIds.includes(cardId), + [completeCardIds] + ); + + const setCardComplete = useCallback( + (cardId: OnboardingHubCardId, complete: boolean) => { + if (complete) { + setCompleteCardIds((currentCompleteCards = []) => [ + ...new Set([...currentCompleteCards, cardId]), + ]); + } else { + setCompleteCardIds((currentCompleteCards = []) => + currentCompleteCards.filter((id) => id !== cardId) + ); + } + }, + [setCompleteCardIds] + ); + + return { isCardComplete, setCardComplete }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts new file mode 100644 index 0000000000000..67a6177f0bb3f --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts @@ -0,0 +1,69 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useStoredExpandedCardId } from '../use_stored_state'; +import { HEIGHT_ANIMATION_DURATION } from './onboarding_card_panel.styles'; +import type { OnboardingHubCardId } from '../../constants'; + +const HEADER_OFFSET = 40; + +/** + * This hook manages the expanded card id state in the LocalStorage and the hash in the URL. + * Scenarios in the initial render: + * - Hash not defined and LS not defined: No card expanded and no scroll + * - Hash not defined and LS defined: Update hash with LS value and scroll to the card + * - Hash defined and LS not defined: Update LS with hash value and scroll to the card + * - Hash defined and LS defined: The hash value takes precedence, update LS if different and scroll to the card + */ +export const useExpandedCard = (spaceId: string) => { + const [expandedCardId, setExpandedCardId] = useStoredExpandedCardId(spaceId); + const location = useLocation(); + + const [documentReadyState, setReadyState] = useState(document.readyState); + + useEffect(() => { + const readyStateListener = () => setReadyState(document.readyState); + document.addEventListener('readystatechange', readyStateListener); + return () => document.removeEventListener('readystatechange', readyStateListener); + }, []); + + // This effect implements auto-scroll in the initial render, further changes in the hash should not trigger this effect + useEffect(() => { + if (documentReadyState !== 'complete') return; // Wait for page to finish loading before scrolling + let cardIdFromHash = location.hash.split('?')[0].replace('#', '') as OnboardingHubCardId | ''; + if (!cardIdFromHash) { + if (expandedCardId == null) return; + // Use the expanded card id if we have it stored and the hash is empty. + // The hash will be synched by the effect below + cardIdFromHash = expandedCardId; + } + + // If the hash is different from the expanded card id, update the expanded card id (fresh load) + if (expandedCardId !== cardIdFromHash) { + setExpandedCardId(cardIdFromHash); + } + + setTimeout(() => { + const element = document.getElementById(cardIdFromHash); + if (element) { + element.focus({ preventScroll: true }); // Scrolling already handled below + window.scrollTo({ top: element.offsetTop - HEADER_OFFSET, behavior: 'smooth' }); + } + }, HEIGHT_ANIMATION_DURATION); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [documentReadyState]); + + // Syncs the expanded card id with the hash, it does not trigger the scrolling effect + useEffect(() => { + history.replaceState(null, '', expandedCardId == null ? ' ' : `#${expandedCardId}`); + }, [expandedCardId]); + + return { expandedCardId, setExpandedCardId }; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts new file mode 100644 index 0000000000000..e0e2b272da3aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts @@ -0,0 +1,81 @@ +/* + * 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'; +import documentation from './images/documentation.png'; +import forum from './images/forum.png'; +import demo from './images/demo.png'; +import labs from './images/labs.png'; + +export const footerItems = [ + { + icon: documentation, + key: 'documentation', + title: i18n.translate('xpack.securitySolution.onboarding.footer.documentation.title', { + defaultMessage: 'Browse documentation', + }), + description: i18n.translate( + 'xpack.securitySolution.onboarding.footer.documentation.description', + { + defaultMessage: 'In-depth guides on all Elastic features', + } + ), + link: { + title: i18n.translate('xpack.securitySolution.onboarding.footer.documentation.link.title', { + defaultMessage: 'Start reading', + }), + href: 'https://docs.elastic.co/integrations/elastic-security-intro', + }, + }, + { + icon: forum, + key: 'forum', + title: i18n.translate('xpack.securitySolution.onboarding.footer.forum.title', { + defaultMessage: 'Explore forum', + }), + description: i18n.translate('xpack.securitySolution.onboarding.footer.forum.description', { + defaultMessage: 'Exchange thoughts about Elastic', + }), + link: { + title: i18n.translate('xpack.securitySolution.onboarding.footer.forum.link.title', { + defaultMessage: 'Discuss Forum', + }), + href: 'https://discuss.elastic.co/c/security/83', + }, + }, + { + icon: demo, + key: 'demo', + title: i18n.translate('xpack.securitySolution.onboarding.footer.demo.title', { + defaultMessage: 'View demo project', + }), + description: i18n.translate('xpack.securitySolution.onboarding.footer.demo.description', { + defaultMessage: 'Discover Elastic using sample data', + }), + link: { + title: i18n.translate('xpack.securitySolution.onboarding.footer.demo.link.title', { + defaultMessage: 'Explore demo', + }), + href: 'https://www.elastic.co/demo-gallery?solutions=security&features=null', + }, + }, + { + icon: labs, + key: 'labs', + title: i18n.translate('xpack.securitySolution.onboarding.footer.labs.title', { + defaultMessage: 'Elastic Security Labs', + }), + description: i18n.translate('xpack.securitySolution.onboarding.footer.labs.description', { + defaultMessage: 'Insights from security researchers', + }), + link: { + title: i18n.translate('xpack.securitySolution.onboarding.footer.labs.link.title', { + defaultMessage: 'Learn more', + }), + href: 'https://www.elastic.co/security-labs', + }, + }, +] as const; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/demo.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..b04bac15e09f0213371a4e554383fae51c67cac5 GIT binary patch literal 6642 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/documentation.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/documentation.png new file mode 100644 index 0000000000000000000000000000000000000000..2027ac603eda1bdd782f9a33ee0f4ec55ef1a117 GIT binary patch literal 7046 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/forum.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/forum.png new file mode 100644 index 0000000000000000000000000000000000000000..b2e748c542923700722d8bb6dc268149b38c2842 GIT binary patch literal 9139 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/labs.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/labs.png new file mode 100644 index 0000000000000000000000000000000000000000..736f18d80228660d27d10e18de515ecc16cc6c42 GIT binary patch literal 9628 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≪Xzx_!|lE7GiZ^{VlX}`V@Q_MK8AdidXxK(8g;^3LA=?DCiasvHufxF=`<=1Nix?0; zTg>oa3GqH9y0Dm|!tq5?PA}6U=VOzf*(LT5+vk%`)$ub6boL*9Nu?S8B}X@!kqoEr z)UT!}wIeC|zXsOu$u5;=U)pI&0ECwc`nYquDuSV=r_-5dcyK&IEdb!ee`!fcK1;cl&LZt?Kid%$1t2XuJ!E z2Wrz{_X8|zSDPjEFz@D@;661-vqhFZ{`;m_OxVuZqE43w!|e42>HT6+RDGa+O+zKs zRGDZVX$F=-3{#9R0iQv|mS*yF>_6V1=_6-7&t=2{^a>XgR?@@2 zvvpr|i+Ht!f!dFby?36T{mK!VKEF_Rw@}PdMHoa<@&Pm*AVAvv@GkMb9g;!ry$9|V zF>O68*Lf6>1hQ~=J@zMe(qo_dD6J+~d?q%hV&m@5J%fjG(ssXux zSgOfRjl#A=Rl}owi{ora_PcBrS`B#sKjgN8Z+!cwH2(52T1}Xy`lQ$A9Y2(#8#wrt z=jhoVy;k@8ty>1@mOa~Pc=r$$_YPJnMIg9f&tgRK1X3(BV=sL7HM;P0^PAiXx!Ngq z_Hj4=}mNj-j(8VaMDcyYK#;;_pTz zBi(rKfid4U_u4DB(7o5+RCy1xGXC>-=(*=#tNVK**44Omt+rBvc=kuH)_jK1|L;8dVap`B03;U@3x{r=rS~6z^33z{TYx?eEIOBdurm^ZjkFlUn0Up*E&_lj!#ps0ZrTJ`-TMfT8fuA zemnMF?M=Ab1Bok;GG+Sp<8Qqynuh+qzj<~5_ahHXkPG9yX@Bi;$T zq<8Bj19i7Y;E!Yqh&-}v<(na~C{}3vlVt+LDaBCbST_V2HH1)|sO5gOaf$HKEw!Hb z*(RZ1yWl|KNq5%D3%*h%o5Yi*yRWLf;@)en9B#NzilRbISBe)lK(13o*K4esZSzqB zAa_V>EWuva-V1`^SPiA(2mEa7dpce(RJEsy-r2a1`1fK1*5hm)OBj^R+Q-x)7Ph)d zs;fS-#J5te^fzJca;^0>M|uIMX4O>g@w>fmf_tf|ooJRA*cpt|$NG|@TLb`6SkXsL zI7eduiW#!&vfi^2rG)#Puy%X6`6M(i^U^38(hk^bKIlg^OSCkhR;X&zc#^t=v*&8n zS6`xe=4WqYb43-ykphY601?utn8FUrCzB6$p(wvQ*x%It*2B_r_g1{ey&n?h(mz_B zF|ASQ0215O1$g~<#FWi&c_3O*nJD(lxlrf;LAV5|giYS$nA?lV>8LZeIx!8z`jOsV z!%Ao*o}d=dGQp@Bb9wI}oRB(%m-OZUOhY6}#(1TWcP`2I!{oP%q+RzsY&r^iRaL(jE?*P+58_m#;WXP|ugz z{m~PdtXP~4sFG;5)6E>L+ryiLdR|LNr>YgZ>qOg&S`4QW6C^zAwFAPI2V<9s_!vzE z(qWn_TNx~1S1cmtE4hLBOOXbPm4n=mw5Xb=Jba3+TL!2Tm}cP7cF6@gTD4?zD*iMz z$%XbBmuM0CYN>yEfM= zso~eBa0BEHH>eW%MOPYo+a>vj-~0k~6MqmNqT8ZL`gLKB&XlvXXs%?fq)i48R>Aw; zg+~#v#Ddrlg;Sj;Gdvu)b_)L8A{S`QrEG$kbml=m;-PM=LER0U)^l`0&-1HDi^hsq zEhTh!s9zKi{5H4w1)_sx4gryhMa|eR$#AG(b30Yb)yM2I@El@8F<)4%cApOltWd~! zCov>;m5d3jf*+vB#72 { + const { euiTheme } = useEuiTheme(); + return css` + padding: ${euiTheme.size.m} ${euiTheme.size.l}; + + .footerItemTitle { + font-weight: ${euiTheme.font.weight.semiBold}; + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx new file mode 100644 index 0000000000000..b6c505d68ed94 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { useFooterStyles } from './onboarding_footer.styles'; +import { footerItems } from './footer_items'; +import { PAGE_CONTENT_WIDTH } from '../../constants'; + +export const OnboardingFooter = React.memo(() => { + const styles = useFooterStyles(); + return ( + + + {footerItems.map((item) => ( + + {item.title} + + +

{item.title}

+
+ + {item.description} + + + + {item.link.title} + + +
+ ))} +
+
+ ); +}); +OnboardingFooter.displayName = 'OnboardingFooter'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts index b64522538b519..50a97ef900b90 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts @@ -15,7 +15,7 @@ const LocalStorageKey = { } as const; /** - * Uses local storage to store a value, but if the value is not defined, it will return the default value + * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`. */ const useDefinedLocalStorage = (key: string, defaultValue: T) => { const [value, setValue] = useLocalStorage(key); diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts index 14ac95a0e97fe..f58ffa7a09da5 100644 --- a/x-pack/plugins/security_solution/public/onboarding/types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/types.ts @@ -5,41 +5,57 @@ * 2.0. */ +import type React from 'react'; import type { IconType } from '@elastic/eui'; import type { LicenseType } from '@kbn/licensing-plugin/public'; import type { OnboardingHubCardId } from './constants'; import type { RequiredCapabilities } from '../common/lib/capabilities'; -export type OnboardingHubCardComponent = React.ComponentType<{ +export type OnboardingCardComponent = React.FunctionComponent<{ setComplete: (complete: boolean) => void; }>; -export type OnboardingHubCardCheckComplete = () => Promise; +export type OnboardingCardCheckComplete = () => Promise; -export interface OnboardingHubCardConfig { +export interface OnboardingCardConfig { id: OnboardingHubCardId; - title: string; // i18n + title: string; icon: IconType; /** - * Component to render the card + * Component that renders the card content when expanded. + * It receives a `setComplete` function to allow the card to mark itself as complete if needed. + * Please use React.lazy() to load the component. */ - component: React.ComponentType<{ setComplete: (complete: boolean) => void }>; // use React.lazy() to load the component + Component: OnboardingCardComponent; /** - * for auto-checking completion + * Function for auto-checking completion for the card * @returns Promise for the complete status */ checkComplete?: () => Promise; /** - * Capabilities strings (using object dot notation) to enable the link. + * Capabilities strings using object dot notation to enable the card. e.g. ['siem.crud'] + * + * The format of defining capabilities supports OR and AND mechanism. To specify capabilities in an OR fashion + * they can be defined in a single level array like: [requiredCap1, requiredCap2]. If either of these capabilities + * is satisfied the link would be included. To require that the capabilities be AND'd together a second level array + * can be specified: [cap1, [cap2, cap3]] this would result in cap1 || (cap2 && cap3). To specify + * capabilities that all must be and'd together an example would be: [[cap1, cap2]], this would result in the boolean + * operation cap1 && cap2. + * + * The final format is to specify a single capability, this would be like: capabilities: cap1, which is the same as + * capabilities: [cap1] + * + * Default is `undefined` (no capabilities required) */ - capabilities?: RequiredCapabilities; // check `x-pack/plugins/security_solution/public/common/lib/capabilities/has_capabilities.ts` + capabilities?: RequiredCapabilities; /** - * Minimum license required to enable the card + * Minimum license required to enable the card. + * Default is `basic` */ licenseType?: LicenseType; } -export interface OnboardingHubGroupConfig { +export interface OnboardingGroupConfig { title: string; - cards: OnboardingHubCardConfig[]; + cards: OnboardingCardConfig[]; } From ec4cd4276825ff182928c97a5ed42b6c66ce797e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 10 Sep 2024 15:00:00 +0200 Subject: [PATCH 04/74] implement card utils --- .../common/card_content_image_panel.styles.ts | 27 +++++++ .../cards/common/card_content_image_panel.tsx | 34 ++++++++ .../cards/common/card_content_panel.tsx | 33 -------- .../cards/common/card_content_wrapper.tsx | 17 ++++ .../cards/dashboards/dashboards_card.tsx | 76 ++++++++++++++++-- .../cards/dashboards/images/dashboards.png | Bin 0 -> 49520 bytes .../onboarding_body/cards/dashboards/index.ts | 10 +-- .../cards/dashboards/translations.ts | 15 ++++ .../cards/integrations/index.ts | 4 +- .../cards/integrations/integrations_card.tsx | 6 +- .../onboarding_body/onboarding_body.tsx | 6 +- .../onboarding_body/onboarding_card_panel.tsx | 4 +- .../use_check_complete_cards.ts | 6 +- .../onboarding_body/use_completed_cards.ts | 6 +- .../onboarding_body/use_expanded_card.ts | 4 +- .../onboarding/components/use_stored_state.ts | 6 +- .../public/onboarding/constants.ts | 2 +- .../public/onboarding/types.ts | 4 +- 18 files changed, 190 insertions(+), 70 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts new file mode 100644 index 0000000000000..16e3c3820b310 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts @@ -0,0 +1,27 @@ +/* + * 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 { css } from '@emotion/css'; +import { useEuiTheme, useEuiShadow } from '@elastic/eui'; + +export const useCardContentImagePanelStyles = () => { + const { euiTheme } = useEuiTheme(); + const shadowStyles = useEuiShadow('m'); + return css` + .cardSpacer { + width: 10%; + } + .cardImage { + width: 50%; + img { + width: 100%; + border-radius: ${euiTheme.size.s}; + ${shadowStyles} + } + } + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx new file mode 100644 index 0000000000000..265a586c220e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx @@ -0,0 +1,34 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { OnboardingCardContentWrapper } from './card_content_wrapper'; +import { useCardContentImagePanelStyles } from './card_content_image_panel.styles'; + +export const IMAGE_WIDTH = 540; + +export const OnboardingCardContentImagePanel = React.memo< + PropsWithChildren<{ imageSrc: string; imageAlt: string }> +>(({ children, imageSrc, imageAlt }) => { + const styles = useCardContentImagePanelStyles(); + return ( + + + + {children} + + + + + {imageAlt} + + + + + ); +}); +OnboardingCardContentImagePanel.displayName = 'OnboardingCardContentImagePanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx deleted file mode 100644 index ef88aacc08679..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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, { type PropsWithChildren } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; - -export const OnboardingCardContentPanel = React.memo>(({ children }) => { - return ( - - {children} - - ); -}); -OnboardingCardContentPanel.displayName = 'OnboardingCardContentPanel'; - -export const OnboardingCardContentImagePanel = React.memo< - PropsWithChildren<{ imageSrc: string; imageAlt: string }> ->(({ children, imageSrc, imageAlt }) => { - return ( - - - {children} - - {imageAlt} - - - - ); -}); -OnboardingCardContentImagePanel.displayName = 'OnboardingCardContentImagePanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx new file mode 100644 index 0000000000000..142ff2ef98184 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx @@ -0,0 +1,17 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import { EuiPanel } from '@elastic/eui'; + +export const OnboardingCardContentWrapper = React.memo>(({ children }) => { + return ( + + {children} + + ); +}); +OnboardingCardContentWrapper.displayName = 'OnboardingCardContentWrapper'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx index 24918ebd7dbcf..51b78e0672257 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx @@ -6,17 +6,79 @@ */ import React from 'react'; -import { EuiButton } from '@elastic/eui'; +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; -import { OnboardingCardContentPanel } from '../common/card_content_panel'; +import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; +import { DASHBOARDS_CARD_TITLE } from './translations'; +import dashboardsImageSrc from './images/dashboards.png'; -export const IntegrationsCard: OnboardingCardComponent = ({ setComplete }) => { +export const DashboardsCard: OnboardingCardComponent = ({ setComplete }) => { return ( - - setComplete(false)}>{'Set not complete'} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setComplete(false)} fill> + + + + + ); }; // eslint-disable-next-line import/no-default-export -export default IntegrationsCard; +export default DashboardsCard; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards.png new file mode 100644 index 0000000000000000000000000000000000000000..0d6b551e096613c60ac1cee0aedd73c966f3493a GIT binary patch literal 49520 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts index 9293e16af84b7..b09a458847475 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts @@ -6,15 +6,13 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import type { OnboardingCardConfig } from '../../../../types'; -import { OnboardingHubCardId } from '../../../../constants'; +import { OnboardingCardId } from '../../../../constants'; +import { DASHBOARDS_CARD_TITLE } from './translations'; export const dashboardsCardConfig: OnboardingCardConfig = { - id: OnboardingHubCardId.dashboards, - title: i18n.translate('xpack.securitySolution.onboarding.dashboardsCard.title', { - defaultMessage: 'View and analyze your data using dashboards', - }), + id: OnboardingCardId.dashboards, + title: DASHBOARDS_CARD_TITLE, icon: 'dashboardApp', Component: React.lazy( () => import('./dashboards_card' /* webpackChunkName: "onboarding_dashboards_card" */) diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts new file mode 100644 index 0000000000000..b93f3e4f0889a --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts @@ -0,0 +1,15 @@ +/* + * 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 DASHBOARDS_CARD_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.dashboardsCard.title', + { + defaultMessage: 'View and analyze your data using dashboards', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts index 37a42e35b4c19..6da21d4e54581 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts @@ -9,10 +9,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import type { OnboardingCardConfig } from '../../../../types'; import { checkIntegrationsCardComplete } from './integrations_check_complete'; -import { OnboardingHubCardId } from '../../../../constants'; +import { OnboardingCardId } from '../../../../constants'; export const integrationsCardConfig: OnboardingCardConfig = { - id: OnboardingHubCardId.integrations, + id: OnboardingCardId.integrations, title: i18n.translate('xpack.securitySolution.onboarding.integrationsCard.title', { defaultMessage: 'Add data with integrations', }), diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx index 24918ebd7dbcf..bbe631316061b 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import type { OnboardingCardComponent } from '../../../../types'; -import { OnboardingCardContentPanel } from '../common/card_content_panel'; +import { OnboardingCardContentWrapper } from '../common/card_content_wrapper'; export const IntegrationsCard: OnboardingCardComponent = ({ setComplete }) => { return ( - + setComplete(false)}>{'Set not complete'} - + ); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index 6f4fb534d4bd2..dd8a1ea695128 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -9,7 +9,7 @@ import React, { Suspense, useCallback, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { css } from '@emotion/react'; -import { PAGE_CONTENT_WIDTH, type OnboardingHubCardId } from '../../constants'; +import { PAGE_CONTENT_WIDTH, type OnboardingCardId } from '../../constants'; import { useCardGroupsConfig } from './use_card_groups_config'; import { useOnboardingContext } from '../onboarding_context'; import { OnboardingCardGroup } from './onboarding_card_group'; @@ -37,7 +37,7 @@ export const OnboardingBody = React.memo(() => { }, [checkAllCardsComplete]); const createOnToggleExpanded = useCallback( - (cardId: OnboardingHubCardId) => () => { + (cardId: OnboardingCardId) => () => { if (expandedCardId === cardId) { setExpandedCardId(null); } else { @@ -50,7 +50,7 @@ export const OnboardingBody = React.memo(() => { ); const createSetCardComplete = useCallback( - (cardId: OnboardingHubCardId) => (complete: boolean) => { + (cardId: OnboardingCardId) => (complete: boolean) => { setCardComplete(cardId, complete); }, [setCardComplete] diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx index 3a56ccf7edf23..c5ea0d01c10cb 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx @@ -17,12 +17,12 @@ import { EuiTitle, } from '@elastic/eui'; import classnames from 'classnames'; -import type { OnboardingHubCardId } from '../../constants'; +import type { OnboardingCardId } from '../../constants'; import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from './translations'; import { useCardPanelStyles } from './onboarding_card_panel.styles'; interface OnboardingCardPanelProps { - id: OnboardingHubCardId; + id: OnboardingCardId; title: string; icon: IconType; isExpanded: boolean; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts index 410636225c4ed..7f2ccdacab987 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_check_complete_cards.ts @@ -6,7 +6,7 @@ */ import { useCallback, useMemo } from 'react'; -import type { OnboardingHubCardId } from '../../constants'; +import type { OnboardingCardId } from '../../constants'; import type { OnboardingCardConfig, OnboardingGroupConfig } from '../../types'; /** @@ -14,7 +14,7 @@ import type { OnboardingCardConfig, OnboardingGroupConfig } from '../../types'; **/ export const useCheckCompleteCards = ( cardsGroupConfig: OnboardingGroupConfig[], - setCardComplete: (cardId: OnboardingHubCardId, complete: boolean) => void + setCardComplete: (cardId: OnboardingCardId, complete: boolean) => void ) => { // Stores all cards that have a checkComplete function in a flat array const cardsWithCheckComplete = useMemo( @@ -37,7 +37,7 @@ export const useCheckCompleteCards = ( // Exported function to run the check for a specific card const checkCardComplete = useCallback( - (cardId: OnboardingHubCardId) => { + (cardId: OnboardingCardId) => { const cardConfig = cardsWithCheckComplete.find(({ id }) => id === cardId); if (cardConfig) { diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts index b40bcfcaa6ad9..1638abb13ee5c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_completed_cards.ts @@ -7,7 +7,7 @@ import { useCallback } from 'react'; import { useStoredCompletedCardIds } from '../use_stored_state'; -import type { OnboardingHubCardId } from '../../constants'; +import type { OnboardingCardId } from '../../constants'; /** * This hook implements the logic for tracking which onboarding cards have been completed using Local Storage. @@ -16,12 +16,12 @@ export const useCompletedCards = (spaceId: string) => { const [completeCardIds, setCompleteCardIds] = useStoredCompletedCardIds(spaceId); const isCardComplete = useCallback( - (cardId: OnboardingHubCardId) => completeCardIds.includes(cardId), + (cardId: OnboardingCardId) => completeCardIds.includes(cardId), [completeCardIds] ); const setCardComplete = useCallback( - (cardId: OnboardingHubCardId, complete: boolean) => { + (cardId: OnboardingCardId, complete: boolean) => { if (complete) { setCompleteCardIds((currentCompleteCards = []) => [ ...new Set([...currentCompleteCards, cardId]), diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts index 67a6177f0bb3f..eb19baf2e2132 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts @@ -9,7 +9,7 @@ import { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useStoredExpandedCardId } from '../use_stored_state'; import { HEIGHT_ANIMATION_DURATION } from './onboarding_card_panel.styles'; -import type { OnboardingHubCardId } from '../../constants'; +import type { OnboardingCardId } from '../../constants'; const HEADER_OFFSET = 40; @@ -36,7 +36,7 @@ export const useExpandedCard = (spaceId: string) => { // This effect implements auto-scroll in the initial render, further changes in the hash should not trigger this effect useEffect(() => { if (documentReadyState !== 'complete') return; // Wait for page to finish loading before scrolling - let cardIdFromHash = location.hash.split('?')[0].replace('#', '') as OnboardingHubCardId | ''; + let cardIdFromHash = location.hash.split('?')[0].replace('#', '') as OnboardingCardId | ''; if (!cardIdFromHash) { if (expandedCardId == null) return; // Use the expanded card id if we have it stored and the hash is empty. diff --git a/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts index 50a97ef900b90..db6f0b72d04e0 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/use_stored_state.ts @@ -6,7 +6,7 @@ */ import { useLocalStorage } from 'react-use'; -import type { OnboardingHubCardId } from '../constants'; +import type { OnboardingCardId } from '../constants'; const LocalStorageKey = { completeCards: 'ONBOARDING_HUB.COMPLETE_CARDS', @@ -23,10 +23,10 @@ const useDefinedLocalStorage = (key: string, defaultValue: T) => { }; export const useStoredCompletedCardIds = (spaceId: string) => - useDefinedLocalStorage(`${LocalStorageKey.completeCards}.${spaceId}`, []); + useDefinedLocalStorage(`${LocalStorageKey.completeCards}.${spaceId}`, []); export const useStoredExpandedCardId = (spaceId: string) => - useDefinedLocalStorage( + useDefinedLocalStorage( `${LocalStorageKey.expandedCard}.${spaceId}`, null ); diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts index 2888f41978412..039b83f754093 100644 --- a/x-pack/plugins/security_solution/public/onboarding/constants.ts +++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts @@ -6,7 +6,7 @@ */ export const PAGE_CONTENT_WIDTH = '1150px'; -export enum OnboardingHubCardId { +export enum OnboardingCardId { integrations = 'integrations', dashboards = 'dashboards', rules = 'rules', diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts index f58ffa7a09da5..d387ef79db9e8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/types.ts @@ -8,7 +8,7 @@ import type React from 'react'; import type { IconType } from '@elastic/eui'; import type { LicenseType } from '@kbn/licensing-plugin/public'; -import type { OnboardingHubCardId } from './constants'; +import type { OnboardingCardId } from './constants'; import type { RequiredCapabilities } from '../common/lib/capabilities'; export type OnboardingCardComponent = React.FunctionComponent<{ @@ -18,7 +18,7 @@ export type OnboardingCardComponent = React.FunctionComponent<{ export type OnboardingCardCheckComplete = () => Promise; export interface OnboardingCardConfig { - id: OnboardingHubCardId; + id: OnboardingCardId; title: string; icon: IconType; /** From bac2c8d86e3dfb45c2cc138bc361650f08d8261f Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Wed, 11 Sep 2024 10:37:10 +0200 Subject: [PATCH 05/74] finish dashboard card and card callout --- .../cards/common/card_callout.styles.ts | 18 +++ .../cards/common/card_callout.tsx | 46 +++++++ .../cards/common/card_content_image_panel.tsx | 28 ++--- .../cards/common/card_content_panel.tsx | 21 ++++ .../cards/common/card_content_wrapper.tsx | 17 --- .../cards/dashboards/dashboards_card.tsx | 114 ++++++++++-------- .../cards/dashboards/translations.ts | 26 ++++ .../cards/integrations/integrations_card.tsx | 6 +- .../onboarding_body/onboarding_body.tsx | 6 +- .../onboarding_body/use_card_groups_config.ts | 4 +- .../onboarding_body/use_expanded_card.ts | 56 +++++---- .../public/onboarding/types.ts | 31 +++-- 12 files changed, 250 insertions(+), 123 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx delete mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts new file mode 100644 index 0000000000000..26766b1c77da8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts @@ -0,0 +1,18 @@ +/* + * 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 { css } from '@emotion/css'; +import { useEuiTheme } from '@elastic/eui'; + +export const useCardCalloutStyles = () => { + const { euiTheme } = useEuiTheme(); + return css` + padding: ${euiTheme.size.s}; + border: 1px solid ${euiTheme.colors.lightShade}; + border-radius: ${euiTheme.size.s}; + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx new file mode 100644 index 0000000000000..84b8fedef19ec --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import type { EuiCallOutProps, IconType } from '@elastic/eui'; +import { useCardCalloutStyles } from './card_callout.styles'; + +export interface CardCalloutProps { + text: string; + color?: EuiCallOutProps['color']; + icon?: IconType; + action?: React.ReactNode; +} + +export const CardCallout = React.memo(({ text, color, icon, action }) => { + const styles = useCardCalloutStyles(); + return ( + + + + + {icon && ( + + + + )} + + {text} + + + + {action && ( + + {action} + + )} + + + ); +}); +CardCallout.displayName = 'CardCallout'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx index 265a586c220e3..8b9fd9d9cc00c 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx @@ -5,8 +5,8 @@ * 2.0. */ import React, { type PropsWithChildren } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { OnboardingCardContentWrapper } from './card_content_wrapper'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { OnboardingCardContentPanel } from './card_content_panel'; import { useCardContentImagePanelStyles } from './card_content_image_panel.styles'; export const IMAGE_WIDTH = 540; @@ -16,19 +16,17 @@ export const OnboardingCardContentImagePanel = React.memo< >(({ children, imageSrc, imageAlt }) => { const styles = useCardContentImagePanelStyles(); return ( - - - - {children} - - - - - {imageAlt} - - - - + + + {children} + + + + + {imageAlt} + + + ); }); OnboardingCardContentImagePanel.displayName = 'OnboardingCardContentImagePanel'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx new file mode 100644 index 0000000000000..c4039955e4216 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx @@ -0,0 +1,21 @@ +/* + * 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, { type PropsWithChildren } from 'react'; +import { EuiPanel, type EuiPanelProps } from '@elastic/eui'; + +export const OnboardingCardContentPanel = React.memo>( + ({ children, ...panelProps }) => { + return ( + + + {children} + + + ); + } +); +OnboardingCardContentPanel.displayName = 'OnboardingCardContentWrapper'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx deleted file mode 100644 index 142ff2ef98184..0000000000000 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_wrapper.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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, { type PropsWithChildren } from 'react'; -import { EuiPanel } from '@elastic/eui'; - -export const OnboardingCardContentWrapper = React.memo>(({ children }) => { - return ( - - {children} - - ); -}); -OnboardingCardContentWrapper.displayName = 'OnboardingCardContentWrapper'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx index 51b78e0672257..81eb118e93ec2 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx @@ -5,27 +5,32 @@ * 2.0. */ -import React from 'react'; -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; +import { SecuritySolutionLinkButton } from '../../../../../common/components/links'; import { OnboardingCardId } from '../../../../constants'; import type { OnboardingCardComponent } from '../../../../types'; import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel'; -import { DASHBOARDS_CARD_TITLE } from './translations'; +import { CardCallout } from '../common/card_callout'; import dashboardsImageSrc from './images/dashboards.png'; +import * as i18n from './translations'; + +export const DashboardsCard: OnboardingCardComponent = ({ isCardComplete, setExpandedCardId }) => { + const isIntegrationsCardComplete = useMemo( + () => isCardComplete(OnboardingCardId.integrations), + [isCardComplete] + ); + + const expandIntegrationsCard = useCallback(() => { + setExpandedCardId(OnboardingCardId.integrations, { scroll: true }); + }, [setExpandedCardId]); -export const DashboardsCard: OnboardingCardComponent = ({ setComplete }) => { return ( - + { > - + {i18n.DASHBOARDS_CARD_DESCRIPTION} - - - - - - - - - - - - - - - - + {!isIntegrationsCardComplete && ( + <> + + + + {i18n.DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + - - - - + } + /> + {/* + + + + + {i18n.DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_TEXT} + + + + + {i18n.DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON} + + + + + */} + + )} - setComplete(false)} fill> - - + + {i18n.DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON} + diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts index b93f3e4f0889a..33d7a2a9be98b 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts @@ -13,3 +13,29 @@ export const DASHBOARDS_CARD_TITLE = i18n.translate( defaultMessage: 'View and analyze your data using dashboards', } ); + +export const DASHBOARDS_CARD_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.dashboardsCard.description', + { + defaultMessage: + "Use dashboards to visualize data and stay up-to-date with key information. Create your own, or use Elastic's default dashboards — including alerts, user authentication events, known vulnerabilities, and more.", + } +); +export const DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate( + 'xpack.securitySolution.onboarding.dashboardsCard.calloutIntegrationsText', + { + defaultMessage: 'To view dashboards add integrations first.', + } +); +export const DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.dashboardsCard.calloutIntegrationsButton', + { + defaultMessage: 'Add integrations', + } +); +export const DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.dashboardsCard.goToDashboardsButton', + { + defaultMessage: 'Go to dashboards', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx index bbe631316061b..24918ebd7dbcf 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import type { OnboardingCardComponent } from '../../../../types'; -import { OnboardingCardContentWrapper } from '../common/card_content_wrapper'; +import { OnboardingCardContentPanel } from '../common/card_content_panel'; export const IntegrationsCard: OnboardingCardComponent = ({ setComplete }) => { return ( - + setComplete(false)}>{'Set not complete'} - + ); }; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index dd8a1ea695128..cfadf651dbee8 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -82,7 +82,11 @@ export const OnboardingBody = React.memo(() => { onToggleExpanded={createOnToggleExpanded(id)} > }> - + diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts index f7cb67ba34365..7ac9083e80c69 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_card_groups_config.ts @@ -19,8 +19,8 @@ export const useCardGroupsConfig = () => { const license = useObservable(licensing.license$); const filteredCardGroupsConfig = useMemo(() => { - // If the license is not defined, return an empty array. It always eventually becomes available. - // This exit case is just to prevent config-dependent code to run multiple times for each card. + // Return empty array when the license is not defined. It should always become defined at some point. + // This exit case prevents code dependant on the cards config from running multiple times. if (!license) { return []; } diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts index eb19baf2e2132..773fa661c6f7a 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/use_expanded_card.ts @@ -5,24 +5,34 @@ * 2.0. */ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useStoredExpandedCardId } from '../use_stored_state'; import { HEIGHT_ANIMATION_DURATION } from './onboarding_card_panel.styles'; import type { OnboardingCardId } from '../../constants'; +import type { SetExpandedCardId } from '../../types'; const HEADER_OFFSET = 40; +const scrollToCard = (cardId: OnboardingCardId) => { + setTimeout(() => { + const element = document.getElementById(cardId); + if (element) { + element.focus({ preventScroll: true }); + window.scrollTo({ top: element.offsetTop - HEADER_OFFSET, behavior: 'smooth' }); + } + }, HEIGHT_ANIMATION_DURATION); +}; + +const setHash = (cardId: OnboardingCardId | null) => { + history.replaceState(null, '', cardId == null ? ' ' : `#${cardId}`); +}; + /** * This hook manages the expanded card id state in the LocalStorage and the hash in the URL. - * Scenarios in the initial render: - * - Hash not defined and LS not defined: No card expanded and no scroll - * - Hash not defined and LS defined: Update hash with LS value and scroll to the card - * - Hash defined and LS not defined: Update LS with hash value and scroll to the card - * - Hash defined and LS defined: The hash value takes precedence, update LS if different and scroll to the card */ export const useExpandedCard = (spaceId: string) => { - const [expandedCardId, setExpandedCardId] = useStoredExpandedCardId(spaceId); + const [expandedCardId, setStorageExpandedCardId] = useStoredExpandedCardId(spaceId); const location = useLocation(); const [documentReadyState, setReadyState] = useState(document.readyState); @@ -39,31 +49,29 @@ export const useExpandedCard = (spaceId: string) => { let cardIdFromHash = location.hash.split('?')[0].replace('#', '') as OnboardingCardId | ''; if (!cardIdFromHash) { if (expandedCardId == null) return; - // Use the expanded card id if we have it stored and the hash is empty. - // The hash will be synched by the effect below + // If the hash is empty, but it is defined the storage we use the storage value cardIdFromHash = expandedCardId; + setHash(cardIdFromHash); } - // If the hash is different from the expanded card id, update the expanded card id (fresh load) + // If the hash is defined and different from the storage, the hash takes precedence if (expandedCardId !== cardIdFromHash) { - setExpandedCardId(cardIdFromHash); + setStorageExpandedCardId(cardIdFromHash); } - - setTimeout(() => { - const element = document.getElementById(cardIdFromHash); - if (element) { - element.focus({ preventScroll: true }); // Scrolling already handled below - window.scrollTo({ top: element.offsetTop - HEADER_OFFSET, behavior: 'smooth' }); - } - }, HEIGHT_ANIMATION_DURATION); - + scrollToCard(cardIdFromHash); // eslint-disable-next-line react-hooks/exhaustive-deps }, [documentReadyState]); - // Syncs the expanded card id with the hash, it does not trigger the scrolling effect - useEffect(() => { - history.replaceState(null, '', expandedCardId == null ? ' ' : `#${expandedCardId}`); - }, [expandedCardId]); + const setExpandedCardId = useCallback( + (cardId, options) => { + setStorageExpandedCardId(cardId); + setHash(cardId); + if (cardId && options?.scroll) { + scrollToCard(cardId); + } + }, + [setStorageExpandedCardId] + ); return { expandedCardId, setExpandedCardId }; }; diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts index d387ef79db9e8..afd90922a90cf 100644 --- a/x-pack/plugins/security_solution/public/onboarding/types.ts +++ b/x-pack/plugins/security_solution/public/onboarding/types.ts @@ -11,8 +11,17 @@ import type { LicenseType } from '@kbn/licensing-plugin/public'; import type { OnboardingCardId } from './constants'; import type { RequiredCapabilities } from '../common/lib/capabilities'; +export type SetComplete = (complete: boolean) => void; +export type IsCardComplete = (cardId: OnboardingCardId) => boolean; +export type SetExpandedCardId = ( + cardId: OnboardingCardId | null, + options?: { scroll?: boolean } +) => void; + export type OnboardingCardComponent = React.FunctionComponent<{ - setComplete: (complete: boolean) => void; + setComplete: SetComplete; + isCardComplete: IsCardComplete; + setExpandedCardId: SetExpandedCardId; }>; export type OnboardingCardCheckComplete = () => Promise; @@ -33,17 +42,19 @@ export interface OnboardingCardConfig { */ checkComplete?: () => Promise; /** - * Capabilities strings using object dot notation to enable the card. e.g. ['siem.crud'] + * The RBAC capability strings required to enable the card. It uses object dot notation. e.g. `'siem.crud'`. + * + * The format of the capabilities property supports OR and AND mechanism: + * + * To specify capabilities in an OR fashion, they can be defined in a single level array like: `capabilities: [cap1, cap2]`. + * If either of "cap1 || cap2" is granted the card will be included. + * + * To specify capabilities with AND conditional, use a second level array: `capabilities: [['cap1', 'cap2']]`. + * This would result in the boolean expression "cap1 && cap2", both capabilities must be granted to include the card. * - * The format of defining capabilities supports OR and AND mechanism. To specify capabilities in an OR fashion - * they can be defined in a single level array like: [requiredCap1, requiredCap2]. If either of these capabilities - * is satisfied the link would be included. To require that the capabilities be AND'd together a second level array - * can be specified: [cap1, [cap2, cap3]] this would result in cap1 || (cap2 && cap3). To specify - * capabilities that all must be and'd together an example would be: [[cap1, cap2]], this would result in the boolean - * operation cap1 && cap2. + * They can also be combined like: `capabilities: ['cap1', ['cap2', 'cap3']]` which would result in the boolean expression "cap1 || (cap2 && cap3)". * - * The final format is to specify a single capability, this would be like: capabilities: cap1, which is the same as - * capabilities: [cap1] + * For the single capability requirement: `capabilities: 'cap1'`, which is the same as `capabilities: ['cap1']` * * Default is `undefined` (no capabilities required) */ From c6c077b526990a6a454b24e6d36393d860b72fea Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 13 Sep 2024 09:00:17 +0200 Subject: [PATCH 06/74] onboarding sub-plugin finished --- .../components/empty_prompt/empty_prompt.tsx | 4 +- .../content/data_ingestion_hub_video.tsx | 4 +- .../public/common/constants.ts | 2 +- .../onboarding/components/onboarding.tsx | 21 +++- .../onboarding_body/onboarding_body.tsx | 79 ++++++--------- .../onboarding_footer/onboarding_footer.tsx | 47 ++++----- .../cards/common/link_card.styles.ts | 42 ++++++++ .../cards/common/link_card.test.tsx | 44 ++++++++ .../cards/common/link_card.tsx | 62 ++++++++++++ .../cards/demo_card/demo_card.tsx | 28 ++++++ .../cards/demo_card/images/demo_card.png | Bin 0 -> 6826 bytes .../cards/demo_card/images/demo_card_dark.png | Bin 0 -> 9548 bytes .../cards/demo_card/index.ts} | 2 +- .../cards/demo_card/translations.ts | 29 ++++++ .../teammates_card/images/teammates_card.png | Bin 0 -> 10464 bytes .../images/teammates_card_dark.png | Bin 0 -> 10901 bytes .../cards/teammates_card/index.ts | 8 ++ .../cards/teammates_card/teammates_card.tsx | 30 ++++++ .../cards/teammates_card/translations.ts | 29 ++++++ .../cards/video_card/images/video_card.png | Bin 0 -> 6645 bytes .../video_card/images/video_card_dark.png | Bin 0 -> 5298 bytes .../cards/video_card/index.ts | 8 ++ .../cards/video_card/translations.ts | 65 ++++++++++++ .../cards/video_card/video_card.tsx | 40 ++++++++ .../cards/video_card/video_modal.test.tsx | 35 +++++++ .../cards/video_card/video_modal.tsx | 95 ++++++++++++++++++ .../images/header_rocket.png | Bin 0 -> 24547 bytes .../images/header_rocket_dark.png | Bin 0 -> 27638 bytes .../onboarding_header.styles.ts | 22 ++++ .../onboarding_header/onboarding_header.tsx | 70 +++++++++---- .../onboarding_header/translations.ts | 28 ++++++ .../onboarding/components/use_stored_state.ts | 22 +++- .../public/onboarding/types.ts | 2 +- 33 files changed, 713 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card_dark.png rename x-pack/plugins/security_solution/public/{common/components/empty_prompt/constants.ts => onboarding/components/onboarding_header/cards/demo_card/index.ts} (76%) create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/images/teammates_card.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/images/teammates_card_dark.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card_dark.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.test.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/images/header_rocket.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/images/header_rocket_dark.png create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts create mode 100644 x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx index 6b36f64d27ea4..8501d01dee5a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx @@ -23,7 +23,7 @@ import endpointSvg from './images/endpoint1.svg'; import cloudSvg from './images/cloud1.svg'; import siemSvg from './images/siem1.svg'; import { useNavigateTo } from '../../lib/kibana'; -import { VIDEO_SOURCE } from './constants'; +import { ONBOARDING_VIDEO_SOURCE } from '../../constants'; import { AddIntegrationsSteps } from '../landing_page/onboarding/types'; const imgUrls = { @@ -115,7 +115,7 @@ export const EmptyPromptComponent = memo(() => { referrerPolicy="no-referrer" sandbox="allow-scripts allow-same-origin" scrolling="no" - src={VIDEO_SOURCE} + src={ONBOARDING_VIDEO_SOURCE} title={i18n.SIEM_HEADER} width="100%" /> diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/data_ingestion_hub_video.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/data_ingestion_hub_video.tsx index 181d4a797ebbc..221bb0810d62f 100644 --- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/data_ingestion_hub_video.tsx +++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/data_ingestion_hub_video.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useCallback } from 'react'; -import { INGESTION_HUB_VIDEO_SOURCE } from '../../../../../constants'; +import { ONBOARDING_VIDEO_SOURCE } from '../../../../../constants'; import { WATCH_VIDEO_BUTTON_TITLE } from '../../translations'; const VIDEO_CONTENT_HEIGHT = 309; @@ -65,7 +65,7 @@ const DataIngestionHubVideoComponent: React.FC = () => { sandbox="allow-scripts allow-same-origin" scrolling="no" allow={isVideoPlaying ? 'autoplay;' : undefined} - src={`${INGESTION_HUB_VIDEO_SOURCE}${isVideoPlaying ? '?autoplay=1' : ''}`} + src={`${ONBOARDING_VIDEO_SOURCE}${isVideoPlaying ? '?autoplay=1' : ''}`} title={WATCH_VIDEO_BUTTON_TITLE} /> )} diff --git a/x-pack/plugins/security_solution/public/common/constants.ts b/x-pack/plugins/security_solution/public/common/constants.ts index 8590646082a9c..c114f70915a75 100644 --- a/x-pack/plugins/security_solution/public/common/constants.ts +++ b/x-pack/plugins/security_solution/public/common/constants.ts @@ -17,4 +17,4 @@ export const RISK_SCORE_MEDIUM = 47; export const RISK_SCORE_HIGH = 73; export const RISK_SCORE_CRITICAL = 99; -export const INGESTION_HUB_VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?'; +export const ONBOARDING_VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx index 2705abdb112ef..e0288eafda119 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx @@ -7,6 +7,8 @@ import React from 'react'; +import { EuiSpacer, useEuiTheme } from '@elastic/eui'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { PluginTemplateWrapper } from '../../common/components/plugin_template_wrapper'; import { CenteredLoadingSpinner } from '../../common/components/centered_loading_spinner'; import { useSpaceId } from '../../common/hooks/use_space_id'; @@ -15,9 +17,11 @@ import { OnboardingAVCBanner } from './onboarding_avc_banner'; import { OnboardingHeader } from './onboarding_header'; import { OnboardingBody } from './onboarding_body'; import { OnboardingFooter } from './onboarding_footer'; +import { PAGE_CONTENT_WIDTH } from '../constants'; export const OnboardingPage = React.memo(() => { const spaceId = useSpaceId(); + const { euiTheme } = useEuiTheme(); if (!spaceId) { return ( @@ -31,9 +35,20 @@ export const OnboardingPage = React.memo(() => { - - - + + + + + + + + ); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx index cfadf651dbee8..a7e3bff6149c0 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx @@ -6,10 +6,8 @@ */ import React, { Suspense, useCallback, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, useEuiTheme } from '@elastic/eui'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import { css } from '@emotion/react'; -import { PAGE_CONTENT_WIDTH, type OnboardingCardId } from '../../constants'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import type { OnboardingCardId } from '../../constants'; import { useCardGroupsConfig } from './use_card_groups_config'; import { useOnboardingContext } from '../onboarding_context'; import { OnboardingCardGroup } from './onboarding_card_group'; @@ -19,7 +17,6 @@ import { useExpandedCard } from './use_expanded_card'; import { useCompletedCards } from './use_completed_cards'; export const OnboardingBody = React.memo(() => { - const { euiTheme } = useEuiTheme(); const { spaceId } = useOnboardingContext(); const cardGroupsConfig = useCardGroupsConfig(); @@ -57,47 +54,37 @@ export const OnboardingBody = React.memo(() => { ); return ( - - - {cardGroupsConfig.map((group, index) => ( - - - - {group.cards.map(({ id, title, icon, Component: LazyCardComponent }) => ( - - - }> - - - - - ))} - - - - ))} - - - + + {cardGroupsConfig.map((group, index) => ( + + + + + {group.cards.map(({ id, title, icon, Component: LazyCardComponent }) => ( + + + }> + + + + + ))} + + + + ))} + ); }); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx index b6c505d68ed94..af0f7bdd6e5ef 100644 --- a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx @@ -7,40 +7,31 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useFooterStyles } from './onboarding_footer.styles'; import { footerItems } from './footer_items'; -import { PAGE_CONTENT_WIDTH } from '../../constants'; export const OnboardingFooter = React.memo(() => { const styles = useFooterStyles(); return ( - - - {footerItems.map((item) => ( - - {item.title} - - -

{item.title}

-
- - {item.description} - - - - {item.link.title} - - -
- ))} -
-
+ + {footerItems.map((item) => ( + + {item.title} + + +

{item.title}

+
+ + {item.description} + + + + {item.link.title} + + +
+ ))} +
); }); OnboardingFooter.displayName = 'OnboardingFooter'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts new file mode 100644 index 0000000000000..c39c9b458f478 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts @@ -0,0 +1,42 @@ +/* + * 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 { COLOR_MODES_STANDARD, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; + +export const useCardStyles = () => { + const { euiTheme, colorMode } = useEuiTheme(); + const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark; + + return css` + min-width: 315px; + + /* We needed to add the "headerCard" class to make it take priority over the default EUI card styles */ + &.headerCard:hover { + *:not(.headerCardLink) { + text-decoration: none; + } + .headerCardLink, + .headerCardLink * { + text-decoration: underline; + text-decoration-color: ${euiTheme.colors.primaryText}; + } + } + + .headerCardTitle { + font-weight: ${euiTheme.font.weight.semiBold}; + } + + ${isDarkMode + ? ` + background-color: ${euiTheme.colors.lightestShade}; + box-shadow: none; + border: 1px solid ${euiTheme.colors.mediumShade}; + ` + : ''} + `; +}; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx new file mode 100644 index 0000000000000..7499e3feaf5f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 { render } from '@testing-library/react'; +import { LinkCard } from './link_card'; + +describe('DataIngestionHubHeaderCardComponent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the title, description, and icon', () => { + const { getByTestId, getByText } = render( + + ); + + expect(getByText('Mock Title')).toBeInTheDocument(); + expect(getByText('Mock Description')).toBeInTheDocument(); + expect(getByTestId('data-ingestion-header-card-icon')).toHaveAttribute('src', 'mockIcon.png'); + }); + + it('should apply dark mode styles when color mode is DARK', () => { + const { container } = render( + + ); + const cardElement = container.querySelector('.euiCard'); + expect(cardElement).toHaveStyle('background-color:rgb(255, 255, 255)'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx new file mode 100644 index 0000000000000..95ce9fbaf05b5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx @@ -0,0 +1,62 @@ +/* + * 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 { EuiCard, EuiImage, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import classNames from 'classnames'; +import { useCardStyles } from './link_card.styles'; + +interface LinkCardProps { + icon: string; + title: string; + description: string; + linkText: string; + onClick?: () => void; + href?: string; + target?: string; +} + +export const LinkCard: React.FC = React.memo( + ({ icon, title, description, onClick, href, target, linkText }) => { + const cardStyles = useCardStyles(); + const cardClassName = classNames(cardStyles, 'headerCard'); + return ( + + } + title={ + +

{title}

+
+ } + description={{description}} + > + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + {linkText} + + +
+ ); + } +); + +LinkCard.displayName = 'LinkCard'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx new file mode 100644 index 0000000000000..d3c76b5c962a7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx @@ -0,0 +1,28 @@ +/* + * 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 { LinkCard } from '../common/link_card'; +import demoImage from './images/demo_card.png'; +import darkDemoImage from './images/demo_card_dark.png'; +import * as i18n from './translations'; + +const demoUrl = 'https://www.elastic.co/demo-gallery/security-overview'; + +export const DemoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => { + return ( + + ); +}); +DemoCard.displayName = 'DemoCard'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card.png new file mode 100644 index 0000000000000000000000000000000000000000..661363b4538884eaf8468895dd47c2ba646486c2 GIT binary patch literal 6826 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card_dark.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..506f7fa2ecfbdc0b62230b57dd4876cdce25d66d GIT binary patch literal 9548 zcmV-SC9~RzP)tb1yElaYKjYeL(@zw#Nen_yp#ct#eTV(5CMSn;?Eg))v zVi)TVDDQuO?Z0cYMh?`6*OKhN#lL~_U>ha7UqA%`49JhI9W2n2i+6XKheeE+P>`tw5K ztFNo~)zz=9EU%~-x_r4qnw$M{jSzlFEt<=^vNLS}|+5Pl0b7dhxet!Pk=h4woxp?6d zsj9A#(vmW1P{Bij!GZhI_(YTR_1%@bce+j4mc0k`T(5MT{m5LWqQWQn1^K~|q2Z4F zg1q<2OUl}R`qQ60wxoFMNPv2OFrIf*B=qg(<_!T_tVLJXbtA+&B>~52>3x0oWW$Ed zru9F3IH6^3NL5vhDZ_UVV|Xa2f^U}3E}t``7*m2EYycthz4_wRa$fyEG7^;9+6I}K znN}@wRmv+W4T%N@`a3sod9pp^@x1j%C)*w?LOeDk*mv-yKuuNE@r|3e1umTb)Cd7J zZdJPlf_8-EsCGchMd;Id4jQ7au2D_HwgvYccv7$*})cx%2vGeCXZY?Za<3k|B!$FzRR*L$z z>ix6J=Z&qe&{l~lKxnj5M|d*#zh&=!85tfDpK5};D#Y&FT}H?VyI-{eB;ozThYw|J z?17YM3rFy17qrTH6)^YYJp@BL?A!N(xi;jveXGl86gOF>r>5RL_vxt{qKJuT0f<1^uw3}}J*`DNxkf=8g(aRiK@Aqy?Osd>GgH{&>d zdVD|yNO=qF{ycTBWkOtEuVVo@1_%4+&MPS?HQI$|u;5$>1t8tk&o9fiCwFn)(dm%q z=)MDo+qduDbK)=l^kloNFsvv|K+C_|d-vGw?whUC(^Edwz1n^w7%KqmJ{7M1fLy(L zS*lf7w)GEdIn+L6LAbR_0)$6PMnDjOR!&RpzJ1dO5!(-e&=TO{g-^}@r*=C|Wu!wH z4cr?)0-!;79`27;z@g;1C;tLxv$qi;Ft>yD~;eG-EAdVy%MsltX#E7iU0hCPg@Va_$`0n&^OPi znOOE5#Ilp%2d!;^=MEhDTaPDnR6m-UruErMFDt8%Y84DZ!4wP+4N9dF0YC^V2>y>g z7%^)6i)&Y;wx-^YrKYyd2p{1ZcpOI&p+g3I0JMVU6{J+}RbF0U?u8&bzr3cxo;58R z5+P7tgqD~XYYvpS=9&`|<1#fhX*3a$$JEr6`8=f0%#Ce%2(3O&OJ9HQk)o0cPfz!k zr)3$jY$QPSKYQTN|JJvEtgl8|C<3I?&VG8@sMQ)3BtR&j^UE(#r>6A+%mF0nR?i&a z5DG#DP3+8R4z6okHlRE&1cx9Y2`|cUo&_zvP>F~D3zYykiJ{P}YNrxYj%RZdkmHm3 z>;?G+vTprGeQh#kp-f4&rl?4tXRXm7m z0G=-t3K>&opOfQ!HqWfu!d?e5a&Of%>d!W`R{Cm&Ze0It$+g0glK|WAQQNO?eeUDf zPaL%JunKn*;FFI}sW7Wd<=ct{CUd7k$kT*>feJ0sfx||Zmsc89Z`Cte1nn>r4hK>D zR`6&U1P>V=sA)k+9#5WeC~3))lMl^j%JRH8kyDeChG<-mjAChNskuIp3D0v`eG0C} zweW9xdTtr>bn9lPldUW*Gep2|=D!}t>r|RM zRHNbu>qybgi?h`~pz;aJ07Mzk^LOlgdQRwsV|Dcn^TIlNE)|cK;T)SwASuK>VtM^s zkEr5V%>lT}kQ6F4_TOHOF|y-Mja878Mj8 zUaAOUNi@OtTig8I-M9WpTbWV+pI^CXT4u8=703i{MqAhri1!YvtBTTC`G>2jAe`Gnd2qb}k zIHcCewf>4VBbW(%27V_QPE1f`Wfj`cr#Crz;Q7O!e{{P2iewpCCjs_N{ z-FS9G%Ub;<6qc}qP-{JnXC2p5LxiNZlLkJVKVS6yvS~%1#yZYP_2?$ zj~D6h`j6|@ZB~D#<9*2@vPuF7;86!qE3DCN<838^N39}%o*zI(+G7Y30bEcUT&ACk zA_Pk)M-s_u5i1M}S~usrk4F**AOv^V842rySkQ6-&K}R?&Lx0I$JYLKT01!kOVG0N za-#`|XgvC7e+NE|YNtTU-WTN3#gDT_gshRk58Unh_9M8kCqT-@JZGDq`arI(Wh>#T$BuaopfUiJHt_ml75Ymu- zrlPC>ZJh1~mF`yQk;0|2BUN=Y5V8_|0QdDE2inDkpcdNMWqgrhjW~Q&)kfU|@(ow6 zTrjO14bl+KV?Y9j5Ki>rCY${YyfmU9sU~MWd_Utv$T$hoA%J!0+%P^tgpEdc(Nrz4 zElz!qIRQjNEWaRHIf&5F@zI=`29U~40JKF<_bp=@D8Fg*_PN{u;CGSHq>S}xFoi@K zgp8C$gK%9)k8^Tc$<$|@%X;HzYO*YF;NXiHHw_sj0Z~9+DD)difB=MusAL;1;q_m; zc4=OA4>);%`2wQ_4yz3%_KpOtkUoVvSNLgx14j1Fl7k#UJV()j@7)=~Xt;Opj@;24 z03A_8J<-a3aqTnXcT_2P5H#Z!$c&P&u)mdhIj7nLtZiCn?#ulzUFhlL)`sR_0 zYJ`lD00MX&4bYfKd48DN(LzPi(jo;C&4I&~Lx5<)qjkpmjxJ%F?E_plmLFlqAUw+Y zEfnfV*e3wS+t=R^>6F^v(5F6vNmpyQN?(X(OdWGlSv^mqa6hg|#~A-PuU83%rmQzkwEC5z!k7Dc#q1f95^H85roF`<}!UVBCZfDY#|Z&cm4)%9arl zC;`X>w%X4y{QC9J&3l+n1w_8jA$=4V6?cw!#`bo!r^@OWn1;+0{j)~IXrZVs6P*-AArfqe&FY?U;S774sD zLP4*~i5gU!meT;)0o~&ao7=c)%W*<;Nf9ZNK+V8${pXMN-Au04F9~osX=EB!`!qN6 zXObdP3K2~{z!?(|6@)Xy;BMHoEuMZl=y~aR`TY03E5V&xWN4dTrYlONU}Q|Z6A$I9 zhV>gZoAfj}6U`ws{`;4|<@xCJ`|Xk>Qi={V8o&$$Vtr3^K+fEZ2^`Ms*a%3#D-z{5 zZb|JYm!;;DOOh(4q*7+2NLC68MbqOl5%c>42fr~d65G0MXRx8BZf~;V5R#sEptk3z z_NxaLD`)5NrHh(fTcq6IBo+E9uB(L{Ra*alElrxU=BzYO-@dje0VC@ z`khx~=WD+sn~uCBbuBv_64X~q52KFbsQ#JaHFEFk&lvMicJq!TgGs58X(^SJf;BRr zt#mZz_l&V5!^j{Q5_Fi_N?)}<@ZxX3|4|Y%o|4i8VJ+ZKq%0XXn`)(lgwHrKi-DHE z`}O~9T75dW^{s%+6s?i9zxa);R?tVmbi8Y-5$xm^2D4~79H+fK(d#4?9jFI-Jel&W z2f$Qu_dJ4l@V|OU_P6~YZ30+e8qf->6?Q%*1i&OP?hS533wu-c z532n>DJ(6RcWiQWN(MTYcN(EJv80d7UA?6Y+9f&*jtnKF4H6~M?%LWP4KXt;l(9@^%ZI~^}Cc9+_}0)fQDs?9P=d+JbnQ_0W+uQzGoy- zX<6B#@>|?748=|lrviEy?R2q*{kvr;F|v7`tTxb~n9heuxEtp(V z+-t=9l9NM8ID~G%QsP(^aBzbjM(Z0?FR>=TB;X{j861}7#O8p1LDYbw+5YA!Nd?q( zUETb-6*M#mfrHrDb>oP{aS-E@AS9vhNeb|}mXNU|-~rvvqPcl#u`D$vt16eB1Z8z4 z3mi@I3CV)_3M`XDo`AZwlVl-+ov_R3YL6qwn7%>$=paQ?z$6Onlsg)TBtWVW!J0ge z`0^9Zam``8Z$MTXNE18bHb+fFHW(h1YC}geOH033AHjF<`9p!XPqwv3yH3p7G$U0~ zGzB~m_Q}Foh@olDgG~nqd==)EEPiqPM=~`!E~^Qsd@_U=&VOQ@LAz`(;W#k48jTQI zT8O}AW157Rj-jfMfMf`U1nM7@mp@8qEOpCVZ3F;lTU0(1Ug$xbG@VG5&g7i%ifQ;@ zTVO%-DvN2Ey+CzPoelE)<8y z72uOW2FS5v=N^+Y(C-5+fk4zD0C7UZP}&J1uuc0A+&zedh}Io9!jp()lw~KzEeb04#WXj-+b)5ZBu77bQ5bZiKLc~x^Pcm6RgveoK z?$1^mRPV}s_OQGCBlAiZqrIatS~q{}AsN0%OpZ-y*|C($jrPSJOBf4f<&|^(m9yx! z6B^pQ0b>o&0wnH8A&-m<`J-JY>X7pv96NqCot{HG4BF-g_#AJ1VS#22zxL)2Guow$ z8Tjz{+ew9@4`ka49BmwXb~kF@uAfL<+eNwA(x*JU#{;mVjlj(0_{PX(_oJGFPzHPg zP-09$lWgDd^xWc9c1c<{h~bkJSUjXcfX5X$g-s?(c(f=7nOZuKSosCu7?MUt9z~_) z3mSJUpCIkEh5%_ySjvi|s4cUTNO=9>NLay*q#4s=TD~d-u%KSe>Z~esmdFFty?f%1 zS$$oP6|5;*Aln7fV2T-wHf9R$@USnuj2!vwXfu-#+y_5=(+o7(`rYJ498Ie>!O86W z$E+^-w_<@=xEfaZJ2gRs1o08<^1C#?^-cryN1mG><$yn33KWLBG$Z>7c%aLd7hhoo zXRcRc`}*g0OS2Lpp3_LnClA0m>86IUzxk5<%%yChAc<$OW`RUeaY^Z1{X5I+I*Wf1 zIj{ zBm?T23}T~eZzz`4eK;{O=P-h)$;k!7pR+;&_drt!029d#*!6dkU9SK^e-#owslayE z+clBws4YlAh-e96M;zrxS=Mg>31-zCWOC_q`U(hDYRyXH{gOjw@o1^ON6~><22T@3 zcC|C>7dW$&7N&r*wi_A`V7Gy}8y*Ph#_S530=`n=k!kZv7Pc>;Fx+uxML&3^2_hJ3z2F{4_eUvXT2fT3BCUFnE8#cUj*Y(}pV8A|A4`(r1Q4<&YUz z$Bzggat}w6N0?$~*qO5nOV6#F3$GD*Kpn$irW`{Y5q2)eFR&dPWDs-69NgRcMrL9; z?oP_liIkV`?TWlcR1#!r!DAf5k??x{Br$MUVmT*|D`tzlGW_*MnS7!|W{bU2*fTB_ zzrG{+gOjopUhF3=h#yce5pysc2&v5rV1rQgC5(h@5^N=5V@HLBeyImtQMG=o|!AV zuG!xid5u^dNNLTvb|y+jZ}F(DL{=?%9B}vdb|?|nEI5vK=>4zT%>QhHaRZTn0CqsH z{oXxmS)KmQ$aA8Sz!RllGMIFcz$P|B zz4zmmt0-S1*@U}=6Mj1*0G!P+j?YN_Z+c^v-BVsFPnJ66BWECI z6J!SQz3eKd^N#P?*B_!BFSBd$GU?!D*IEz1 zeDjo)yw@qdUo0FBSMkAJ@x1+L!pjSi%uf@W67#0r>QR~JNLo|ug~CJBsR;7KiM&KqPOWS4mJWx6gD(ot_qn#g@8nZcc2;+?zLV zbpFj>{mC1#zF%w!yOd0VnG2^cR>-i6DKWBz?3G;t-@C}VL-Fiz$)(_&L*~vFQpVlt zaK_RBg0n3b4Uo#>Qh@W$hn+&4M2NMN(ZcA+gZ6me$0b2{p`)Zj#mVqO2^%+Un^RGk zh1+2gn7)IO*nJ07kUJdv+3|Ks6sOb%+g4^g^Aw#iHwmZodgp(fo@f8rKVF+7C_c+? zwH?YT5!}vr^QMh|9$V)XI?m#Obc|k zI^!Rt*y^5kg@l*Y%THAcxWPJwgW*N7A;PO~zMRpBVR+b?Gr@==MjJEkaAta{^DqAN zWP7~x7EQ73I~ZW3(f1_@un9FQZL=c>kpW}iFb@!dO{1BR5*#m=@dzIeVIBN$&&a03 ze)AbK(EfjuU*NI-+JLECRwpmkjwW8W&h#XFKEB9~R~=J=(0+CDL&*eYre>w5!LZp6iKqcG433q`#FV3msEnq)jdJJ@zBXrrApm9|6-Nx*yWA&D zEsdtKAp@`eFYYhuklDTg`wai`hrdes8KQw}UH$nDDJ(0Hx~)!#$Ft#oKnR|hI^dX$ z-Ib|e>`tm!jv-{g8D|D~Nnzm|r%%UU)jUi=L7_PY zAOHj@YuO^(UVX~^@V&pg+f;&8+q6C^KOGfpTiyEoO-{7XRIbiqld@BdZtG1%!+ZOz7vA|xxgBSv6&9(8hXRjhV%*?%t z*Az%WzGuOA_V~=q!+GTsd1){6g8Rff9<7`=Z#Hh(be=ETYmMAj0mWX4B}$0csS&|z zi{%9(kVN=d0Ef>ZCD4WY1=2W~8R3j-AEld=)^>opL-EyO3A< zs-&d2)ZEj8>+?H!ru+Au2!d#b-(&C1ak|VSK{+RTnrB|r2}HN&r3$wuP&3{B&dIh# zO?5P+JUsWz>Gw~^G7D5xFuWUAPz6lo%!WcH9%W$3=T|P8inl|}W<#^md+(06v_k3X zx*=g~4cJDjW9GcR+^zjeJKm0U$$dO}!Lp3xhngc+Tfskmt!W8UbN* z9V(pvbeMSaobNccwrRaNudDNhLrN70LNw$Yah`#&mBBB32D_D@rZo=@7%fG4vlx^T z2<^Z#q7CeQxu*4Z&{+H?BkbBbLyD2`?ju~2@@PD(4ejqq2bKhC2LAhxk_ZVSkroLs z3kRM*Oc&cx_i-u)woj^nX_3_`RD{;h*ldJ@t#?}ebtzLJ<0H^&Sd*7ZXI3-hp#q=Y zv(Hp2YGhnnCcXnns3){VL6}`nwHQ@Pr6K^+b**Kanl_mCkcbu?Qf-6qDCbjwmX%ei zaC!}CI9{mz8)zV&({3FDc#jF<8PFQ|FFYg91)*r&TpvPkEeJF*K0YUWzk@jloSIQCV5J5ghlxCy1N@ zjV8IJ_jFR!GxFs2r;Mhs{q3k?~70AuR;e349~dK0qWg z>KH}?&Z6eE_lDA#<@^;zrxM_Bwg~{Tt_j@uL2FxEC=@y_IV^?_Z8*P^i9}E$SW*&z z@Tg^o;Fla0fW82A09$1VvE(EGj-McZX4A|$7z91q`rl5YH!xK!3kfW8=D`8}QlUp) zBkL;5fMq2CFj_&RP5XMkCtp2uYU_V(IaVGSrE|+C${cfxn5?HK$ zGI-uC(B#ib5WdBVBZ0*#BLfR6{T-7+mMZK^1}l~X7LY+RTE{|Zth&C8SW#*}7FIk7 zER+l_`mn9)Kpv76t{oWut^W{y`$`tR#bZMPH#D?75)wJ0g!s0^m;kfXK=2_6y;tB& z16%QecjK`nfg4JQfH;F(52?ur$WpZDcRbEQkHo4YK`gXtQffNvOU$}_3UxaDvk}AL*d;dK qITl<^!p5$M#5#q*d!8Re-#*2*cQkzwqN>Y`i*XO@aSM^Hu-qU+e zD!WYoe1+tQwY;i1u`UXM3XAeu+28fy#(5)FP&j4z3e zjum1^?H+h-DWz*q0ljkgQ-lS zfeKfs9`6!GRohjfgx90}oCcqlGNXC%-tY5JG}Iv852SNcEUMytW4J*Oog2$D(LN@e zN$2}Va%18e+=JKtp-jI@%4Sue%>keHMZT|-@9U>y2*(V`kW}(zm&T&Om8wdsRhH7! zP*xCBQp2e>LKkI~i zvD_&DbPRN)f#KZdqC%Z&(YL-e-Z1SYecF@UlghL!9$KZS#VZt#Cl+rEme2JMXDJ-; zQ-j|-;T{lmcrYv8`#x()Ij{^M6 z?ATbcF%;O4&C)}mkdmZnh-pbOq&C`Ayj3mIZ9Im?WW%Guqe9IRtJ#8(Sg2S%@e;$C zQHlipf;a#YiDLkySTCUfIzgDPmmH_FJi;@vGn&Sqo#KuItpRf75HQSRJZ^!yZ}9s% z2Zu-2&x}Xc(KLdGrV${uy#wjT3Q8t5lpgJ1)ffwuq!bO?6b%ScK`#CPkd-eKCp;%N zS`Y*cq({qSW^#E!jMw9tL@+SM&wzB}`S_EcTP(3II$B92D(`25PMlNKVmm3G$NKt5 z_Vf*8Z%wh?K+^!zLXw3_R@628wp|{B1V%%C(SVRzNLeW06X&u_7$i*sq=3Kz6E&JI z2(qN~>+b{cmE*7;Uavwt=8N|u{Cso_JR8OvI+FfA9EXyS$K?0Ln4d2lW9d%Dn}X~3 z?Rq|+q@#qRN-}zS!BbpAs+Q?%Ig94j|~Zy(h14M7$49XZUrVs--$wO z*ukofHwMbrz*5ICoRgm`Kga0%!M)>S4?vA)GNY^M2;+!LGE}oiQ6J-$Na!U44IT+L zh*wCM@KJ|zldWAQ2U3E@jg*WZ9K-b>2ar-H4J4eal+19@CrAPY*h-fw77`AXj)no5 z0shm*cx~9w8(o0>o*)*_<8hg_LqP;u3KiN$ckep>TYIEb_z@!+(p&Uo)^LRBwh%{{ zWaCr<5+Yq9z4S@QNEO;ouZqQIu=VTrYrzk=R~RpGJudNcfF#Rg={t=!)~beTSbh%> zEUj{&nT$qv(WR6_8{j>L2UymXc7U|7(a9D_gI+Hq(LdDJ8Ye4WY8j=D$F>P~W^y(2~=#al^H zSL2^n*M92bXVA@GxR8#U9h;!u(D0$4rb=N+OTuIa(31>E>B2Rsp`4|G1j0eus803b zdUn|a*;c^Vj}`-=4g6h%D(A3hI+GWc^)Qnyzb7D#FAPD?|K@G_$#ZYg8(ZI_D(K|T zSfAW<_d2Q*>Pm9ryt{7YU)jM=jXAmRsy|;!U%s)O8XH5}mnumZB26GqZwP^yN_Pgt z%Z5dy%Zg+@@Hw%P?+xh3bpR48)I3WcI$%q4Xp;L@4R37OOKb1{IUVR4Acshj;{CJa zrj8`lh`Nw0q&HJ|fZwhrmzc}a{H?n`M<*|AE2}*;z6>K)0-XSo1c{bJN#7Pg7;-GH zO*Z7&XhKlK$nAn~hC=}nBmt=yxBY%?+?1V1l5aJJ>meom;m11Z(I5VvYQ*iySMILK z^{Nla=l$mZrwbe2iYQ7&g+eY%Cu>!kp@NYQXmyy^kTRcJf}`s6dO(f!|kf$9p6`sw7$ zU*AA3P!q|CWq-Gt&DqDSu4$dsNVnZ|3AOPntjkTW?-7k)g|il)NdI{Mb;6P!$>v2= z3OLflngN7ISd~YZaKFzd0wo|CR`#BKeRR!Vevb{goay~%oO&Ex{poW=n{@RI(NCY> zN}Jx;Q|)8>Dde5+#`^YB4T#OX_!+!dUGuGbK1V0bo5>9lpm=jw{6I7i+z2xqWFuwX z+!=JnDaQ$`8`d???oi+1A<8hpg+gtrxK^OEp_2^_XT*5#g_M`39>4jm=V|LZ`%Uk~ z@BhZ%tP}*JP4IfrvW4`=Z3n2kcgX5vgTxWo{{1g@l0&!-YYHi@;v#+Q!jq|WX5*ye zD_1NQL|fshU%W}Z{UfyZ;9)v&=r9fNcw6f@I~7QeWGVT65AEacxSM_74bN|}ydDzl zaD`abqF81$Es=6aAangw`^wv8EOwYrge7^H-I&D!Kxq-qez~}J_1Ioij z8dR)D7vDx;#lxm`j>ypB&;zSW-C9=5xE)E5^v-&*Kc{>9vdCk z=(pqF4?X%@vWbhyAzZb_3CFeA_KEL#=wEr<8vjwSBA!m>?YQE79FMZ0mW@VBdXwRh_`+ z)AG6OK#bj<@n*u<4ygv&&>~(3S_AGREbH5DzGOn1+;rV?dgz|d+8vi^`>w8M$N?Oa zT;RFja5#{tb{&4m%1KS*iixj@CNk(2l{t~1J1 z0ol+Z_+CbmJ?XbFf!}0;;X)bVq!VYBU8`z1c|lt{OSA0&lp~Un&A~72jZy0#1u3;7 zsn=Y2w&jnCMuI|Ws!1+l*fIgnUqL7)7IFq86r86`asF1m z&I(QAnW<}-=mgn-ZIYp;Rm?Dv7Ll^THCNj3`4~`HAG^T3Gb}!Jt}Q8{(&Pg&JTh5( zmTY6%tqx4^^s1uxcK}TZ~~}qGqm$(AX^2qZES3| z_oPOus%=|-a&KyQHNRY;KL%-jV~fEAxT3iUc2Q|uNkA&hI+FggB<#*1PbENrQCuDt zwUahM85z9b)Em-qcmx;`g zV+pX>zwkfS8F&6V0PS(lU#}DyEJ!hu=}|n-{a|x6c)QtsRn;|&E!7}K4V8WK^%v4q zVD_x0V~${8kY~s~{mI2N6=>vG|CerT=hp_|K_H`aq%;#32{znjWrSIhES7nwB3H{0 zS6^{Xb(QJ@9e4aWYlsm#mVmf-ch3NaVQ14{to#Vo8_0#5)zVl_+|T9;A^?P$FCJ4pCLg6wmG&2(BYT9tInR(1FEvjJr8 zo4>YYAFcoKCdb*D(2g)Z^)U$~8p3UqAz7K3s!vHC7|6|!$3sc|vrI`gvZ(C64e7#sT_%u@1=yAbNIdTtt_w04e>&P>=Yllvp=tkIbNp!!zjg zK-BR3q3)C=!H8`m^och4*{|QC=YF-dD%sFzP|ep~bso)aZDOA|KOtf#)0I{JZ=@9X z0r^;^0qN%J*UpMZI`q#pB)K=0zJj0TC7dHzuO7}WE@ z?tatnD+!js$_tS7GS9;8*wsaU+HrsdDJ|LrNSx3@^ZD41S}L1~U4-P^p+0kx~h(_`UtTLQ;(Y&7~irkSvIJkACHd%nOH_ zhE!vW*7DK4w|l+ba$>C6IyNhshMX1Op(_ zYzRUJ(5D@bi4`FeH3l?;JSJa7W@|dvu1_qOY_cleRbtt+3qXp^)O3uc*e*acRF#oM zjdAyp)YOCxoj{Td^(~3kCl}ZSI{t7bYdE$;t5tI!5gaK6L(0cM_~F6KYAWS|P54$? zArrNoT*54PA$z$KOlN{6K=k?nEyr49gDTF~D!_ASiWs*Igr=yge1r-`mCI%U5F9R5 zM3CUyR+c|w!o@YHYDlozss<-gYzC<4=|r%^&)MjcQtbw?kWV}=F05oD3=NHFwNKb^ zLuG|v!+7Uc^qor4Ew1Md!DDqtnp@bn{Cq89f}i&%Nt z{xB8#E&QUVyUfBhR8xk>u0QDT6?3F-6 zW_iY`a=skyQlX&9COX@T+ya)F$vU1e-=MIyD4 z3@NFV;Cyd}4&L_)jrC5n#_{jW_QT|x8KTx37gO-$>Pu$LkS-R4wplZ4AvuMXD5wY{ z%Sc$Tz^~E(gUggFYHhB!1VFYdtWzwgLNk}ub%Dkb1;Wq3CY(-z+`>VY-#$@Tbh0Z z3S&7P$y9Vz4r0t?7RD=zy0}->m{l1rPJH%oAx}>ouD+;DBD2kTW7n#(ffq;IKf_Eg z#A?fu?UgMRh?T&i0)#iQ5I~DzYq?)g+e~~ykeo`T*q>4yuX`0Am@_ttf(_2B%u4*P z_qWj_hmz%guZYbOlI#TeKIdW2+&{lT`7q_IUw1m zTYTqlF#e4hcB6!=#S-hUSbv53VUX+shepxv2%5}+lfiej(YzrCcGM87huj-iqv zp%5-u)3U-KtZURPK_>``B6Q{U*CZJtO)g<(bom?H7cuxFH_*;(nr_*(wR~(gOY)-& zmN-(`jU4E3ipJ%~Q#}A}WM_^WsumJDIyOGW`b|hiMFiL?a^*-U=o59wK(xPPXvv5e zj^SESi&VQ^btTDW0T8kH%tf$X;`6O@1*yxM;^Gy66#u_NdxY_27an#Y;+NYGPDIXZ zt0^E-iR7p~o1k*YGZPS~U_`fMbzo6Tl#MYIt3xOgY8-|aipK)sl2n(}wL6l{Rt9=& zSn1ze9Mj0;XikL3-3Q^Zh;K6bOoBxAn35HdccOf~4Q-!UD*-3Z~wb= zWy>64RRhnSat^KB_L@jqsSJ{s0p#|4|E;QFO+>};m-^{WF^1R6_^O71;rcBW#MrA% zMQZiAM|7d)+REfiAr?oP=qe_8Nw`rMTmLipcG>9C`_E=bJ^L*vNd z4r(#rg~2f0KmT;m0pKSe8Z}pQkl@Er@vQAE6UPaGHW*wh7}oUXjQr zC0AQ6GjUPANBf`ZYTpAh1A*s7$So43RSK(Gj~5m;aQ&D zJ`vuGvP59ALe4Ae4X@=*Gs;o0S|!g2eLvpjdVJO|t8oG=uSMHuBU@I-M%PxR3dvwd zl3c)4T^DLm*DBpN?^OCC2SJuMwutcNmh=!UV6uk{o-3t$s@xzD>~{JQDP?DKbIwrTwmu31U=u4wDq%rlGXY zr4U6g`IF=lX2Ep3m1T{9vcx;tSB8Y5Pv5(LC*AelcF~|;JMm7; zcNQAg2$|R5PE}(j3Db&3z&_B;(V#H2AjueeM{<+fc}#3dS`?NxOM0>-$>}7Kb&+v1 zAee466IF|a9Z)^=gWmUPb|4^pW%$UCFFKQrFD++GM!le_WjxzWf`DB>W#iAtow{Y` zR>OO+uGtOD);nvvyej3L>j1L~{)m8yDNMG!In1e%9FNEQd;8Pl8rJMMDlkH zJH9{?Zf~0};@DlIS#Ic25x16kUmy??E`lRW!nN=IK@fbtE`3){ZU9j2AP3=6J#Y-q zk{O|G4S{m!zh=dxaDS-AOTECLGZ!OO0Shiydp_R==g)@mL5q>)F{% zE@76>h0On2u*e%0l5*F+?R3koErPH+=PnS93DTglmp6_l7GMr~Nr^;AKx41r4gp*mYZZjZbCMjW-#hpo89;f8pRt~2x4Z= zU1M59EQvOWsYpl~B4cRy1+02M!Q>*d6H}E?-K$zp5K<_oETIkVJ+M<`e(H^-ojmqq zSdds6ZW3en{qs)~O=9wbF_IPq3IccGXEPVY*n0$K_&; zWr{JrEDy%YDi)S7h{c!*g9k1YNCfe)iZQvdFrXPsAP^};m=SvRRIS+hz590wf&p|0 zeYOb8XJ}B^54FHbRaaYyV%EN13U}hw zGx6Y{7O#eZp>MW2jl#l9g)kZ52*D9mH_d=YtC5o;CaKqA!d^{lF) z0S=Y&#vu6sj)5$6gr|ECA$DKN4xK@dE?6v#G4X6B6VeN90iz0per3P} zcTuEJTkZt{Ahj$S%xcA8Xdpeo{pyr8uqS*w1VyBXI|WpBG-NEzL+MP-(hNXap6NOu zd{ND)I$kt5)HjI3u^}b`J~T*!T1FW330BFFL@BW_pgT{^$@SN&QMS4kQY3=Z-i)VfGID5Vbluv^;@6si~! z{wwYPu*jtkj4@b3TGFucVaQ<|Kh+c_B?v_t~TTtvcoVNnsQp z<<=D_@Bq00325tys9Q)Q zUM1;}K4}T#I8^aYcDFA(RP}ff)k%_Ei^>MfD*Zs3&3=#`14HTZHg`~CX7?a1wU{kXA4@#O zWw=EcHxa=C*%*g64)qD?12BeT42Cp=OsHuDQiMw97&z8l$?TJ@!$Y+4o!97QzD^$t zfB3*2Dr5?xlS$w^$_j#3Ws!|wWl={%A@@svb*dUL^srGJTT#^Cl1udT4N~vnA+dL% zETB5Vk{9M!YFkSaoxE^1oxNn5sf0vB8^8yL)QeQ32F-Yb&xLbP_dXu;ym@89xaa2k zzOiMms0+ShPZt|ZV>CFFq1Kj0nl&>@r=L8B=AYOiq!`8D+>p$lm~4rKCv4GUn5{W0 z9_n-(7(v${0h)#`&>=KZS@pZN@P!5jZ{IxvMfk_Y$Iv$r+MJf_jCIoV-mK>+6 zREQzT>ieP&wj>&xu{`@2DV1VOOj2zHp4g<1W6U+1Lj z%pTB_N_X%>){q0B&NT{~I|Vit!M+nlMa=>J;$oLKyP-W;`tOVm({lqobaFH*j4BW) zLz^HPDZfzFIEGm)ZOooQ2Xa|z;PCHzBN>|Kv$kwOGCHRjU>X0abDL-vr@QW;sQFFT zT_6O&45Icm&px3e%9J6ms@N}Tj_LyirD2a9Y1XzUcuLf9b_=uZ381>>w9cq$m1IuQ zrrsKPPIk{|Mi^yqquN-dV>#&N^Z*UA^qm`yIo@B&is<@mPjG?QWW*K}W!=o?&<&Fg zTis(vnnCa_cRcR6{Jt5m(TF6vma*wb0-K^#?tb%szxO-(&Bh&657;HzNQwc=P>qj9 zAEYWd*&ToXT-^u;9^>8*gY8%$7(i8hebC~cp8Z4J2nH|&8toi60W8Hv1dCHt1y<6G z55E7~FI0Pk*i+%8d758|O|r2=x)m@s|MUl6JSGU%<6w7+_8;u0|9Ew95iyVzD3 zdop6nzMnn6mAZPY-Ain?_QWGMPVz`3%%oVHqbm5DzrJHLDZbD1+X-Vc)buyM^~j;jpF*0c%z}MN|tEQ$w zsL_t2*f%*6v5w|cRZr6pJCohC`A-QqIhbxeUpLCf8Q}lsNffSi?r72 zK3mf#GvkpBl~J%KH&iFVY)e`EsJ5Bbc~Iy;Q?;?>RKd>9s%F~B?pF8kxAjm}Wv!W- zmasiX=2htiX@p_NRXe-6$y&{Z=hD^dX}qy@FC9zRzPq{$X*Tv+sdzo!*gD?vA;mM} zp>@^nu}w01i^9DI?|1CB=MTT|(hiRNO#V?Yyt;a;$FaBVoVryJQvo|2-)tDFW{j3U zWB2%Hc+~M7W31q4w!0-Beh_N=z#)4s6D0Yo-*~n>;Q-*reD_BiZQZ}`K%bbPsAU1& zkpSk(x9_r7^Yg8D4$wccOQdg;Cs@tfC3&b+zKK8b_G)Tc!f~YmsO|h%;Z3Trwv7P8 z>!$0LPcX7zNy6}W=)1qE>NddI#7qtT;D=)8mFebI;k%^<(&fgt0@~qn4pXlD#2Laa zk+|l{bHuqPpLu-(`DWO$r+dTsXC@vbn{b@Bg5Gn_!@s<}+I3g+7ysMaJ~7FX1kO11 zIC|*opDp{&X3*b@#>6zc4JxKfpyH3Kh2v7DbRAgYng}-X@#p`1^m#zz3r2hKPp^NP`*bnW@3-i8FuUtOi$82xtDw1hj7M<{7^`tlX_>Z3zaS6*tqtvxoUGy}-S z7I;%lIEaPRiqGom6B80x2axEtnU?O97*73L@7OQ#-)&-iVLtW4r_6P<)CLlk%M~XPg2i86 zoxuj<4XgR(9BiS00%v4vY_-A#=ba=bCn|wSeJ5=)JYr>hIbY7?dXhG=v?E4=B3s#N zf~El21^iPVKSM|=RAjB|s-_Z1fR8mrL=xjIcRoIKkvfIeB{$uDgVWnwXN3!utri_w zd3J%|JqQLMbuIBmvF~!NJH^!-AQ#i2$Xbz0SvtOQ88t>+XIWZ6NwO5LB^Rj4>FnSC zyKS-KFN*UQSVnaM)b#xwpAl1QdZ2PF^16}Mi$po7TC;KrQwgy(Pc+OOuZfy?rmV8~ z4Ze3E;G>sc-%WLeN4+7r>1&(F1!|5gFwwt^9p8>!)7`JxECCYm@I8MawthDQ$Og$+ zJ|YQ+9>e|%E_Vd33P@};91Gs<0PR{A%*mum2ya0q(Qo43Jp%cdPk}t#N9MwSROGt+`{W#xl({ zD(C_iFy>HbPD&#zZ;!XOEfvgwXx^NKd5Vyo4(#jbwd5St`3QFI+l`>Pq4@= z{rt+aRy9R~2^c0&$@rTp$(_zX3J+4BWcu>&U){a#Pdg55;c2BLg?*jL&JI`A5v?$# zBv-;s!u|&p3aS07>bqfPJm7jwxc$#M!bqhuiK3$ZmhGVg{pZC7uNt~RE)t(=9C4B* z{xeE7(shceas$?y(GkQ^BN>2Hk5RFMty;&$XpSX7HFNCdp;_@r2OR|*Rgxt>sANlW zeH4E1NU3Jy?=g+=K_Pp(kURc;rXfIfo{FnM_AzFlbz+sjmx3Fn1Gy5@l4Lzl^+099 zIUu!Cb-CT(R6;YR)b%R)H?+nZYK~4#1*Sd8di18!SMW5%6<$TT_$W(wr>d$ia`e3O zNL$Koa7>Y`M|Wy?sfWBvi&Si9DPO`;?s_+OD~O;4DVuqW-GYjRsB$-*RIwc#vm_fq z6ttmYjHP_3;_=Mm8K{J|%RAJ(8Z%P&P`BDUK_;tS+RJlINtM*(7+{@?AAKqS0000IIA0WY(M2Zpxy)9c0Qr3fVoS53}CU&+{%SmU}>15ZkXJ;qtWGZE6XaCzZ zwX=`ecBYkQ;%v7&wdKjQb|=$XuA9ns(wI(TTefA1l59~D^#CPOB*ho_0zeSJ?e{yt z0WUA_1uiZ?iL~{bkpN!YdvS5j?|kob4$*zw$9>$#ecZ==+y{|DUjZCB((dCIUyP{v zT<0gga3tc#@rnu`J^0{`dFS@_254w_Fc^-^1RdlEtf;OE{=+}~ypz7faMG6y&mU=T z=9lIoQg2vDa=AMn-n0LG{-Bc^uJhQj_5gheuWy|Qq0Um2+gs6!)6*A8zFEb8jUbf zYQ<+ad#>^O8shIr-zRXRJ2*Nz($TVe|C?^2j^#>~GbG{ZMF{u3Y{?bS?-o(BDTzECqJ&euQ!X$Rh4h-zZ*( zhXy-l znwSU*BIwcFvX_F~Y5m;c$x%;KSGQ7hKJlopTTwqxT7W2Hqr>7o?hgro@94UCUVKMs zX}KU4j^Tbd9^>bE@WGwb6Szie{Tn;Nk?B`o|JhHE(-Oy$w+S%nRZ^cBx-)pFgc~27 zgU&+3U%z%mbPz_O>?Cw7M7NX%KrxH`nVA`yo}Nk*V6A_Hc!iYEcD@A8?M>MZKs+Aj zYt{<_0FW{`hVPUT0RVA8jw_chwy`>C`{CgiUf}OOUheU{x>VIg5iL=m^Y`p~>=l07 z&i|S{9uHmZzC;eiK|Wu#=x8)PP+nd^)z!7>YZX3ce0)rF5YAnO0!iSI4fp-Iib{<)Ll6{hs3DbL@cxX$fJ;sRJ?cRrLja+a|@l1V&pC8+vl|L9YYrH*67| zqJ@+M+Uo%+&Vkh|J*1&GIWaEoU#q*1Jm2U$GC@8cq|mnQyVB1wFwjS}HMPe}+@7OL zMKUZINdRB|;nv6gFYD)H{HLFdEc)V$PSH8C@zIbNT|h}msTlot2Kz+^$>S3ETYqe9 zbcF8Q>8C^@Ax3}WYQMNAM)A3Gr$xu%dn+n^LP7u)>^}yvq(d=wg+`E*YwUi|Z0IA_9ayGMMIJkR%n{xWp`={t0V@Z(U`9C~*I6>+O ze%mUKjdSNiq{G-kN5fsKWYMk#Ix5Z`P*&y@jgJl#mj9KDqVe@&9vv0r4g@wZaZ1aQ zwt%W8gxJS5_=k=vOLbZ|-wOo5cj4YJM4?Xb9-alH$A$B!#q}8F5Uu#$I1^2(q%}?J z7f2+AFFF%>ysWJ9(5i;UH}9p2xTosi2S?id@tK)dhVJyY!AggLr^N&l0lFVX8UV{1 ze!UTE)j3!lw9_AtAA=v26X5c_x!qPST^!{^tlnwve>sf3Nh)tXoUq=VPXv4p-J zaUY2Ht^6Ft#l_-tHURmY;d$S?Wy?e4gq|m0#4~p*ufo38RnwSiVdjiF7 z&#^rZ?}LiC>tt}yJwV$Vs_*;%^Jj<06Gdlv6t%$$*IMEd5XERr38~8^&~X^`2=+Je(o=X6qBf5)p+FEMELBt|MH7wy34pLk^r4PRZfSBRC9ocd;s6rTn9~P|BvOGLTFH_J zk_P%3Qb^v<>^dVL`Z&mHKuRlgJZu!)2f-^42G4`zO=~xZzY(jrH8@CX4}6Cv8n)47 zji2J)TK%m%nLwQ!xqS1JZz(5e5wWNwK<9_b=`jD0BMUm-1m4jAdH?bMaSsoqEqAB| zj2?;YJ9dd+4a7SdSRc-X-j;s5>l=!O!=T4Y)u2YkFw- zLQ0G$I}xJ}jjh$$5@>XcesqY`(`F+Yor(JjLr^1 zEtS0cQ`)v^bJkN~Q@}QWF$jGRtYz_w5iTNy(HRO&Pl*nR={i0WWJD3(!az#w<2`); zTo7`V)73}+JVZ!q=9V*Eg>M0U|&d=@ce-zSqTvB z;@}duiL<_e+vEYt1~wWyKK`b1fC_+ul3?Gv>MLvn9?Epi^n34#f9VI{oIusH&yusD z!StG<*$AyXag=&5e4G}==xkW!5YczLR40K|jA1?^;lKcxboU?fnB=!I1^D5GQJyGRJpoIBwruA(JeK|91%Nc@^R6Z}_C-$DL46?Xi&q4N zFiMS#IgDDkjncPACq~($!10OkVA=)E13Y~5jLWJ^h)y$}9@6jiCv zHHZLmpm0Q6NMZ_q_6V@2rG;i$B`i4J+(K5sL8K@7t||MyhYnyh|J0tU}-|B2#5!z|Y4 z>vR`;2THaMV_!!7=x^p7f>j})zC)~?vkwVUfi(hn7f>X~p z7t#aOO7IAvYH$w(qXf#UN!kD1toMVlJoEVlK?Dhv?}4_+uFBF-nMr{5B9(fP<4 zq61_Bh!$`kQ9>q*IV7a+*GdR+qgGvqHIX~`gm(oeZybdtx3qsJ3u{vq9oe0od*dp@x~hqYymCM z5W&~}D)WsZQ>lEwP7%8|i`Kyw2ypuL8oQExx)il^-Lw$NB3&(m8M1i~$ZH^9QRgko zcptccS`}afe4gzbf$*eHEppqA(&j~iB2^X6Z23qa<9QZ+Ug0l8Vwj;eJ|!Bs!lbZ7 zLR#?<)wR0%0C)%4uFn@B%KFAeDvBACK4Co$AkI@W44&fAUh#bjsm#} zo1m5giL-SZwj4gf(wTCEEz?=30d>9_sO#;d0e|a)a}(?Z6jfPIoT#ju&EU--aUkMD z)6+cqYlIbVmdn4J!1Ef}O6b_!+QeKE)b`GsE9;w8RfJ!2$|=~&9sKaf3$KtaM2s9c zGG&Ve{MdEp7q!5q;OHA3p}{}+V~RWN2gx#_I25EE9mm9bQC!0V0Je>yC{O+5Si9xM z%2^#?UH}>72v8AJk;OL12dla}Xt|-WmZj3UcWALte&$V4MFks3YwTX_?)uZzbbL$5 znFOk;nkFs$JW|bUgM8rlr@tf$4$^W15?$kOz9?!a3k^ud`hm}dYg$}fBJ6_n#Mt3G z{?lhZu^dIri3HCdX+K1%$)HAnfw=x$VMVsu^#R3x`4^)04@D!(1;}v9NMho(|HdR9 zph5v^0Fn`Q&nBJmt8s}BTa^Ym1&j72=LTjB@_VL-#X|<`vlm8W#f?rK0Yo;1v>zP1;@2h!gEEU*h!O3 zD=1Q1L9^Zxid1_k?soEVm-yT1?c#ZZEGpiP%ZvtKG!Awfgsv*L55N$L(S(s z83}Cg z2go?0X#R|1BZL~NXz1aMSvx*gKtfc19-zuAx91&4o#RE#+z37N)7OxS4sHG#g`1iw zpr8MWn_{ zw;$V1L%TN8+FyJsqy&HzHb`kiTs;8*%urfAJw+Rbk`dB@v2nV2CuvBk;VEH9Nc5!E zR?^O)kQLS1nO7J%Vtxaki>N8Uf4A*!{hhYcr+(F8_M0pjhI{w5AtGT*xyuzr=79}8 z{K!&AvUJ-FSYRTGNFb+ZC7}Br+SW{N_rlEKD2x4D|Eh%|D+^ZIZ3?Ijd>>?KS$}e- zvz!NsQ7U0X0C_G(sx)$4j$}dY#GSrm`)Gi{P{ikH=UGloO^CMeX=Z8<^xvk*$zXcc zG$cm#{rC779qskjb#iWYp;)8r0x? z5TNH@?-6?XUPC;~@9w3U)U!0`)=snvxiX3o`UmFQM@ALbxL$frS~B^{D-MzgGLj&r z3jAaPViIO*VJSTz1@cYtLIr>TX#AxD#2mClunRL43D-~B8yC)>5pyo#F&F`BA`ECq z6Ank7AroYi1P93`V5OSP4ah|bn4#|*+m{ppBtQfhr*@wW17~#9yl$d5wfd@Ds8d!+ zqrG8&NkAH`PZ`~qLVvFFQ{SFP+6>itMypI!X(!o)VFfM~MhXn?+CWnUumKXGS&H78 zpqZP=0FbM;ggjfS$x-T}d;kL6ery}9{rgWTS3sO2DI^!Y&;uDmsK?||Jag0bBTNje zf&zdLki>{7;8otMva-63Upn>oG$jF&53{Ph*)3wihRH;YT$X_|0vZ@WpnNg@%K(l2 zT{jb9p|q;{y^U1$WcC8jOn}!gx_vEGU+Br{I&?51f$*r1G&7Q>@Bk27(W(vDAPuQP zSFEpujt2?kvN^c#<}Ewges%J1kLmAcst#f{Jpw@MCu>oh3VP%N*a7)s=;iY?@_Lsc z0f4c;yGniS9|%d24~99QWJfI^TMDDH)?~n7q-O#$i`#cREZ$23z*U?HhaNLJKa)++ zMm8~O@{pO_8!H&!yh=1`OjXG)#@_6v$q)Lno;Sm4V(8@yln-DZ4DH&G>t*Qz5zM4A z!Zh0hkQ7GV7mi0$nM4$v^aQSps3IhA_mwUmR)yDJlYs=PZF>MwDtn-VG0LrC`s^S@E{{?^Kp-hsZ$ah*Dm1gHw1AhOwHd^cc&Shu9wjC znfxo7f_daIyk2AJiEz5~vh?kZP6sKFYuMSGlmf|Rcgo4%v|)joHzk2;(C6J+ZqLlaDIlK zUBEU#SlJHDB*9E7u?8a%wTuW3No?Buka!(Tt>U6J)MK&P1$}EsPi|_TUDGQxZJaF9 z&k4*06ZJl}1LAZm9HC9+$x!Sg)m4-$93{E?|Blka%xa!ow~{KmZ)ZIZo!irMU4+S# ztEQQUp4YA4EY?tHO0Ci+Q6bXK$wfaeb)8QyodW}r&-3f=V<-s{c}#bfkt{v(2@r+3 z0iB#Y@oIi(u0|nHwkvnTwESzUsSvPQ+#*(8=g|uhE}>sLK>|zaVvhw$0C)p)TmDq0 zH}(QRuGD*+Q2D~+5S zK`eQoFqKJA307hTQh#d)jal1aF-_zTWR3t+?{F^uMs5LpfqTi>(l|GjRLV{QyNE=L zOTx{VYz1gMwW5Tk_y@rb=$cG2-NSzH_6n~cM=q%O-d4*ZzV_*@r1WRZ$? ziGH|?4jl=W{6<)Dw~NMRXX(PkBt7UYpQjQ4r;@uc0}+rPK;US0c>V-f5TJ%7fcI;K zuTjiK0+f)$*wY7y%mxvXar&kAVGBH*+S(8I=}xJNWaU!YLt8u&L`~eoqC85z;9u9d z-L#|9%R!>K$GtEE-7RMMh3Um zAjCcm@D5gLig}fLAqkvC`K4w7sDtz4W3*(2_fUbpPUgn0V|Lp z=E)EHXa%bV&z8cLt{b6Hi)TPB!w5t<-sUY2E?9Q|R$pM=`Ad5~mjmqKW$D zf7$R4Xs<6U35v{WJECG^nf=nJl{~za1014C}#74qo z7jmLXu85g)|;~yZ|#MTY)zerNQxOp$68Jc*N^w&L6Di_t#k(=y5hUo{l0; zeOP$$9pQ=l`?*=}AV4;eDcNBcTgH-|iAZ5Ap;8HZ1Xb>2KB1R0i?>2^ z_m*MgZ~^K7%rM<)PJ-^KX~TR*@ks8T34me{vjM~sJDnqt>oX!QpoeVx)Fz$_2Dl*! zVtJ6jEZH$(QhaI!b-hC(T3F?BQa>9%`)jI1XCq*AGaRPj=)!EG9e2d=V!eIPxWK8^x)Go{{>({`gGDt0PZCH;15W)8>r6D9H_35| z627PYb8RRY-!1DNr4)es3dx0Rr|(LiZTHZu-HaqI3`nVt5hMzw<{kja%|1M+RJK0( zgjw)DYaKu32s7~|iyG|iKTydHIk0QJ=xFF&gp1LEYf3#d%<+R-KJH6(E{Ni3X-vD1 zvSYN`5@f*jXV{zA!9l2U*Zk17Tx9+XOBYC*b!DZZvI+OX-?(1hS6}!FX@rzFNYuh% z?%Ffg$S%z0aFiNU9BJ(_1c_ut5ej@^Tw=lR^73+P>L3ZVlWagT&Ip(}V;8WlXFk0m z8h;0S03wILBQ40slcD1ok!h4^=2}iVp_6_yoD{?GDzueIFfbwRc2!oU$peT$lqMRv zx7x=`a89lQu?7bSyf1&d^im8GX1Xz7K$u&;rDQgjftIkle&8vmGFp@0XGn zL8G<)jRC!T2$+(NxeD_p&JqO# zk_E6@fB<%-0AMz$*X~TNl?{!CD{GmOAi4IPJ!xPza|Re3$S;c5I6O%^uMJW;s~yxB zK*VFj%5Fzz)TeYdI_wH}7_yAe>HngxMvy{~NK>vp%eH}30f;-GvyrUwmnGEz?gbr> zd(5Ut1+@cLbq}ixp&IypkS@O&85OQ;oTHp|pV?o|HlInokhlmO>8GhFq;?pcpD}^N zzwLdp3A4$RX6()dt>*75x97_QWDZeJu&Qz1VjzK^9&0z0>={Y$lVj~2WYaFlwDwMKZ*Ft;jQ|2P zzuQUc9|N!qmyD3L)QbFG(Pm^)O&O)MEwkRGStA7 zrp5sh0d|Bm3PBKzdx(6f0dzFN)sj3&Qh^|_BM_y8cd^mwqNhLVruwS`rq62yVyXH? zSmlLd95TKD5f9q}q(Hs`-h&`e%5V(wy&w_JPfUoy78jqdOoXY4 zZGeh_q|gKbkh#;YA!HTF8O3H&U=`U!ASKlek+lr?lT#gmz5Ck^k@kRMZ<>sVXJ(=LWXq6Ko1WB@D6%-^v~8 zN(Gfb7IYv2N+1Km$~cA&jq>xJW{HA`B=P_bC8_U04!EP0tZHrvqNzgvw+v6yW52mb zIth#i#_G-8BMqs!HHUPB;<;15Scf4u#sij)*eXR#Db*7DM z01prGCMTvSSl76&N&K5~aYU`Mid+d}BT6=?T;UT7I)Vt$d1VF@T)hKhwE5;R-E^~# z_LhoZ4LYuhI~(a|kYR#dxs6Pa9_~npcZm57tTL`lPE#3^a-2Vhp*g{%L)>530el{= zfvs_kB?5>7!|%YE8?@nk4>=QR`gtJ%B9Xab7Z`Zih=7eWXN$|Jdy)xSR+C)!d1`*b z2@^-`hTNIlPn|l|VQSl1n!}Y^#A=LeqR-`{+vRM~Y<<*lt$$}MBKB6S8oZR=-Agv8 znP@~vG*dMV7PYw4R738jmE4IlVl-pq!Z3uGhwevl3djL}Ujk|YaRo>X`27Hk!8NH} z%tt3fw1eXmySnDbJ92$qbUb#Co2>DRy&()CRylu%YNrQieYLf6bQGLwC!~yy4tKov zvzIcia-Pek7jLMldXREJw8Tf(cOEqVkp{rB_ba=8Mg1pV&RCm>g_*3Z7&=^8O6NAL zpkgLNO`R{D_!(feaFIO<fdp$3MTII_~Ycs5Q4RE{^CxtiAddO*^l zz$4Q$i+um8wW9OytZSv1#a+_SCN(@b8nbK$hVMp@2;Ui&L`OD z;?{$YTmGR7aQ1CVoc!s$CEq2qKQb3g)K^iU%uULgI=aGk<5-D{TqSPd_ha;<(_t5E zN+{$DPIJ_6iW+)HMa8pYCcCwtO&T~?KR~X3`@d+W#9r7JWO(4zadL;vy>?xuHt^Dm zN7qpna_JpJF(4Ts8@ zLK6Uhn^_0~0Lj_E^Y>^{@H+}X002_t>cdY9RhtC?D+B;?nkg#W#nuCDWHNOf4|Zkm z_p{wbZ2KC!)hjlR&bnQ!x0)2z*^QAq1z8jMZU6+pUIe4;$-qT+JpbpMuFSIk61N35 z3`cZY@nlw^Fe*b8^;Fwup5J3B@Z(QUby!LO=InDo8mK1A-{07o-EPFL=u;<8QX(~f zu*m15s^^{~hc5#e_WwRrC~O;~_D5O{NbXtq5M5f?oOSmzv`6TpkLIr3zn{vVcp~F9 z@G4+n)(1`*eQa3@069-XL*$7=$uH`U!vmPe;st>16K}kcCIAo@$NrYGp7**FvZTuU^V1$8*^%8?Z$JQE;L&636n~5j8QEJy|*2 zn4M@#2_qH;M#99MR7$K7UN`uHE(^xH9=2yCzjW;AF`1r(kz!?gc(>ngcpP?tYn?ya z<>Gsm6Q(UZucx`i@VK+y+HZw~OoIt<%*LFNyr!nd6?&Fmf>Db*eY)1Iqp}0@BbW}h zge$-IJxkY*sZP^0>r%sv;CNBS6P!H~*i=}`X zu|9V*T3Q`oJMCGriM%%v{PfiOry9TiU#lYJD_S$12hU*a|NlA7bX}sv)LbYS2I0&l zJ{BA!H%kg7Q>Kg~hwACKM4A?PcdYA=<#O$2*9r-AJ~pDfcJ&ek`L`t8rKIftZpLfi z8BF~A=M>e>6i1mG4+`lqpiQ!&=GM7q|Ll+c^C_~4g6t$&^B-S=D0tfJb3uStUZ!{k z;}1Vv<@|f`)~%-J(C|%>Rg=(?p%^2`Wqrl-LSaGYC+5~zRK9Mm=3xC#zV#NV87h!w znH;c}oDB_>3BULcyRGR{6BJ|!ITF;F$e>u@^TsV$|KYQp!ccOVuwNZ8uGGO&Epq#K z>;r@aoATXkD-+Q1SR@j)fTnXSBdPt4O+v-f9oBMjlN?CGz%Y#>Dr9?-kVHX9aOw#q zu=iaxNUCx5(M(qtSw*<>tSJd7koosQGRU$u5CX-n)z!6PDpo!!1Dzhm1W&#{w)VlR zl^Q~|>jP3+zO~?Zl-t9~pb+<9+W_jK_yJ=VTuIoU79deDWvV9VRSqd1)*p{^JUo$u zku%GIBSY}0LZezmNbD0dm0r06qyP~R&c&R5vpJK<{-HJ?5{Za;{}}1E*Ia_?G9baM zMMGF3*57YVf+G7hpy_*1fzWzV^|MO2Vf0xdm|NAlQ62#mN2@-4U0B{j%fL-UIT;dO zf!}+9W^^tzMt3gOKU9H{I=}*BAAd{9ZaHSVKNS#r-d2wpNrQY*917BwkB^I7I)IV6 zRLLMaUry0SMDNCqoC>lmgKe>ASrs3rJW)^*G{2|%ilcPa?keeKXu9||tu)A%$(cHR zsjXx06~xZuwJXJT_D1V7T;6v`2fb%U zH8^J@LISJ|*bait1Kc@C3RwME#|MAI@;8zg(qllJ0q@=XuQX2=Qvwp<=GMn8rQMAH z6;KkU_CgXs+?N&^Ool^Dh8L`oL3ja7fZ}jLf9H|{NN{^&n`NoM#&(Air=x5HcF-bV z(Mce&_Z=~3;UIq_w%_$jRe{b%<*aukKs9}xLJE9UfNCt%VT3v5jN*{kT+vK`J3gSs z>3t5wb}UxsOWZXQ=<)oK_Ct!QzCZ=`B$hLf4n{WcSRU(0+WKC)hxoyfc7G8{)l~Hv z`f5W|YT2y^$QSpN1QK6msc^4G|Ga0QC4?UxX>V7PWcVt8^}h0Krga1E7M2tVfUkgx z2%==4JHMTl2$mcPB))`X5S{Ntv9?$qaOq&ll0X7v;D$TGYT`*+hBgCqeuC&#H_?fu z*7*`ko&;LJfMmb<874#(uBB$5k%cbcsZx*YMPBY_z}jwl(({ isDarkMode }) => { + const { usersUrl$ } = useKibana().services.onboarding; + const usersUrl = useObservable(usersUrl$, undefined); + return ( + + ); +}); +TeammatesCard.displayName = 'TeammatesCard'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts new file mode 100644 index 0000000000000..721807721c254 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts @@ -0,0 +1,29 @@ +/* + * 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 ONBOARDING_HEADER_TEAMMATES_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.teammates.title', + { + defaultMessage: 'Add teammates', + } +); + +export const ONBOARDING_HEADER_TEAMMATES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.teammates.description', + { + defaultMessage: 'Increase collaboration across your org', + } +); + +export const ONBOARDING_HEADER_TEAMMATES_LINK_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.teammates.link.title', + { + defaultMessage: 'Add users', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card.png new file mode 100644 index 0000000000000000000000000000000000000000..b7bfff6c3da9aacb820eb7eb01bdcd5ecf79a496 GIT binary patch literal 6645 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card_dark.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..af8e5783c196a9f83ef2cd528019e149abda1975 GIT binary patch literal 5298 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts new file mode 100644 index 0000000000000..aa3018a3b032e --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { VideoCard } from './video_card'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts new file mode 100644 index 0000000000000..f31a1a8880dd9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts @@ -0,0 +1,65 @@ +/* + * 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 ONBOARDING_HEADER_VIDEO_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.video.title', + { + defaultMessage: 'Watch 2 minute overview video', + } +); + +export const ONBOARDING_HEADER_VIDEO_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.video.description', + { + defaultMessage: 'Get acquainted with Elastic Security', + } +); + +export const ONBOARDING_HEADER_VIDEO_LINK_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.video.link.title', + { + defaultMessage: 'Watch video', + } +); + +export const ONBOARDING_HEADER_VIDEO_MODAL_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.videoModal.title', + { + defaultMessage: 'Welcome to Elastic Security!', + } +); + +export const ONBOARDING_HEADER_VIDEO_MODAL_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.videoModal.description', + { + defaultMessage: + "We're excited to support you in protecting your organization's data. Here's a preview of the steps you'll take to set up.", + } +); + +export const ONBOARDING_HEADER_VIDEO_MODAL_BUTTON = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.videoModal.button', + { + defaultMessage: 'Head to steps', + } +); + +export const ONBOARDING_HEADER_VIDEO_MODAL_BUTTON_CLOSE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.videoModal.buttonClose', + { + defaultMessage: 'Close', + } +); + +export const ONBOARDING_HEADER_VIDEO_MODAL_VIDEO_TITLE = i18n.translate( + 'xpack.securitySolution.onboarding.header.card.videoModal.viewTitle', + { + defaultMessage: 'Elastic Security', + } +); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx new file mode 100644 index 0000000000000..2e2bb6a0805da --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx @@ -0,0 +1,40 @@ +/* + * 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, { useState } from 'react'; +import { OnboardingHeaderVideoModal } from './video_modal'; +import * as i18n from './translations'; +import videoImage from './images/video_card.png'; +import darkVideoImage from './images/video_card_dark.png'; +import { LinkCard } from '../common/link_card'; + +export const VideoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => { + const [isModalVisible, setIsModalVisible] = useState(false); + + const closeVideoModal = () => { + setIsModalVisible(false); + }; + + const showVideoModal = () => { + setIsModalVisible(true); + }; + + return ( + <> + + {isModalVisible && } + + ); +}); + +VideoCard.displayName = 'VideoCard'; diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.test.tsx new file mode 100644 index 0000000000000..3092788ffdb4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { render } from '@testing-library/react'; +import { OnboardingHeaderVideoModal } from './video_modal'; + +const mockOnCloseModal = jest.fn(); + +describe('VideoCardComponent', () => { + it('should render the title, description', () => { + const { getByText } = render(); + + expect(getByText('Welcome to Elastic Security!')).toBeInTheDocument(); + expect( + getByText( + "We're excited to support you in protecting your organization's data. Here's a preview of the steps you'll take to set up." + ) + ).toBeInTheDocument(); + }); + + it('should render the button label "Close" on isOnboardingHubVisited true', () => { + const { getByTestId } = render(); + expect(getByTestId('video-modal-button')).toHaveTextContent('Close'); + }); + + it('should render the button label "Head to steps" on isOnboardingHubVisited false', () => { + const { getByTestId } = render(); + expect(getByTestId('video-modal-button')).toHaveTextContent('Head to steps'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx new file mode 100644 index 0000000000000..c46aadf33a952 --- /dev/null +++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx @@ -0,0 +1,95 @@ +/* + * 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, { useCallback } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeaderTitle, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { ONBOARDING_VIDEO_SOURCE } from '../../../../../common/constants'; +import { useStoredHasVideoVisited } from '../../../use_stored_state'; +import { useOnboardingContext } from '../../../onboarding_context'; +import * as i18n from './translations'; + +// Not ideal, but we could not find any other way to remove the padding from the modal body +const modalStyles = css` + .euiModalBody__overflow { + overflow: hidden; + padding: 0px; + mask-image: none; + } +`; + +interface OnboardingHeaderVideoModalProps { + onClose: () => void; +} + +export const OnboardingHeaderVideoModal = React.memo( + ({ onClose }) => { + const modalTitle = useGeneratedHtmlId(); + const { spaceId } = useOnboardingContext(); + const [isVideoVisited, setIsVideoVisited] = useStoredHasVideoVisited(spaceId); + + const closeModal = useCallback(() => { + setIsVideoVisited(true); + onClose(); + }, [onClose, setIsVideoVisited]); + + return ( + + +