diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx index 3a19a68643006..baed2ff4cdb86 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/settings_context_menu/settings_context_menu.tsx @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import { euiThemeVars } from '@kbn/ui-theme'; +import { KnowledgeBaseTour } from '../../../tour/knowledge_base'; import { AnonymizationSettingsManagement } from '../../../data_anonymization/settings/anonymization_settings_management'; import { useAssistantContext } from '../../../..'; import * as i18n from '../../assistant_header/translations'; @@ -172,13 +173,15 @@ export const SettingsContextMenu: React.FC = React.memo( <> + + + } isOpen={isPopoverOpen} closePopover={closePopover} diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx index b199039b4efae..e3a86c62d1222 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/knowledge_base_settings_management/index.tsx @@ -30,6 +30,7 @@ import { } from '@kbn/elastic-assistant-common'; import { css } from '@emotion/react'; import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { KnowledgeBaseTour } from '../../tour/knowledge_base'; import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management'; import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries'; import { useAssistantContext } from '../../assistant_context'; @@ -295,7 +296,6 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d ); } - return ( <> @@ -412,6 +412,7 @@ export const KnowledgeBaseSettingsManagement: React.FC = React.memo(({ d

{i18n.DELETE_ENTRY_CONFIRMATION_CONTENT}

)} + ); }); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/const.ts b/x-pack/packages/kbn-elastic-assistant/impl/tour/const.ts new file mode 100644 index 0000000000000..1c79500792ba6 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/const.ts @@ -0,0 +1,10 @@ +/* + * 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 NEW_FEATURES_TOUR_STORAGE_KEYS = { + KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16', +}; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx new file mode 100644 index 0000000000000..4dfd4657212f8 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.test.tsx @@ -0,0 +1,142 @@ +/* + * 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, screen } from '@testing-library/react'; +import { EuiTourStepProps } from '@elastic/eui'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { KnowledgeBaseTour } from '.'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { useAssistantContext } from '../../..'; +jest.mock('../../..'); +jest.mock('react-use/lib/useLocalStorage'); +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + EuiTourStep: ({ children, panelProps }: EuiTourStepProps) => + children ? ( +
{children}
+ ) : ( +
+ ), + }; +}); + +describe('Attack discovery tour', () => { + const persistToLocalStorage = jest.fn(); + const navigateToApp = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + (useAssistantContext as jest.Mock).mockReturnValue({ + navigateToApp, + assistantFeatures: { + assistantKnowledgeBaseByDefault: true, + }, + }); + jest.mocked(useLocalStorage).mockReturnValue([ + { + currentTourStep: 1, + isTourActive: true, + }, + persistToLocalStorage, + ] as unknown as ReturnType); + }); + + it('should not render any tour steps when tour is not activated', () => { + jest.mocked(useLocalStorage).mockReturnValue([ + { + currentTourStep: 1, + isTourActive: false, + }, + persistToLocalStorage, + ] as unknown as ReturnType); + render( + +

{'Hello world'}

+
, + { + wrapper: TestProviders, + } + ); + expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull(); + expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull(); + }); + + it('should not render any tour steps when knowledge base feature flag is not activated', () => { + (useAssistantContext as jest.Mock).mockReturnValue({ + navigateToApp, + assistantFeatures: { + assistantKnowledgeBaseByDefault: false, + }, + }); + render( + +

{'Hello world'}

+
, + { + wrapper: TestProviders, + } + ); + expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull(); + expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull(); + }); + + it('should not render any tour steps when tour is on step 2 and page is not knowledge base', () => { + jest.mocked(useLocalStorage).mockReturnValue([ + { + currentTourStep: 2, + isTourActive: true, + }, + persistToLocalStorage, + ] as unknown as ReturnType); + render( + +

{'Hello world'}

+
, + { + wrapper: TestProviders, + } + ); + expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull(); + }); + + it('should render tour step 1 when element is mounted', async () => { + const { getByTestId } = render( + +

{'Hello world'}

+
, + { + wrapper: TestProviders, + } + ); + + expect(getByTestId('knowledgeBase-tour-step-1')).toBeInTheDocument(); + }); + + it('should render tour video when tour is on step 2 and page is knowledge base', () => { + jest.mocked(useLocalStorage).mockReturnValue([ + { + currentTourStep: 2, + isTourActive: true, + }, + persistToLocalStorage, + ] as unknown as ReturnType); + const { getByTestId } = render(, { + wrapper: TestProviders, + }); + expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull(); + expect(getByTestId('knowledgeBase-tour-step-2')).toBeInTheDocument(); + }); + + it('should advance to tour step 2 when page is knowledge base', () => { + render(, { wrapper: TestProviders }); + const nextStep = persistToLocalStorage.mock.calls[0][0]; + expect(nextStep()).toEqual({ isTourActive: true, currentTourStep: 2 }); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx new file mode 100644 index 0000000000000..f7ef0252147c0 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/index.tsx @@ -0,0 +1,137 @@ +/* + * 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. + */ + +/* + * The knowledge base tour for 8.14 + * + * */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EuiButton, EuiButtonEmpty, EuiTourStep, EuiTourStepProps } from '@elastic/eui'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { KNOWLEDGE_BASE_TAB } from '../../assistant/settings/const'; +import { useAssistantContext } from '../../..'; +import { VideoToast } from './video_toast'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const'; +import { knowledgeBaseTourStepOne, tourConfig } from './step_config'; +import * as i18n from './translations'; + +interface TourState { + currentTourStep: number; + isTourActive: boolean; +} +const KnowledgeBaseTourComp: React.FC<{ + children?: EuiTourStepProps['children']; + isKbSettingsPage?: boolean; +}> = ({ children, isKbSettingsPage = false }) => { + const { + navigateToApp, + assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault }, + } = useAssistantContext(); + + const [tourState, setTourState] = useLocalStorage( + NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, + tourConfig + ); + + const advanceToVideoStep = useCallback( + () => + setTourState((prev = tourConfig) => ({ + ...prev, + currentTourStep: 2, + })), + [setTourState] + ); + + useEffect(() => { + if (tourState?.isTourActive && isKbSettingsPage) { + advanceToVideoStep(); + } + }, [advanceToVideoStep, isKbSettingsPage, tourState?.isTourActive]); + + const finishTour = useCallback( + () => + setTourState((prev = tourConfig) => ({ + ...prev, + isTourActive: false, + })), + [setTourState] + ); + + const navigateToKnowledgeBase = useCallback( + () => + navigateToApp('management', { + path: `kibana/securityAiAssistantManagement?tab=${KNOWLEDGE_BASE_TAB}`, + }), + [navigateToApp] + ); + + const nextStep = useCallback(() => { + if (tourState?.currentTourStep === 1) { + navigateToKnowledgeBase(); + advanceToVideoStep(); + } + }, [tourState?.currentTourStep, navigateToKnowledgeBase, advanceToVideoStep]); + + const footerAction = useMemo( + () => [ + // if exit, set tour to the video step without navigating to the page + + {i18n.KNOWLEDGE_BASE_TOUR_EXIT} + , + // if next, set tour to the video step and navigate to the page + + {i18n.KNOWLEDGE_BASE_TRY_IT} + , + ], + [advanceToVideoStep, nextStep] + ); + + const isTestAutomation = + // @ts-ignore + window.Cypress != null || // TODO: temporary workaround to disable the tour when running in Cypress, because the tour breaks other projects Cypress tests + navigator.webdriver === true; // TODO: temporary workaround to disable the tour when running in the FTR, because the tour breaks other projects FTR tests + + const [isTimerExhausted, setIsTimerExhausted] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => { + setIsTimerExhausted(true); + }, 1000); + + return () => clearTimeout(timer); + }, []); + + if (!enableKnowledgeBaseByDefault || isTestAutomation || !tourState?.isTourActive) { + return children ?? null; + } + + return tourState?.currentTourStep === 1 && children ? ( + + {children} + + ) : isKbSettingsPage ? ( + + ) : ( + children ?? null + ); +}; + +export const KnowledgeBaseTour = React.memo(KnowledgeBaseTourComp); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/overview.gif b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/overview.gif similarity index 100% rename from x-pack/plugins/security_solution/public/attack_discovery/tour/overview.gif rename to x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/overview.gif diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/step_config.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/step_config.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/attack_discovery/tour/step_config.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/step_config.tsx index 6984cbd8542df..bd54fab3fbc42 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/tour/step_config.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/step_config.tsx @@ -7,14 +7,9 @@ import * as i18n from './translations'; -export const ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS = { - NAV_LINK: 'solutionSideNavItemLink-attack_discovery', -}; - -export const attackDiscoveryTourStepOne = { - title: i18n.ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_TITLE, - content: i18n.ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_DESC, - anchor: ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS.NAV_LINK, +export const knowledgeBaseTourStepOne = { + title: i18n.KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_TITLE, + content: i18n.KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_DESC, }; export const tourConfig = { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/translations.ts new file mode 100644 index 0000000000000..4008b5425e848 --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/translations.ts @@ -0,0 +1,59 @@ +/* + * 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 KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_TITLE = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.navStep.title', + { + defaultMessage: 'New: Custom Knowledge Sources', + } +); + +export const KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_DESC = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.navStep.desc', + { + defaultMessage: + 'Access the new settings menu to add custom data sources for use within the AI Assistant.', + } +); + +export const KNOWLEDGE_BASE_TOUR_VIDEO_STEP_TITLE = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.videoStep.title', + { + defaultMessage: 'Introducing Custom Knowledge Sources', + } +); + +export const KNOWLEDGE_BASE_TOUR_VIDEO_STEP_DESC = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.videoStep.desc', + { + defaultMessage: + 'Custom knowledge sources enable you to receive bespoke, tailored responses from the AI Assistant. Watch this video to learn how to add your own data sources and explore examples of how they can be applied in a security operations context.', + } +); + +export const KNOWLEDGE_BASE_TOUR_EXIT = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.exit', + { + defaultMessage: 'Close', + } +); + +export const KNOWLEDGE_BASE_TRY_IT = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.tryIt', + { + defaultMessage: 'Try it', + } +); + +export const WATCH_OVERVIEW_VIDEO = i18n.translate( + 'xpack.elasticAssistant.knowledgeBase.tour.video', + { + defaultMessage: 'Watch overview video', + } +); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/video_toast.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/attack_discovery/tour/video_toast.test.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.test.tsx index 18c5d852e0343..89979efd63fef 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/tour/video_toast.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.test.tsx @@ -20,7 +20,7 @@ describe('VideoToast', () => { jest.restoreAllMocks(); }); it('should render the video toast', () => { - const videoToast = screen.getByTestId('attackDiscovery-tour-step-2'); + const videoToast = screen.getByTestId('knowledgeBase-tour-step-2'); expect(videoToast).toBeInTheDocument(); }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/video_toast.tsx b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx similarity index 72% rename from x-pack/plugins/security_solution/public/attack_discovery/tour/video_toast.tsx rename to x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx index ae44027fa801d..b1b2bfe02a1eb 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/tour/video_toast.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/tour/knowledge_base/video_toast.tsx @@ -19,6 +19,8 @@ import * as i18n from './translations'; import theGif from './overview.gif'; const VIDEO_CONTENT_WIDTH = 250; +// TODO before removing assistantKnowledgeBaseByDefault feature flag +// update the VIDEO_PAGE to the correct URL const VIDEO_PAGE = `https://videos.elastic.co/watch/BrDaDBAAvdygvemFKNAkBW`; const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => { @@ -28,11 +30,17 @@ const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => { return ( -
- +
+ void }> = ({ onClose }) => { />

- {i18n.ATTACK_DISCOVERY_TOUR_VIDEO_STEP_TITLE} + {i18n.KNOWLEDGE_BASE_TOUR_VIDEO_STEP_TITLE}

-

{i18n.ATTACK_DISCOVERY_TOUR_VIDEO_STEP_DESC}

+

{i18n.KNOWLEDGE_BASE_TOUR_VIDEO_STEP_DESC}

diff --git a/x-pack/packages/kbn-elastic-assistant/tsconfig.json b/x-pack/packages/kbn-elastic-assistant/tsconfig.json index f1dde6cbeafee..15009c7f702a5 100644 --- a/x-pack/packages/kbn-elastic-assistant/tsconfig.json +++ b/x-pack/packages/kbn-elastic-assistant/tsconfig.json @@ -5,7 +5,8 @@ "types": [ "jest", "node", - "react" + "react", + "@kbn/ambient-ui-types" ] }, "include": [ diff --git a/x-pack/packages/security-solution/side_nav/src/solution_side_nav.test.tsx b/x-pack/packages/security-solution/side_nav/src/solution_side_nav.test.tsx index 91ba09202a13e..b5db8e66f10dd 100644 --- a/x-pack/packages/security-solution/side_nav/src/solution_side_nav.test.tsx +++ b/x-pack/packages/security-solution/side_nav/src/solution_side_nav.test.tsx @@ -167,11 +167,5 @@ describe('SolutionSideNav', () => { expect(result.queryByTestId('solutionSideNavPanel')).toBeInTheDocument(); expect(result.getByText('Users')).toBeInTheDocument(); }); - - it('should call onMount when function is provided', () => { - const onMount = jest.fn(); - renderNav({ onMount }); - expect(onMount).toHaveBeenCalled(); - }); }); }); diff --git a/x-pack/packages/security-solution/side_nav/src/solution_side_nav.tsx b/x-pack/packages/security-solution/side_nav/src/solution_side_nav.tsx index 1615df71011f7..0b06dfd75169a 100644 --- a/x-pack/packages/security-solution/side_nav/src/solution_side_nav.tsx +++ b/x-pack/packages/security-solution/side_nav/src/solution_side_nav.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EuiListGroup, EuiFlexGroup, @@ -50,7 +50,6 @@ export interface SolutionSideNavProps { * e.g.: usageCollection?.reportUiCounter?.bind(null, appId) * */ tracker?: Tracker; - onMount?: () => void; } type ActivePanelNav = string | null; /** @@ -63,7 +62,6 @@ export const SolutionSideNav: React.FC = React.memo(functi panelBottomOffset, panelTopOffset, tracker, - onMount, }) { const isMobileSize = useIsWithinBreakpoints(['xs', 's']); @@ -75,10 +73,6 @@ export const SolutionSideNav: React.FC = React.memo(functi setActivePanelNavId(id); }; - useEffect(() => { - if (onMount) onMount(); - }, [onMount]); - const onClosePanelNav = useCallback(() => { activePanelNavIdRef.current = null; setActivePanelNavId(null); diff --git a/x-pack/plugins/osquery/cypress/tasks/navigation.ts b/x-pack/plugins/osquery/cypress/tasks/navigation.ts index 3f7814c7feb2d..72107207d7a8c 100644 --- a/x-pack/plugins/osquery/cypress/tasks/navigation.ts +++ b/x-pack/plugins/osquery/cypress/tasks/navigation.ts @@ -47,7 +47,7 @@ export const NEW_FEATURES_TOUR_STORAGE_KEYS = { TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour', TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12', FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14', - ATTACK_DISCOVERY: 'securitySolution.attackDiscovery.newFeaturesTour.v8.14', + KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16', }; /** diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2fd83a4849a75..d0fb117eafc50 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -424,7 +424,6 @@ export const NEW_FEATURES_TOUR_STORAGE_KEYS = { TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour', TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12', FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14', - ATTACK_DISCOVERY: 'securitySolution.attackDiscovery.newFeaturesTour.v8.14', }; export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY = diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index b6492ef97cae7..19e8d55aa2dd5 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React, { type ReactNode, useMemo, useState, useCallback } from 'react'; +import React, { type ReactNode, useMemo } from 'react'; import styled from 'styled-components'; import { EuiThemeProvider, useEuiTheme, type EuiThemeComputed } from '@elastic/eui'; import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout'; -import { AttackDiscoveryTour } from '../../../attack_discovery/tour'; import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state'; import { SecuritySolutionFlyout, TimelineFlyout } from '../../../flyout'; import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation'; @@ -56,11 +55,7 @@ export type SecuritySolutionTemplateWrapperProps = Omit = React.memo(({ children, ...rest }) => { - const [didMount, setDidMount] = useState(false); - const onMount = useCallback(() => { - setDidMount(true); - }, []); - const solutionNavProps = useSecuritySolutionNavigation(onMount); + const solutionNavProps = useSecuritySolutionNavigation(); const [isTimelineBottomBarVisible] = useShowTimeline(); const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []); const { show: isShowingTimelineOverlay } = useDeepEqualSelector((state) => @@ -107,8 +102,6 @@ export const SecuritySolutionTemplateWrapper: React.FC - - {didMount && } {isTimelineBottomBarVisible && ( diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/tour/index.test.tsx deleted file mode 100644 index fc3f2a2344a14..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/tour/index.test.tsx +++ /dev/null @@ -1,137 +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 from 'react'; - -import { useIsElementMounted } from '../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted'; -import { render, screen } from '@testing-library/react'; -import { - createMockStore, - createSecuritySolutionStorageMock, - TestProviders, -} from '../../common/mock'; -import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__'; -import { useKibana } from '../../common/lib/kibana'; -import { AttackDiscoveryTour } from '.'; -import { ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS } from './step_config'; -import { NEW_FEATURES_TOUR_STORAGE_KEYS, SecurityPageName } from '../../../common/constants'; -import type { RouteSpyState } from '../../common/utils/route/types'; -import { useRouteSpy } from '../../common/utils/route/use_route_spy'; - -const mockRouteSpy: RouteSpyState = { - pageName: SecurityPageName.overview, - detailName: undefined, - tabName: undefined, - search: '', - pathName: '/', -}; -jest.mock( - '../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted' -); -jest.mock('../../common/lib/kibana'); -jest.mock('../../common/utils/route/use_route_spy'); -jest.mock('@elastic/eui', () => { - const original = jest.requireActual('@elastic/eui'); - return { - ...original, - EuiTourStep: () =>
, - }; -}); -const mockedUseKibana = mockUseKibana(); - -const { storage: storageMock } = createSecuritySolutionStorageMock(); -const mockStore = createMockStore(undefined, undefined, undefined, storageMock); - -const TestComponent = () => { - return ( - -
- - - ); -}; - -describe('Attack discovery tour', () => { - beforeAll(() => { - (useIsElementMounted as jest.Mock).mockReturnValue(true); - (useRouteSpy as jest.Mock).mockReturnValue([mockRouteSpy]); - }); - - beforeEach(() => { - (useKibana as jest.Mock).mockReturnValue({ - ...mockedUseKibana, - services: { - ...mockedUseKibana.services, - storage: storageMock, - }, - }); - - storageMock.clear(); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should not render tour step 1 when element is not mounted', () => { - (useIsElementMounted as jest.Mock).mockReturnValueOnce(false); - render(); - expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull(); - }); - - it('should not render any tour steps when tour is not activated', () => { - storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, { - currentTourStep: 1, - isTourActive: false, - }); - render(); - expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull(); - expect(screen.queryByTestId('attackDiscovery-tour-step-2')).toBeNull(); - }); - - it('should not render any tour steps when tour is on step 2 and page is not attack discovery', () => { - storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, { - currentTourStep: 2, - isTourActive: true, - }); - const { debug } = render(); - expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull(); - debug(); - }); - - it('should render tour step 1 when element is mounted', async () => { - const { getByTestId } = render(); - - expect(getByTestId('attackDiscovery-tour-step-1')).toBeInTheDocument(); - }); - - it('should render tour video when tour is on step 2 and page is attack discovery', () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { ...mockRouteSpy, pageName: SecurityPageName.attackDiscovery }, - ]); - storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, { - currentTourStep: 2, - isTourActive: true, - }); - const { getByTestId } = render(); - expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull(); - expect(getByTestId('attackDiscovery-tour-step-2')).toBeInTheDocument(); - }); - - it('should advance to tour step 2 when page is attack discovery', () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { ...mockRouteSpy, pageName: SecurityPageName.attackDiscovery }, - ]); - storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, { - currentTourStep: 1, - isTourActive: true, - }); - render(); - expect( - storageMock.get(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY).currentTourStep - ).toEqual(2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/tour/index.tsx deleted file mode 100644 index ebddaca6d3f45..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/tour/index.tsx +++ /dev/null @@ -1,133 +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. - */ - -/* - * The attack discovery tour for 8.14 - * - * */ - -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { EuiButton, EuiButtonEmpty, EuiTourStep } from '@elastic/eui'; -import { useRouteSpy } from '../../common/utils/route/use_route_spy'; -import { VideoToast } from './video_toast'; -import { useIsElementMounted } from '../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted'; -import { NEW_FEATURES_TOUR_STORAGE_KEYS, SecurityPageName } from '../../../common/constants'; -import { useKibana, useNavigation } from '../../common/lib/kibana'; -import { attackDiscoveryTourStepOne, tourConfig } from './step_config'; -import * as i18n from './translations'; - -interface TourState { - currentTourStep: number; - isTourActive: boolean; -} - -const AttackDiscoveryTourComp = () => { - const { - services: { storage }, - } = useKibana(); - - const { navigateTo } = useNavigation(); - const [{ pageName }] = useRouteSpy(); - const [tourState, setTourState] = useState( - storage.get(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY) ?? tourConfig - ); - - const advanceToVideoStep = useCallback(() => { - setTourState((prev) => { - storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, { - ...prev, - currentTourStep: 2, - }); - return { - ...prev, - currentTourStep: 2, - }; - }); - }, [storage]); - - useEffect(() => { - if (tourState.isTourActive && pageName === SecurityPageName.attackDiscovery) { - advanceToVideoStep(); - } - }, [advanceToVideoStep, pageName, tourState.isTourActive]); - - const finishTour = useCallback(() => { - setTourState((prev) => { - storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, { - ...prev, - isTourActive: false, - }); - return { - ...prev, - isTourActive: false, - }; - }); - }, [storage]); - - const navigateToAttackDiscovery = useCallback(() => { - navigateTo({ - deepLinkId: SecurityPageName.attackDiscovery, - }); - }, [navigateTo]); - - const nextStep = useCallback(() => { - if (tourState.currentTourStep === 1) { - navigateToAttackDiscovery(); - advanceToVideoStep(); - } - }, [tourState.currentTourStep, navigateToAttackDiscovery, advanceToVideoStep]); - - const footerAction = useMemo( - () => [ - // if exit, set tour to the video step without navigating to the page - - {i18n.ATTACK_DISCOVERY_TOUR_EXIT} - , - // if next, set tour to the video step and navigate to the page - - {i18n.ATTACK_DISCOVERY_TRY_IT} - , - ], - [advanceToVideoStep, nextStep] - ); - - const isElementAtCurrentStepMounted = useIsElementMounted(attackDiscoveryTourStepOne?.anchor); - - const isTestAutomation = - window.Cypress != null || // TODO: temporary workaround to disable the tour when running in Cypress, because the tour breaks other projects Cypress tests - navigator.webdriver === true; // TODO: temporary workaround to disable the tour when running in the FTR, because the tour breaks other projects FTR tests - - if ( - isTestAutomation || - !tourState.isTourActive || - (tourState.currentTourStep === 1 && !isElementAtCurrentStepMounted) - ) { - return null; - } - - return tourState.currentTourStep === 1 ? ( - - ) : pageName === SecurityPageName.attackDiscovery ? ( - - ) : null; -}; - -export const AttackDiscoveryTour = React.memo(AttackDiscoveryTourComp); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/tour/translations.ts b/x-pack/plugins/security_solution/public/attack_discovery/tour/translations.ts deleted file mode 100644 index eadfb384297c0..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/tour/translations.ts +++ /dev/null @@ -1,59 +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 { i18n } from '@kbn/i18n'; - -export const ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_TITLE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.navStep.title', - { - defaultMessage: 'Introducing attack discovery', - } -); - -export const ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_DESC = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.navStep.desc', - { - defaultMessage: - 'Leverage Generative AI to find relationships among your alerts and describe attack chains.', - } -); - -export const ATTACK_DISCOVERY_TOUR_VIDEO_STEP_TITLE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.videoStep.title', - { - defaultMessage: 'Start discovering attacks', - } -); - -export const ATTACK_DISCOVERY_TOUR_VIDEO_STEP_DESC = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.videoStep.desc', - { - defaultMessage: - 'Dive into data-driven attack discoveries and streamline your workflow with our intuitive AI technology, designed to elevate your productivity instantly.', - } -); - -export const ATTACK_DISCOVERY_TOUR_EXIT = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.exit', - { - defaultMessage: 'Close', - } -); - -export const ATTACK_DISCOVERY_TRY_IT = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.tryIt', - { - defaultMessage: 'Try it', - } -); - -export const WATCH_OVERVIEW_VIDEO = i18n.translate( - 'xpack.securitySolution.attackDiscovery.tour.video', - { - defaultMessage: 'Watch overview video', - } -); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx index 6b327df2a847c..47ff3e6e4fad1 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -137,7 +137,7 @@ const usePanelBottomOffset = (): string | undefined => { * Main security navigation component. * It takes the links to render from the generic application `links` configs. */ -export const SecuritySideNav: React.FC<{ onMount?: () => void }> = ({ onMount }) => { +export const SecuritySideNav: React.FC = () => { const items = useSolutionSideNavItems(); const selectedId = useSelectedId(); const panelTopOffset = usePanelTopOffset(); @@ -151,7 +151,6 @@ export const SecuritySideNav: React.FC<{ onMount?: () => void }> = ({ onMount }) void -): KibanaPageTemplateProps['solutionNav'] => { +export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['solutionNav'] => { const { chrome } = useKibana().services; const chromeStyle$ = useMemo(() => chrome.getChromeStyle$(), [chrome]); const chromeStyle = useObservable(chromeStyle$, 'classic'); @@ -41,7 +39,7 @@ export const useSecuritySolutionNavigation = ( canBeCollapsed: true, name: translatedNavTitle, icon: 'logoSecurity', - children: , + children: , closeFlyoutButtonPosition: 'inside', }; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index fc240ba84ad29..296607b92f2f0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35302,13 +35302,6 @@ "xpack.securitySolution.attackDiscovery.summaryCount.alertsLabel": "{alertsCount} {alertsCount, plural, =1 {alerte} other {alertes}}", "xpack.securitySolution.attackDiscovery.summaryCount.discoveriesLabel": "{attackDiscoveriesCount} {attackDiscoveriesCount, plural, =1 {découverte} other {découvertes}}", "xpack.securitySolution.attackDiscovery.summaryCount.lastGeneratedLabel": "Généré", - "xpack.securitySolution.attackDiscovery.tour.exit": "Fermer", - "xpack.securitySolution.attackDiscovery.tour.navStep.desc": "Tirez parti de l’IA générative pour trouver des relations entre vos alertes et détailler les chaînes d’attaque.", - "xpack.securitySolution.attackDiscovery.tour.navStep.title": "Présentation d’Attack Discovery", - "xpack.securitySolution.attackDiscovery.tour.tryIt": "Essayer", - "xpack.securitySolution.attackDiscovery.tour.video": "Regardez la vidéo de présentation", - "xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "Plongez dans les découvertes d'attaques axées sur les données et rationalisez votre flux de travail grâce à notre technologie d'IA intuitive, conçue pour accroître instantanément votre productivité.", - "xpack.securitySolution.attackDiscovery.tour.videoStep.title": "Démarrez la découverte des attaques", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "démarrage de l'audit abandonné", "xpack.securitySolution.auditd.accessErrorDescription": "erreur d'accès", "xpack.securitySolution.auditd.accessPermissionDescription": "autorisation d'accès", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4d39022965da0..7f9fcb170e919 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35047,13 +35047,6 @@ "xpack.securitySolution.attackDiscovery.summaryCount.alertsLabel": "{alertsCount} {alertsCount, plural, other {件のアラート}}", "xpack.securitySolution.attackDiscovery.summaryCount.discoveriesLabel": "{attackDiscoveriesCount} {attackDiscoveriesCount, plural, other {件の検出}}", "xpack.securitySolution.attackDiscovery.summaryCount.lastGeneratedLabel": "生成済み", - "xpack.securitySolution.attackDiscovery.tour.exit": "閉じる", - "xpack.securitySolution.attackDiscovery.tour.navStep.desc": "生成AIを活用して、アラート全体の関係を特定し、攻撃チェーンを解析します。", - "xpack.securitySolution.attackDiscovery.tour.navStep.title": "Attack Discoveryの概要", - "xpack.securitySolution.attackDiscovery.tour.tryIt": "お試しください", - "xpack.securitySolution.attackDiscovery.tour.video": "概要動画を視聴", - "xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "データ主導のAttack Discoveryを導入し、生産性を即時に高めるために設計されたElasticの直感的なAI技術でワークフローを合理化しましょう。", - "xpack.securitySolution.attackDiscovery.tour.videoStep.title": "攻撃の検出を開始", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "中断された監査のスタートアップ", "xpack.securitySolution.auditd.accessErrorDescription": "アクセスエラー", "xpack.securitySolution.auditd.accessPermissionDescription": "アクセス権限", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 27ee1dd00a563..6f1c8da6d1b40 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35090,13 +35090,6 @@ "xpack.securitySolution.attackDiscovery.summaryCount.alertsLabel": "{alertsCount} 个{alertsCount, plural, other {告警}}", "xpack.securitySolution.attackDiscovery.summaryCount.discoveriesLabel": "{attackDiscoveriesCount} 个{attackDiscoveriesCount, plural, other {发现}}", "xpack.securitySolution.attackDiscovery.summaryCount.lastGeneratedLabel": "已生成", - "xpack.securitySolution.attackDiscovery.tour.exit": "关闭", - "xpack.securitySolution.attackDiscovery.tour.navStep.desc": "利用生成式 AI 找出您的告警之间的关系并描述攻击链。", - "xpack.securitySolution.attackDiscovery.tour.navStep.title": "Attack Discovery 简介", - "xpack.securitySolution.attackDiscovery.tour.tryIt": "试用", - "xpack.securitySolution.attackDiscovery.tour.video": "观看概述视频", - "xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "深入了解数据驱动式 Attack Discovery,并利用旨在即时提高生产力的直观式 AI 技术精简您的工作流。", - "xpack.securitySolution.attackDiscovery.tour.videoStep.title": "开始发现攻击", "xpack.securitySolution.auditd.abortedAuditStartupDescription": "已中止审计启动", "xpack.securitySolution.auditd.accessErrorDescription": "访问错误", "xpack.securitySolution.auditd.accessPermissionDescription": "访问权限",