mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[8.16] [Security Solution] Repurpose attack discover tour into knowledge base tour (#196615) (#197535)
# Backport This will backport the following commits from `main` to `8.16`: - [[Security Solution] Repurpose attack discover tour into knowledge base tour (#196615)](https://github.com/elastic/kibana/pull/196615) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Steph Milovic","email":"stephanie.milovic@elastic.co"},"sourceCommit":{"committedDate":"2024-10-23T21:02:35Z","message":"[Security Solution] Repurpose attack discover tour into knowledge base tour (#196615)","sha":"fa9bb19f14648bbe34493481df0b32838d0e5734","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team: SecuritySolution","backport:prev-minor","Team:Security Generative AI","v8.16.0"],"title":"[Security Solution] Repurpose attack discover tour into knowledge base tour","number":196615,"url":"https://github.com/elastic/kibana/pull/196615","mergeCommit":{"message":"[Security Solution] Repurpose attack discover tour into knowledge base tour (#196615)","sha":"fa9bb19f14648bbe34493481df0b32838d0e5734"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196615","number":196615,"mergeCommit":{"message":"[Security Solution] Repurpose attack discover tour into knowledge base tour (#196615)","sha":"fa9bb19f14648bbe34493481df0b32838d0e5734"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
871137b456
commit
a8d1c24dab
24 changed files with 388 additions and 405 deletions
|
@ -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<Params> = React.memo(
|
|||
<>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="test"
|
||||
isDisabled={isDisabled}
|
||||
iconType="boxesVertical"
|
||||
onClick={onButtonClick}
|
||||
data-test-subj="chat-context-menu"
|
||||
/>
|
||||
<KnowledgeBaseTour>
|
||||
<EuiButtonIcon
|
||||
aria-label="test"
|
||||
isDisabled={isDisabled}
|
||||
iconType="boxesVertical"
|
||||
onClick={onButtonClick}
|
||||
data-test-subj="chat-context-menu"
|
||||
/>
|
||||
</KnowledgeBaseTour>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
|
|
|
@ -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<Params> = React.memo(({ d
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
|
||||
|
@ -412,6 +412,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
|
|||
<p>{i18n.DELETE_ENTRY_CONFIRMATION_CONTENT}</p>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
<KnowledgeBaseTour isKbSettingsPage />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
10
x-pack/packages/kbn-elastic-assistant/impl/tour/const.ts
Normal file
10
x-pack/packages/kbn-elastic-assistant/impl/tour/const.ts
Normal file
|
@ -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',
|
||||
};
|
|
@ -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 ? (
|
||||
<div data-test-subj={panelProps?.['data-test-subj']}>{children}</div>
|
||||
) : (
|
||||
<div data-test-subj={panelProps?.['data-test-subj']} />
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
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<typeof useLocalStorage>);
|
||||
});
|
||||
|
||||
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<typeof useLocalStorage>);
|
||||
render(
|
||||
<KnowledgeBaseTour>
|
||||
<h1>{'Hello world'}</h1>
|
||||
</KnowledgeBaseTour>,
|
||||
{
|
||||
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(
|
||||
<KnowledgeBaseTour>
|
||||
<h1>{'Hello world'}</h1>
|
||||
</KnowledgeBaseTour>,
|
||||
{
|
||||
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<typeof useLocalStorage>);
|
||||
render(
|
||||
<KnowledgeBaseTour>
|
||||
<h1>{'Hello world'}</h1>
|
||||
</KnowledgeBaseTour>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render tour step 1 when element is mounted', async () => {
|
||||
const { getByTestId } = render(
|
||||
<KnowledgeBaseTour>
|
||||
<h1>{'Hello world'}</h1>
|
||||
</KnowledgeBaseTour>,
|
||||
{
|
||||
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<typeof useLocalStorage>);
|
||||
const { getByTestId } = render(<KnowledgeBaseTour isKbSettingsPage />, {
|
||||
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(<KnowledgeBaseTour isKbSettingsPage />, { wrapper: TestProviders });
|
||||
const nextStep = persistToLocalStorage.mock.calls[0][0];
|
||||
expect(nextStep()).toEqual({ isTourActive: true, currentTourStep: 2 });
|
||||
});
|
||||
});
|
|
@ -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<TourState>(
|
||||
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
|
||||
<EuiButtonEmpty size="s" color="text" onClick={advanceToVideoStep}>
|
||||
{i18n.KNOWLEDGE_BASE_TOUR_EXIT}
|
||||
</EuiButtonEmpty>,
|
||||
// if next, set tour to the video step and navigate to the page
|
||||
<EuiButton color="success" size="s" onClick={nextStep}>
|
||||
{i18n.KNOWLEDGE_BASE_TRY_IT}
|
||||
</EuiButton>,
|
||||
],
|
||||
[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 ? (
|
||||
<EuiTourStep
|
||||
anchorPosition={'downRight'}
|
||||
content={knowledgeBaseTourStepOne.content}
|
||||
footerAction={footerAction}
|
||||
isStepOpen={isTimerExhausted}
|
||||
maxWidth={450}
|
||||
onFinish={advanceToVideoStep}
|
||||
panelProps={{
|
||||
'data-test-subj': `knowledgeBase-tour-step-1`,
|
||||
}}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={knowledgeBaseTourStepOne.title}
|
||||
>
|
||||
{children}
|
||||
</EuiTourStep>
|
||||
) : isKbSettingsPage ? (
|
||||
<VideoToast onClose={finishTour} />
|
||||
) : (
|
||||
children ?? null
|
||||
);
|
||||
};
|
||||
|
||||
export const KnowledgeBaseTour = React.memo(KnowledgeBaseTourComp);
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
@ -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 = {
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<EuiPortal>
|
||||
<div
|
||||
data-test-subj="attackDiscovery-tour-step-2"
|
||||
css={{ position: 'fixed', bottom: 16, right: 16, zIndex: 9999 }}
|
||||
>
|
||||
<EuiToast onClose={onClose} css={{ maxWidth: VIDEO_CONTENT_WIDTH }}>
|
||||
<div data-test-subj="knowledgeBase-tour-step-2">
|
||||
<EuiToast
|
||||
onClose={onClose}
|
||||
css={{
|
||||
maxWidth: VIDEO_CONTENT_WIDTH,
|
||||
position: 'fixed',
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<EuiImage
|
||||
onClick={openVideoInNewTab}
|
||||
css={{ marginTop: 20, '&:hover': { cursor: 'pointer' } }}
|
||||
|
@ -42,9 +50,9 @@ const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => {
|
|||
/>
|
||||
<EuiText size="s" grow={false} css={{ marginTop: 20 }}>
|
||||
<h4>
|
||||
<EuiIcon type="cheer" color="success" /> {i18n.ATTACK_DISCOVERY_TOUR_VIDEO_STEP_TITLE}
|
||||
<EuiIcon type="cheer" color="success" /> {i18n.KNOWLEDGE_BASE_TOUR_VIDEO_STEP_TITLE}
|
||||
</h4>
|
||||
<p>{i18n.ATTACK_DISCOVERY_TOUR_VIDEO_STEP_DESC}</p>
|
||||
<p>{i18n.KNOWLEDGE_BASE_TOUR_VIDEO_STEP_DESC}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton color="success" onClick={openVideoInNewTab} fullWidth>
|
|
@ -5,7 +5,8 @@
|
|||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
"react",
|
||||
"@kbn/ambient-ui-types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<SolutionSideNavProps> = React.memo(functi
|
|||
panelBottomOffset,
|
||||
panelTopOffset,
|
||||
tracker,
|
||||
onMount,
|
||||
}) {
|
||||
const isMobileSize = useIsWithinBreakpoints(['xs', 's']);
|
||||
|
||||
|
@ -75,10 +73,6 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
|
|||
setActivePanelNavId(id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (onMount) onMount();
|
||||
}, [onMount]);
|
||||
|
||||
const onClosePanelNav = useCallback(() => {
|
||||
activePanelNavIdRef.current = null;
|
||||
setActivePanelNavId(null);
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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<KibanaPageTemplateProps,
|
|||
|
||||
export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionTemplateWrapperProps> =
|
||||
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<SecuritySolutionTemplateW
|
|||
{children}
|
||||
<SecuritySolutionFlyout />
|
||||
</ExpandableFlyoutProvider>
|
||||
|
||||
{didMount && <AttackDiscoveryTour />}
|
||||
</KibanaPageTemplate.Section>
|
||||
{isTimelineBottomBarVisible && (
|
||||
<KibanaPageTemplate.BottomBar data-test-subj="timeline-bottom-bar-container">
|
||||
|
|
|
@ -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: () => <div data-test-subj="attackDiscovery-tour-step-1" />,
|
||||
};
|
||||
});
|
||||
const mockedUseKibana = mockUseKibana();
|
||||
|
||||
const { storage: storageMock } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createMockStore(undefined, undefined, undefined, storageMock);
|
||||
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<TestProviders store={mockStore}>
|
||||
<div id={ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS.NAV_LINK} />
|
||||
<AttackDiscoveryTour />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
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(<TestComponent />);
|
||||
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(<TestComponent />);
|
||||
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(<TestComponent />);
|
||||
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
|
||||
debug();
|
||||
});
|
||||
|
||||
it('should render tour step 1 when element is mounted', async () => {
|
||||
const { getByTestId } = render(<TestComponent />);
|
||||
|
||||
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(<TestComponent />);
|
||||
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(<TestComponent />);
|
||||
expect(
|
||||
storageMock.get(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY).currentTourStep
|
||||
).toEqual(2);
|
||||
});
|
||||
});
|
|
@ -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<TourState>(
|
||||
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
|
||||
<EuiButtonEmpty size="s" color="text" onClick={advanceToVideoStep}>
|
||||
{i18n.ATTACK_DISCOVERY_TOUR_EXIT}
|
||||
</EuiButtonEmpty>,
|
||||
// if next, set tour to the video step and navigate to the page
|
||||
<EuiButton color="success" size="s" onClick={nextStep}>
|
||||
{i18n.ATTACK_DISCOVERY_TRY_IT}
|
||||
</EuiButton>,
|
||||
],
|
||||
[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 ? (
|
||||
<EuiTourStep
|
||||
anchor={`#${attackDiscoveryTourStepOne.anchor}`}
|
||||
content={attackDiscoveryTourStepOne.content}
|
||||
footerAction={footerAction}
|
||||
isStepOpen
|
||||
maxWidth={450}
|
||||
onFinish={advanceToVideoStep}
|
||||
panelProps={{
|
||||
'data-test-subj': `attackDiscovery-tour-step-1`,
|
||||
}}
|
||||
repositionOnScroll
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={attackDiscoveryTourStepOne.title}
|
||||
/>
|
||||
) : pageName === SecurityPageName.attackDiscovery ? (
|
||||
<VideoToast onClose={finishTour} />
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const AttackDiscoveryTour = React.memo(AttackDiscoveryTourComp);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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 })
|
|||
<SolutionSideNav
|
||||
items={items}
|
||||
categories={CATEGORIES}
|
||||
onMount={onMount}
|
||||
selectedId={selectedId}
|
||||
panelTopOffset={panelTopOffset}
|
||||
panelBottomOffset={panelBottomOffset}
|
||||
|
|
|
@ -23,9 +23,7 @@ const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mai
|
|||
defaultMessage: 'Security',
|
||||
});
|
||||
|
||||
export const useSecuritySolutionNavigation = (
|
||||
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: <SecuritySideNav onMount={onMount} />,
|
||||
children: <SecuritySideNav />,
|
||||
closeFlyoutButtonPosition: 'inside',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -35336,13 +35336,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",
|
||||
|
|
|
@ -35081,13 +35081,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": "アクセス権限",
|
||||
|
|
|
@ -35124,13 +35124,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": "访问权限",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue