mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SecuritySolution] Get Started page UI (#172616)
## Summary Fix up for https://github.com/elastic/kibana/pull/171078 Test Env: https://p.elstc.co/paste/vmb8YG18#nCnDFTVE4HZxFK9M4TyHii3Gt4rq0YV25LQK33PqNly <img width="2556" alt="Screenshot 2023-12-05 at 19 00 06" src="ce6e3da7
-c169-4213-85a7-577625b8b350"> **- Add footer section:** https://www.figma.com/file/07wil4wWtUy90m4NTBxZxG/Updated-Security-GSH-Flows%3A?node-id=1574%3A161997&mode=dev <img width="748" alt="Screenshot 2023-12-05 at 18 42 36" src="596f1968
-f754-4bbc-a5a6-e6987bb96699"> **- Expand / Collapse task fix up:** 1. When no data integrated, clicking on `Add integrations step`from the callout should expand the step. 2. When visiting get started page with hash, it should expand the target step: e.g.: `/app/security/get_started#add_integrations` 3. All tasks should be collapsable.91f8fe94
-1c9d-48ef-be74-6f65bb63dfbd **- Designer review:** 1. Background color for task icons: ```Task not completed``` Background-grey on all states: Default, Hover, Expanded ```Task completed``` Background-green on all states: Default, Hover, Expanded  5. Remove shadow on create project image:  6. Change the gab between task to 16px:  7. Apply **bold** to completed task counts:  8. Update badge padding:  ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
9034cb6181
commit
45cbd2b743
39 changed files with 959 additions and 261 deletions
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const ContentWrapper = ({ children }: { children: React.ReactElement }) => <>{children}</>;
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 connectToDataSources from '../../images/connect_to_existing_sources.png';
|
||||||
|
import { ADD_INTEGRATIONS_IMAGE_TITLE } from '../../translations';
|
||||||
|
import { ContentWrapper } from './content_wrapper';
|
||||||
|
|
||||||
|
const AddIntegrationsImageComponent = () => {
|
||||||
|
return (
|
||||||
|
<ContentWrapper>
|
||||||
|
<img
|
||||||
|
src={connectToDataSources}
|
||||||
|
alt={ADD_INTEGRATIONS_IMAGE_TITLE}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</ContentWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddIntegrationsImage = React.memo(AddIntegrationsImageComponent);
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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 { useStepContentStyles } from '../../styles/step_content.styles';
|
||||||
|
|
||||||
|
const ContentWrapperComponent: React.FC<{ children: React.ReactElement; shadow?: boolean }> = ({
|
||||||
|
children,
|
||||||
|
shadow = true,
|
||||||
|
}) => {
|
||||||
|
const { getRightContentStyles } = useStepContentStyles();
|
||||||
|
const rightContentStyles = getRightContentStyles({ shadow });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="right-panel-content" css={rightContentStyles}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContentWrapper = React.memo(ContentWrapperComponent);
|
|
@ -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 React from 'react';
|
||||||
|
import createProjects from '../../images/create_projects.png';
|
||||||
|
import { CREATE_PROJECT_TITLE } from '../../translations';
|
||||||
|
import { ContentWrapper } from './content_wrapper';
|
||||||
|
|
||||||
|
const CreateProjectImageComponent = () => (
|
||||||
|
<ContentWrapper shadow={false}>
|
||||||
|
<img src={createProjects} alt={CREATE_PROJECT_TITLE} height="100%" width="100%" />
|
||||||
|
</ContentWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CreateProjectImage = React.memo(CreateProjectImageComponent);
|
|
@ -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 React from 'react';
|
||||||
|
import enablePrebuiltRules from '../../images/enable_prebuilt_rules.png';
|
||||||
|
import { ENABLE_RULES } from '../../translations';
|
||||||
|
import { ContentWrapper } from './content_wrapper';
|
||||||
|
|
||||||
|
const EnableRuleImageComponent = () => (
|
||||||
|
<ContentWrapper>
|
||||||
|
<img src={enablePrebuiltRules} alt={ENABLE_RULES} height="100%" width="100%" />
|
||||||
|
</ContentWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EnableRuleImage = React.memo(EnableRuleImageComponent);
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { WATCH_VIDEO_DESCRIPTION1, WATCH_VIDEO_DESCRIPTION2 } from '../translations';
|
import { WATCH_VIDEO_DESCRIPTION1, WATCH_VIDEO_DESCRIPTION2 } from '../../translations';
|
||||||
|
|
||||||
const OverviewVideoDescriptionComponent = () => (
|
const OverviewVideoDescriptionComponent = () => (
|
||||||
<>
|
<>
|
|
@ -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 React from 'react';
|
||||||
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
|
import { Video } from './video';
|
||||||
|
import { OverviewSteps, QuickStartSectionCardsId, SectionId } from '../../types';
|
||||||
|
import type { EuiFlexGroupProps } from '@elastic/eui';
|
||||||
|
import { useStepContext } from '../../context/step_context';
|
||||||
|
import { WATCH_VIDEO_BUTTON_TITLE } from '../../translations';
|
||||||
|
import { defaultExpandedCards } from '../../storage';
|
||||||
|
|
||||||
|
jest.mock('../../context/step_context');
|
||||||
|
jest.mock('./content_wrapper');
|
||||||
|
|
||||||
|
jest.mock('@elastic/eui', () => ({
|
||||||
|
EuiFlexGroup: ({ children, onClick }: EuiFlexGroupProps) => {
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||||
|
<div data-test-subj="watch-video-overlay" onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
EuiFlexItem: ({ children }: { children: React.ReactElement }) => <div>{children}</div>,
|
||||||
|
EuiIcon: () => <span data-test-subj="mock-play-icon" />,
|
||||||
|
useEuiTheme: () => ({ euiTheme: { colors: { fullShade: '#000', emptyShade: '#fff' } } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Video Component', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders overlay if step is not completed', () => {
|
||||||
|
const { getByTestId } = render(<Video />);
|
||||||
|
const overlay = getByTestId('watch-video-overlay');
|
||||||
|
expect(overlay).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders video after clicking the overlay', () => {
|
||||||
|
const { toggleTaskCompleteStatus } = useStepContext();
|
||||||
|
const { getByTestId, queryByTestId } = render(<Video />);
|
||||||
|
const overlay = getByTestId('watch-video-overlay');
|
||||||
|
fireEvent.click(overlay);
|
||||||
|
expect(toggleTaskCompleteStatus).toHaveBeenCalledWith({
|
||||||
|
stepId: OverviewSteps.getToKnowElasticSecurity,
|
||||||
|
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
|
||||||
|
sectionId: SectionId.quickStart,
|
||||||
|
undo: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframe = screen.getByTitle(WATCH_VIDEO_BUTTON_TITLE);
|
||||||
|
expect(iframe).toBeInTheDocument();
|
||||||
|
|
||||||
|
const overlayAfterClick = queryByTestId('watch-video-overlay');
|
||||||
|
expect(overlayAfterClick).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders video if step is completed', () => {
|
||||||
|
(useStepContext as jest.Mock).mockReturnValue({
|
||||||
|
expandedCardSteps: defaultExpandedCards,
|
||||||
|
finishedSteps: {
|
||||||
|
[QuickStartSectionCardsId.watchTheOverviewVideo]: new Set([
|
||||||
|
OverviewSteps.getToKnowElasticSecurity,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
onStepClicked: jest.fn(),
|
||||||
|
toggleTaskCompleteStatus: jest.fn(),
|
||||||
|
});
|
||||||
|
const { getByTitle, queryByTestId } = render(<Video />);
|
||||||
|
const iframe = getByTitle(WATCH_VIDEO_BUTTON_TITLE);
|
||||||
|
expect(iframe).toBeInTheDocument();
|
||||||
|
|
||||||
|
const overlay = queryByTestId('watch-video-overlay');
|
||||||
|
expect(overlay).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* 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, EuiIcon, useEuiTheme } from '@elastic/eui';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useStepContext } from '../../context/step_context';
|
||||||
|
import { WATCH_VIDEO_BUTTON_TITLE } from '../../translations';
|
||||||
|
import { OverviewSteps, QuickStartSectionCardsId, SectionId } from '../../types';
|
||||||
|
import { ContentWrapper } from './content_wrapper';
|
||||||
|
|
||||||
|
const VideoComponent: React.FC = () => {
|
||||||
|
const { toggleTaskCompleteStatus, finishedSteps } = useStepContext();
|
||||||
|
const ref = React.useRef<HTMLIFrameElement>(null);
|
||||||
|
const [isVideoPlaying, setIsVideoPlaying] = React.useState(false);
|
||||||
|
const { euiTheme } = useEuiTheme();
|
||||||
|
const cardId = QuickStartSectionCardsId.watchTheOverviewVideo;
|
||||||
|
const isFinishedStep = useMemo(
|
||||||
|
() => finishedSteps[cardId]?.has(OverviewSteps.getToKnowElasticSecurity),
|
||||||
|
[finishedSteps, cardId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onVideoClicked = useCallback(() => {
|
||||||
|
toggleTaskCompleteStatus({
|
||||||
|
stepId: OverviewSteps.getToKnowElasticSecurity,
|
||||||
|
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
|
||||||
|
sectionId: SectionId.quickStart,
|
||||||
|
undo: false,
|
||||||
|
});
|
||||||
|
setIsVideoPlaying(true);
|
||||||
|
}, [toggleTaskCompleteStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentWrapper>
|
||||||
|
<>
|
||||||
|
{!isVideoPlaying && !isFinishedStep && (
|
||||||
|
<EuiFlexGroup
|
||||||
|
css={css`
|
||||||
|
background-color: ${euiTheme.colors.fullShade};
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
`}
|
||||||
|
gutterSize="none"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
onClick={onVideoClicked}
|
||||||
|
>
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<EuiIcon type="playFilled" size="xxl" color={euiTheme.colors.emptyShade} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
)}
|
||||||
|
{(isVideoPlaying || isFinishedStep) && (
|
||||||
|
<iframe
|
||||||
|
ref={ref}
|
||||||
|
allowFullScreen
|
||||||
|
className="vidyard_iframe"
|
||||||
|
frameBorder="0"
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
scrolling="no"
|
||||||
|
allow={isVideoPlaying ? 'autoplay;' : undefined}
|
||||||
|
src={`//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html${
|
||||||
|
isVideoPlaying ? '?autoplay=1' : ''
|
||||||
|
}`}
|
||||||
|
title={WATCH_VIDEO_BUTTON_TITLE}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</ContentWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Video = React.memo(VideoComponent);
|
|
@ -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 React from 'react';
|
||||||
|
import viewAlerts from '../../images/view_alerts.png';
|
||||||
|
import { VIEW_ALERTS_TITLE } from '../../translations';
|
||||||
|
import { ContentWrapper } from './content_wrapper';
|
||||||
|
|
||||||
|
const ViewAlertsImageComponent = () => (
|
||||||
|
<ContentWrapper>
|
||||||
|
<img src={viewAlerts} alt={VIEW_ALERTS_TITLE} height="100%" width="100%" />
|
||||||
|
</ContentWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ViewAlertsImage = React.memo(ViewAlertsImageComponent);
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 analyzeDataUsingDashboards from '../../images/analyze_data_using_dashboards.png';
|
||||||
|
import { VIEW_DASHBOARDS_IMAGE_TITLE } from '../../translations';
|
||||||
|
import { ContentWrapper } from './content_wrapper';
|
||||||
|
|
||||||
|
const ViewDashboardImageComponent = () => (
|
||||||
|
<ContentWrapper>
|
||||||
|
<img
|
||||||
|
src={analyzeDataUsingDashboards}
|
||||||
|
alt={VIEW_DASHBOARDS_IMAGE_TITLE}
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
/>
|
||||||
|
</ContentWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ViewDashboardImage = React.memo(ViewDashboardImageComponent);
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 { autoCheckPrebuildRuleStepCompleted } from './helpers';
|
||||||
|
import { fetchRuleManagementFilters } from '../apis';
|
||||||
|
import type { HttpSetup } from '@kbn/core/public';
|
||||||
|
|
||||||
|
jest.mock('../apis');
|
||||||
|
|
||||||
|
describe('autoCheckPrebuildRuleStepCompleted', () => {
|
||||||
|
const mockHttp = {} as HttpSetup;
|
||||||
|
const mockAbortController = new AbortController();
|
||||||
|
|
||||||
|
it('should return true if there are enabled rules', async () => {
|
||||||
|
(fetchRuleManagementFilters as jest.Mock).mockResolvedValue({ total: 1 });
|
||||||
|
const result = await autoCheckPrebuildRuleStepCompleted({
|
||||||
|
abortSignal: { current: mockAbortController },
|
||||||
|
kibanaServicesHttp: mockHttp,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onError and return false on error', async () => {
|
||||||
|
const mockError = new Error('Test error');
|
||||||
|
(fetchRuleManagementFilters as jest.Mock).mockRejectedValue(mockError);
|
||||||
|
const mockOnError = jest.fn();
|
||||||
|
|
||||||
|
const result = await autoCheckPrebuildRuleStepCompleted({
|
||||||
|
abortSignal: { current: mockAbortController },
|
||||||
|
kibanaServicesHttp: mockHttp,
|
||||||
|
onError: mockOnError,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnError).toHaveBeenCalledWith(mockError);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call onError if the request is aborted', async () => {
|
||||||
|
(fetchRuleManagementFilters as jest.Mock).mockRejectedValue({ name: 'AbortError' });
|
||||||
|
const mockOnError = jest.fn();
|
||||||
|
|
||||||
|
mockAbortController.abort();
|
||||||
|
|
||||||
|
const result = await autoCheckPrebuildRuleStepCompleted({
|
||||||
|
abortSignal: { current: mockAbortController },
|
||||||
|
kibanaServicesHttp: mockHttp,
|
||||||
|
onError: mockOnError,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOnError).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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 { MutableRefObject } from 'react';
|
||||||
|
import type { HttpSetup } from '@kbn/core/public';
|
||||||
|
import { ENABLED_FIELD } from '@kbn/security-solution-plugin/common';
|
||||||
|
import { fetchRuleManagementFilters } from '../apis';
|
||||||
|
|
||||||
|
export const autoCheckPrebuildRuleStepCompleted = async ({
|
||||||
|
abortSignal,
|
||||||
|
kibanaServicesHttp,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
abortSignal: MutableRefObject<AbortController>;
|
||||||
|
kibanaServicesHttp: HttpSetup;
|
||||||
|
onError?: (e: Error) => void;
|
||||||
|
}) => {
|
||||||
|
// Check if there are any rules installed and enabled
|
||||||
|
try {
|
||||||
|
const data = await fetchRuleManagementFilters({
|
||||||
|
http: kibanaServicesHttp,
|
||||||
|
signal: abortSignal.current.signal,
|
||||||
|
query: {
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
sort_field: 'enabled',
|
||||||
|
sort_order: 'desc',
|
||||||
|
filter: `${ENABLED_FIELD}: true`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data?.total > 0;
|
||||||
|
} catch (e) {
|
||||||
|
if (!abortSignal.current.signal.aborted) {
|
||||||
|
onError?.(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const autoCheckAddIntegrationsStepCompleted = async ({
|
||||||
|
indicesExist,
|
||||||
|
}: {
|
||||||
|
indicesExist: boolean;
|
||||||
|
}) => Promise.resolve(indicesExist);
|
|
@ -77,18 +77,19 @@ const CardStepComponent: React.FC<{
|
||||||
const toggleStep = useCallback(
|
const toggleStep = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const newStatus = !isExpandedStep;
|
||||||
|
|
||||||
if (hasStepContent) {
|
if (hasStepContent) {
|
||||||
// Toggle step and sync the expanded card step to storage & reducer
|
// Toggle step and sync the expanded card step to storage & reducer
|
||||||
onStepClicked({ stepId, cardId, sectionId, isExpanded: !isExpandedStep });
|
onStepClicked({ stepId, cardId, sectionId, isExpanded: newStatus });
|
||||||
|
|
||||||
navigateTo({
|
navigateTo({
|
||||||
deepLinkId: SecurityPageName.landing,
|
deepLinkId: SecurityPageName.landing,
|
||||||
path: `#${stepId}`,
|
path: newStatus ? `#${stepId}` : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[hasStepContent, onStepClicked, stepId, cardId, sectionId, isExpandedStep, navigateTo]
|
[isExpandedStep, hasStepContent, onStepClicked, stepId, cardId, sectionId, navigateTo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -170,6 +171,4 @@ const CardStepComponent: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
CardStepComponent.displayName = 'CardStepComponent';
|
|
||||||
|
|
||||||
export const CardStep = React.memo(CardStepComponent);
|
export const CardStep = React.memo(CardStepComponent);
|
||||||
|
|
|
@ -84,7 +84,7 @@ const StepContentComponent = ({
|
||||||
css={rightPanelStyles}
|
css={rightPanelStyles}
|
||||||
>
|
>
|
||||||
{splitPanel && (
|
{splitPanel && (
|
||||||
<div className="right-content-panel" css={rightPanelContentStyles}>
|
<div className="right-panel-wrapper" css={rightPanelContentStyles}>
|
||||||
{splitPanel}
|
{splitPanel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,86 +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 { EuiFlexGroup, EuiFlexItem, EuiIcon, useEuiTheme } from '@elastic/eui';
|
|
||||||
import { css } from '@emotion/react';
|
|
||||||
import React, { useCallback, useMemo } from 'react';
|
|
||||||
import { useStepContext } from '../context/step_context';
|
|
||||||
import { WATCH_VIDEO_BUTTON_TITLE } from '../translations';
|
|
||||||
import { OverviewSteps, QuickStartSectionCardsId, SectionId } from '../types';
|
|
||||||
|
|
||||||
const VideoComponent: React.FC = () => {
|
|
||||||
const { toggleTaskCompleteStatus, finishedSteps } = useStepContext();
|
|
||||||
const ref = React.useRef<HTMLIFrameElement>(null);
|
|
||||||
const [isVideoPlaying, setIsVideoPlaying] = React.useState(false);
|
|
||||||
const { euiTheme } = useEuiTheme();
|
|
||||||
const cardId = QuickStartSectionCardsId.watchTheOverviewVideo;
|
|
||||||
const isFinishedStep = useMemo(
|
|
||||||
() => finishedSteps[cardId]?.has(OverviewSteps.getToKnowElasticSecurity),
|
|
||||||
[finishedSteps, cardId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onVideoClicked = useCallback(() => {
|
|
||||||
toggleTaskCompleteStatus({
|
|
||||||
stepId: OverviewSteps.getToKnowElasticSecurity,
|
|
||||||
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
|
|
||||||
sectionId: SectionId.quickStart,
|
|
||||||
undo: false,
|
|
||||||
});
|
|
||||||
setIsVideoPlaying(true);
|
|
||||||
}, [toggleTaskCompleteStatus]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
css={css`
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{!isVideoPlaying && !isFinishedStep && (
|
|
||||||
<EuiFlexGroup
|
|
||||||
css={css`
|
|
||||||
background-color: ${euiTheme.colors.fullShade};
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
`}
|
|
||||||
gutterSize="none"
|
|
||||||
justifyContent="center"
|
|
||||||
alignItems="center"
|
|
||||||
onClick={onVideoClicked}
|
|
||||||
>
|
|
||||||
<EuiFlexItem grow={false}>
|
|
||||||
<EuiIcon type="playFilled" size="xxl" color={euiTheme.colors.emptyShade} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
</EuiFlexGroup>
|
|
||||||
)}
|
|
||||||
{(isVideoPlaying || isFinishedStep) && (
|
|
||||||
<iframe
|
|
||||||
ref={ref}
|
|
||||||
allowFullScreen
|
|
||||||
className="vidyard_iframe"
|
|
||||||
frameBorder="0"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
sandbox="allow-scripts allow-same-origin"
|
|
||||||
scrolling="no"
|
|
||||||
allow={isVideoPlaying ? 'autoplay;' : undefined}
|
|
||||||
src={`//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html${
|
|
||||||
isVideoPlaying ? '?autoplay=1' : ''
|
|
||||||
}`}
|
|
||||||
title={WATCH_VIDEO_BUTTON_TITLE}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Video = React.memo(VideoComponent);
|
|
|
@ -9,19 +9,17 @@ import React from 'react';
|
||||||
import { defaultExpandedCards } from '../../storage';
|
import { defaultExpandedCards } from '../../storage';
|
||||||
import { CreateProjectSteps, QuickStartSectionCardsId } from '../../types';
|
import { CreateProjectSteps, QuickStartSectionCardsId } from '../../types';
|
||||||
|
|
||||||
|
export const mockOnStepClicked = jest.fn();
|
||||||
|
export const mockToggleTaskCompleteStatus = jest.fn();
|
||||||
export const StepContextProvider = ({ children }: { children: React.ReactElement }) => (
|
export const StepContextProvider = ({ children }: { children: React.ReactElement }) => (
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useStepContext = () => {
|
export const useStepContext = jest.fn(() => ({
|
||||||
return {
|
expandedCardSteps: defaultExpandedCards,
|
||||||
expandedCardSteps: defaultExpandedCards,
|
finishedSteps: {
|
||||||
finishedSteps: {
|
[QuickStartSectionCardsId.createFirstProject]: new Set([CreateProjectSteps.createFirstProject]),
|
||||||
[QuickStartSectionCardsId.createFirstProject]: new Set([
|
},
|
||||||
CreateProjectSteps.createFirstProject,
|
onStepClicked: mockOnStepClicked,
|
||||||
]),
|
toggleTaskCompleteStatus: mockToggleTaskCompleteStatus,
|
||||||
},
|
}));
|
||||||
onStepClicked: jest.fn(),
|
|
||||||
toggleTaskCompleteStatus: jest.fn(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import documentation from '../images/documentation.png';
|
||||||
|
import forum from '../images/forum.png';
|
||||||
|
import demo from '../images/demo.png';
|
||||||
|
import labs from '../images/labs.png';
|
||||||
|
import * as i18n from './translations';
|
||||||
|
|
||||||
|
const footer = [
|
||||||
|
{
|
||||||
|
icon: documentation,
|
||||||
|
key: 'documentation',
|
||||||
|
title: i18n.FOOTER_DOCUMENTATION_TITLE,
|
||||||
|
description: i18n.FOOTER_DOCUMENTATION_DESCRIPTION,
|
||||||
|
link: {
|
||||||
|
title: i18n.FOOTER_DOCUMENTATION_LINK_TITLE,
|
||||||
|
href: 'https://docs.elastic.co/integrations/elastic-security-intro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: forum,
|
||||||
|
key: 'forum',
|
||||||
|
title: i18n.FOOTER_FORUM_TITLE,
|
||||||
|
description: i18n.FOOTER_FORUM_DESCRIPTION,
|
||||||
|
link: {
|
||||||
|
title: i18n.FOOTER_FORUM_LINK_TITLE,
|
||||||
|
href: 'https://discuss.elastic.co/c/security/83',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: demo,
|
||||||
|
key: 'demo',
|
||||||
|
title: i18n.FOOTER_DEMO_TITLE,
|
||||||
|
description: i18n.FOOTER_DEMO_DESCRIPTION,
|
||||||
|
link: {
|
||||||
|
title: i18n.FOOTER_DEMO_LINK_TITLE,
|
||||||
|
href: 'https://www.elastic.co/demo-gallery?solutions=security&features=null',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: labs,
|
||||||
|
key: 'labs',
|
||||||
|
title: i18n.FOOTER_LABS_TITLE,
|
||||||
|
description: i18n.FOOTER_LABS_DESCRIPTION,
|
||||||
|
link: {
|
||||||
|
title: i18n.FOOTER_LABS_LINK_TITLE,
|
||||||
|
href: 'https://www.elastic.co/security-labs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getFooter = () => footer;
|
|
@ -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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useFooterStyles } from '../styles/footer.styles';
|
||||||
|
import { getFooter } from './footer';
|
||||||
|
|
||||||
|
const FooterComponent = () => {
|
||||||
|
const { wrapperStyle, titleStyle, descriptionStyle, linkStyle } = useFooterStyles();
|
||||||
|
const footer = useMemo(() => getFooter(), []);
|
||||||
|
return (
|
||||||
|
<EuiFlexGroup
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="spaceBetween"
|
||||||
|
gutterSize="none"
|
||||||
|
css={wrapperStyle}
|
||||||
|
>
|
||||||
|
{footer.map((item) => (
|
||||||
|
<EuiFlexItem key={`footer-${item.key}`}>
|
||||||
|
<img src={item.icon} alt={item.title} height="64" width="64" />
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
<p css={titleStyle}>{item.title}</p>
|
||||||
|
<p css={descriptionStyle}>{item.description}</p>
|
||||||
|
<EuiSpacer size="m" />
|
||||||
|
<EuiLink href={item.link.href} external={true} target="_blank" css={linkStyle}>
|
||||||
|
{item.link.title}
|
||||||
|
</EuiLink>
|
||||||
|
</EuiFlexItem>
|
||||||
|
))}
|
||||||
|
</EuiFlexGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Footer = React.memo(FooterComponent);
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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 FOOTER_DOCUMENTATION_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.documentation.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Browse documentation',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_DOCUMENTATION_DESCRIPTION = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.documentation.description',
|
||||||
|
{
|
||||||
|
defaultMessage: 'In-depth guides on all Elastic features',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_DOCUMENTATION_LINK_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.documentation.link.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Start reading',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_FORUM_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.forum.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Explore forum',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_FORUM_DESCRIPTION = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.forum.description',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Exchange thoughts about Elastic',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_FORUM_LINK_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.forum.link.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Discuss Forum',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_DEMO_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.demo.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'View demo project',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_DEMO_DESCRIPTION = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.demo.description',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Discover Elastic using sample data',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_DEMO_LINK_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.demo.link.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Explore demo',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_LABS_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.labs.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Elastic Security Labs',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_LABS_DESCRIPTION = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.labs.description',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Insights from security researchers',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FOOTER_LABS_LINK_TITLE = i18n.translate(
|
||||||
|
'xpack.securitySolutionServerless.getStarted.footer.labs.link.title',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Learn more',
|
||||||
|
}
|
||||||
|
);
|
|
@ -18,6 +18,8 @@ import { Progress } from './progress_bar';
|
||||||
import { StepContextProvider } from './context/step_context';
|
import { StepContextProvider } from './context/step_context';
|
||||||
import { CONTENT_WIDTH } from './helpers';
|
import { CONTENT_WIDTH } from './helpers';
|
||||||
import { WelcomeHeader } from './welcome_header';
|
import { WelcomeHeader } from './welcome_header';
|
||||||
|
import { Footer } from './footer';
|
||||||
|
import { useScrollToHash } from './hooks/use_scroll';
|
||||||
|
|
||||||
export interface GetStartedProps {
|
export interface GetStartedProps {
|
||||||
indicesExist?: boolean;
|
indicesExist?: boolean;
|
||||||
|
@ -42,6 +44,8 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes, i
|
||||||
(product) => product.product_line === ProductLine.security
|
(product) => product.product_line === ProductLine.security
|
||||||
)?.product_tier;
|
)?.product_tier;
|
||||||
|
|
||||||
|
useScrollToHash();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KibanaPageTemplate
|
<KibanaPageTemplate
|
||||||
restrictWidth={false}
|
restrictWidth={false}
|
||||||
|
@ -88,7 +92,7 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes, i
|
||||||
restrictWidth={CONTENT_WIDTH}
|
restrictWidth={CONTENT_WIDTH}
|
||||||
paddingSize="none"
|
paddingSize="none"
|
||||||
css={css`
|
css={css`
|
||||||
padding: 0 ${euiTheme.size.xxl} ${euiTheme.base * 3.5}px;
|
padding: 0 ${euiTheme.size.xxl} ${euiTheme.size.xxxl};
|
||||||
background-color: ${euiTheme.colors.lightestShade};
|
background-color: ${euiTheme.colors.lightestShade};
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
@ -102,6 +106,9 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes, i
|
||||||
<TogglePanel activeProducts={activeProducts} activeSections={activeSections} />
|
<TogglePanel activeProducts={activeProducts} activeSections={activeSections} />
|
||||||
</StepContextProvider>
|
</StepContextProvider>
|
||||||
</KibanaPageTemplate.Section>
|
</KibanaPageTemplate.Section>
|
||||||
|
<KibanaPageTemplate.Section grow={true} restrictWidth={CONTENT_WIDTH} paddingSize="none">
|
||||||
|
<Footer />
|
||||||
|
</KibanaPageTemplate.Section>
|
||||||
</KibanaPageTemplate>
|
</KibanaPageTemplate>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,16 @@
|
||||||
|
|
||||||
import type { ProductLine } from '../../common/product';
|
import type { ProductLine } from '../../common/product';
|
||||||
import { getSections } from './sections';
|
import { getSections } from './sections';
|
||||||
import type { ActiveCard, ActiveSections, Card, CardId, SectionId, Step, StepId } from './types';
|
import type {
|
||||||
|
ActiveCard,
|
||||||
|
ActiveSections,
|
||||||
|
Card,
|
||||||
|
CardId,
|
||||||
|
Section,
|
||||||
|
SectionId,
|
||||||
|
Step,
|
||||||
|
StepId,
|
||||||
|
} from './types';
|
||||||
import { CreateProjectSteps, QuickStartSectionCardsId } from './types';
|
import { CreateProjectSteps, QuickStartSectionCardsId } from './types';
|
||||||
|
|
||||||
export const CONTENT_WIDTH = 1150;
|
export const CONTENT_WIDTH = 1150;
|
||||||
|
@ -53,14 +62,30 @@ const getfinishedActiveSteps = (
|
||||||
return new Set(finishedActiveSteps);
|
return new Set(finishedActiveSteps);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findCardByStepId = (
|
export const findCardSectionByStepId = (
|
||||||
stepId: string
|
stepId: string
|
||||||
): { matchedCard: Card | null; matchedStep: Step | null } => {
|
): { matchedCard: Card | null; matchedStep: Step | null; matchedSection: Section | null } => {
|
||||||
const cards = getSections().flatMap((s) => s.cards);
|
const cards = getSections().flatMap((s) => s.cards);
|
||||||
const matchedStep: Step | null = null;
|
let matchedStep: Step | null = null;
|
||||||
const matchedCard = cards.find((c) => !!c.steps?.find((step) => stepId === step.id)) ?? null;
|
|
||||||
|
|
||||||
return { matchedCard, matchedStep };
|
const matchedCard =
|
||||||
|
cards.find(
|
||||||
|
(c) =>
|
||||||
|
!!c.steps?.find((step) => {
|
||||||
|
if (stepId === step.id) {
|
||||||
|
matchedStep = step;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
const matchedSection = matchedCard
|
||||||
|
? getSections().find((s) => s.cards?.includes(matchedCard)) ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { matchedCard, matchedStep, matchedSection };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCard = ({ cardId, sectionId }: { cardId: CardId; sectionId: SectionId }) => {
|
export const getCard = ({ cardId, sectionId }: { cardId: CardId; sectionId: SectionId }) => {
|
||||||
|
|
|
@ -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 { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { useCheckStepCompleted } from './use_check_step_completed';
|
||||||
|
import {
|
||||||
|
EnablePrebuiltRulesSteps,
|
||||||
|
GetStartedWithAlertsCardsId,
|
||||||
|
OverviewSteps,
|
||||||
|
QuickStartSectionCardsId,
|
||||||
|
SectionId,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
jest.mock('../../common/services', () => ({
|
||||||
|
useKibana: () => ({
|
||||||
|
services: {
|
||||||
|
http: {},
|
||||||
|
notifications: {
|
||||||
|
toasts: {
|
||||||
|
addError: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useCheckStepCompleted', () => {
|
||||||
|
it('does nothing when autoCheckIfStepCompleted is not provided', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useCheckStepCompleted({
|
||||||
|
indicesExist: true,
|
||||||
|
stepId: OverviewSteps.getToKnowElasticSecurity,
|
||||||
|
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
|
||||||
|
sectionId: SectionId.quickStart,
|
||||||
|
toggleTaskCompleteStatus: jest.fn(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(result.current).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls autoCheckIfStepCompleted and toggleTaskCompleteStatus', async () => {
|
||||||
|
const mockAutoCheck = jest.fn().mockResolvedValue(true);
|
||||||
|
const mockToggleTask = jest.fn();
|
||||||
|
|
||||||
|
const { waitFor } = renderHook(() =>
|
||||||
|
useCheckStepCompleted({
|
||||||
|
autoCheckIfStepCompleted: mockAutoCheck,
|
||||||
|
cardId: GetStartedWithAlertsCardsId.enablePrebuiltRules,
|
||||||
|
indicesExist: true,
|
||||||
|
sectionId: SectionId.getStartedWithAlerts,
|
||||||
|
stepId: EnablePrebuiltRulesSteps.enablePrebuiltRules,
|
||||||
|
toggleTaskCompleteStatus: mockToggleTask,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockAutoCheck).toHaveBeenCalled();
|
||||||
|
expect(mockToggleTask).toHaveBeenCalledWith({
|
||||||
|
sectionId: SectionId.getStartedWithAlerts,
|
||||||
|
stepId: EnablePrebuiltRulesSteps.enablePrebuiltRules,
|
||||||
|
cardId: GetStartedWithAlertsCardsId.enablePrebuiltRules,
|
||||||
|
undo: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 { useEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { HEIGHT_ANIMATION_DURATION } from '../styles/card_step.styles';
|
||||||
|
|
||||||
|
const HEADER_OFFSET = 40;
|
||||||
|
|
||||||
|
export const useScrollToHash = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const [documentReadyState, setReadyState] = useState(document.readyState);
|
||||||
|
useEffect(() => {
|
||||||
|
const readyStateListener = () => setReadyState(document.readyState);
|
||||||
|
document.addEventListener('readystatechange', readyStateListener);
|
||||||
|
return () => document.removeEventListener('readystatechange', readyStateListener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (documentReadyState !== 'complete') return; // Wait for page to finish loading before scrolling
|
||||||
|
|
||||||
|
const hash = location.hash.split('?')[0].replace('#', '');
|
||||||
|
const element = hash ? document.getElementById(hash) : null;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
// Wait for transition to complete before scrolling
|
||||||
|
setTimeout(() => {
|
||||||
|
element.focus({ preventScroll: true }); // Scrolling already handled below
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: element.offsetTop - HEADER_OFFSET,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, HEIGHT_ANIMATION_DURATION);
|
||||||
|
}
|
||||||
|
}, [location.hash, documentReadyState]);
|
||||||
|
};
|
|
@ -11,8 +11,6 @@ import { useSetUpSections } from './use_setup_sections';
|
||||||
import type { ActiveSections, CardId, ExpandedCardSteps, StepId } from '../types';
|
import type { ActiveSections, CardId, ExpandedCardSteps, StepId } from '../types';
|
||||||
import { CreateProjectSteps, QuickStartSectionCardsId, SectionId } from '../types';
|
import { CreateProjectSteps, QuickStartSectionCardsId, SectionId } from '../types';
|
||||||
|
|
||||||
import { ProductLine } from '../../../common/product';
|
|
||||||
|
|
||||||
const mockEuiTheme: EuiThemeComputed = {
|
const mockEuiTheme: EuiThemeComputed = {
|
||||||
size: {
|
size: {
|
||||||
l: '16px',
|
l: '16px',
|
||||||
|
@ -44,7 +42,6 @@ describe('useSetUpSections', () => {
|
||||||
} as ActiveSections;
|
} as ActiveSections;
|
||||||
|
|
||||||
const sections = result.current.setUpSections({
|
const sections = result.current.setUpSections({
|
||||||
activeProducts: new Set([ProductLine.security]),
|
|
||||||
activeSections,
|
activeSections,
|
||||||
expandedCardSteps: {} as ExpandedCardSteps,
|
expandedCardSteps: {} as ExpandedCardSteps,
|
||||||
onStepClicked,
|
onStepClicked,
|
||||||
|
@ -62,7 +59,6 @@ describe('useSetUpSections', () => {
|
||||||
|
|
||||||
const sections = result.current.setUpSections({
|
const sections = result.current.setUpSections({
|
||||||
activeSections,
|
activeSections,
|
||||||
activeProducts: new Set([ProductLine.security]),
|
|
||||||
expandedCardSteps: {} as ExpandedCardSteps,
|
expandedCardSteps: {} as ExpandedCardSteps,
|
||||||
onStepClicked,
|
onStepClicked,
|
||||||
toggleTaskCompleteStatus,
|
toggleTaskCompleteStatus,
|
||||||
|
|
|
@ -21,12 +21,10 @@ import type {
|
||||||
|
|
||||||
import { CardItem } from '../card_item';
|
import { CardItem } from '../card_item';
|
||||||
import { getSections } from '../sections';
|
import { getSections } from '../sections';
|
||||||
import type { ProductLine } from '../../../common/product';
|
|
||||||
|
|
||||||
export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => {
|
export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => {
|
||||||
const setUpCards = useCallback(
|
const setUpCards = useCallback(
|
||||||
({
|
({
|
||||||
activeProducts,
|
|
||||||
activeSections,
|
activeSections,
|
||||||
expandedCardSteps,
|
expandedCardSteps,
|
||||||
finishedSteps,
|
finishedSteps,
|
||||||
|
@ -34,7 +32,6 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
|
||||||
onStepClicked,
|
onStepClicked,
|
||||||
sectionId,
|
sectionId,
|
||||||
}: {
|
}: {
|
||||||
activeProducts: Set<ProductLine>;
|
|
||||||
activeSections: ActiveSections | null;
|
activeSections: ActiveSections | null;
|
||||||
expandedCardSteps: ExpandedCardSteps;
|
expandedCardSteps: ExpandedCardSteps;
|
||||||
finishedSteps: Record<CardId, Set<StepId>>;
|
finishedSteps: Record<CardId, Set<StepId>>;
|
||||||
|
@ -65,14 +62,12 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
|
||||||
|
|
||||||
const setUpSections = useCallback(
|
const setUpSections = useCallback(
|
||||||
({
|
({
|
||||||
activeProducts,
|
|
||||||
activeSections,
|
activeSections,
|
||||||
expandedCardSteps,
|
expandedCardSteps,
|
||||||
finishedSteps,
|
finishedSteps,
|
||||||
toggleTaskCompleteStatus,
|
toggleTaskCompleteStatus,
|
||||||
onStepClicked,
|
onStepClicked,
|
||||||
}: {
|
}: {
|
||||||
activeProducts: Set<ProductLine>;
|
|
||||||
activeSections: ActiveSections | null;
|
activeSections: ActiveSections | null;
|
||||||
expandedCardSteps: ExpandedCardSteps;
|
expandedCardSteps: ExpandedCardSteps;
|
||||||
finishedSteps: Record<CardId, Set<StepId>>;
|
finishedSteps: Record<CardId, Set<StepId>>;
|
||||||
|
@ -81,7 +76,6 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
|
||||||
}) =>
|
}) =>
|
||||||
getSections().reduce<React.ReactNode[]>((acc, currentSection) => {
|
getSections().reduce<React.ReactNode[]>((acc, currentSection) => {
|
||||||
const cardNodes = setUpCards({
|
const cardNodes = setUpCards({
|
||||||
activeProducts,
|
|
||||||
activeSections,
|
activeSections,
|
||||||
expandedCardSteps,
|
expandedCardSteps,
|
||||||
finishedSteps,
|
finishedSteps,
|
||||||
|
@ -117,10 +111,10 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
|
||||||
</span>
|
</span>
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
gutterSize="m"
|
gutterSize="none"
|
||||||
direction="column"
|
direction="column"
|
||||||
css={css`
|
css={css`
|
||||||
${euiTheme.size.base}
|
gap: ${euiTheme.size.base};
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{cardNodes}
|
{cardNodes}
|
||||||
|
|
|
@ -27,10 +27,10 @@ import type {
|
||||||
Switch,
|
Switch,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { GetStartedPageActions } from '../types';
|
import { GetStartedPageActions } from '../types';
|
||||||
import { findCardByStepId } from '../helpers';
|
import { findCardSectionByStepId } from '../helpers';
|
||||||
|
|
||||||
const syncExpandedCardStepsToStorageFromURL = (maybeStepId: string) => {
|
const syncExpandedCardStepsToStorageFromURL = (maybeStepId: string) => {
|
||||||
const { matchedCard, matchedStep } = findCardByStepId(maybeStepId);
|
const { matchedCard, matchedStep } = findCardSectionByStepId(maybeStepId);
|
||||||
const hasStepContent = matchedStep && matchedStep.description;
|
const hasStepContent = matchedStep && matchedStep.description;
|
||||||
|
|
||||||
if (matchedCard && matchedStep && hasStepContent) {
|
if (matchedCard && matchedStep && hasStepContent) {
|
||||||
|
@ -54,7 +54,9 @@ const syncExpandedCardStepsFromStorageToURL = (
|
||||||
);
|
);
|
||||||
|
|
||||||
if (expandedCardStep?.expandedSteps[0]) {
|
if (expandedCardStep?.expandedSteps[0]) {
|
||||||
const { matchedCard, matchedStep } = findCardByStepId(expandedCardStep?.expandedSteps[0]);
|
const { matchedCard, matchedStep } = findCardSectionByStepId(
|
||||||
|
expandedCardStep?.expandedSteps[0]
|
||||||
|
);
|
||||||
|
|
||||||
callback?.({ matchedCard, matchedStep });
|
callback?.({ matchedCard, matchedStep });
|
||||||
}
|
}
|
||||||
|
@ -114,28 +116,6 @@ export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProduct
|
||||||
return getAllExpandedCardStepsFromStorage();
|
return getAllExpandedCardStepsFromStorage();
|
||||||
}, [getAllExpandedCardStepsFromStorage, stepIdFromHash]);
|
}, [getAllExpandedCardStepsFromStorage, stepIdFromHash]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
syncExpandedCardStepsFromStorageToURL(
|
|
||||||
expandedCardsInitialStates,
|
|
||||||
({ matchedStep }: { matchedStep: Step | null }) => {
|
|
||||||
if (!matchedStep) return;
|
|
||||||
navigateTo({
|
|
||||||
deepLinkId: SecurityPageName.landing,
|
|
||||||
path: `#${matchedStep.id}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [expandedCardsInitialStates, getAllExpandedCardStepsFromStorage, navigateTo]);
|
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
|
||||||
activeProducts: activeProductsInitialStates,
|
|
||||||
activeSections: activeSectionsInitialStates,
|
|
||||||
expandedCardSteps: expandedCardsInitialStates,
|
|
||||||
finishedSteps: finishedStepsInitialStates,
|
|
||||||
totalActiveSteps: totalActiveStepsInitialStates,
|
|
||||||
totalStepsLeft: totalStepsLeftInitialStates,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onStepClicked: OnStepClicked = useCallback(
|
const onStepClicked: OnStepClicked = useCallback(
|
||||||
({ stepId, cardId, isExpanded }) => {
|
({ stepId, cardId, isExpanded }) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -157,6 +137,15 @@ export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProduct
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
activeProducts: activeProductsInitialStates,
|
||||||
|
activeSections: activeSectionsInitialStates,
|
||||||
|
expandedCardSteps: expandedCardsInitialStates,
|
||||||
|
finishedSteps: finishedStepsInitialStates,
|
||||||
|
totalActiveSteps: totalActiveStepsInitialStates,
|
||||||
|
totalStepsLeft: totalStepsLeftInitialStates,
|
||||||
|
});
|
||||||
|
|
||||||
const toggleTaskCompleteStatus: ToggleTaskCompleteStatus = useCallback(
|
const toggleTaskCompleteStatus: ToggleTaskCompleteStatus = useCallback(
|
||||||
({ stepId, cardId, sectionId, undo }) => {
|
({ stepId, cardId, sectionId, undo }) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -182,5 +171,64 @@ export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProduct
|
||||||
[toggleActiveProductsInStorage]
|
[toggleActiveProductsInStorage]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/** Handle landing on the page without hash
|
||||||
|
** e.g.: https://localhost:5601/app/security/get_started
|
||||||
|
** If there is no expanded card step in storage, do nothing.
|
||||||
|
** If there is expanded card step in storage, sync it to the url.
|
||||||
|
**/
|
||||||
|
if (!stepIdFromHash) {
|
||||||
|
// If all steps are collapsed, do nothing
|
||||||
|
if (Object.values(state.expandedCardSteps).every((c) => !c.isExpanded)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncExpandedCardStepsFromStorageToURL(
|
||||||
|
expandedCardsInitialStates,
|
||||||
|
({ matchedStep }: { matchedStep: Step | null }) => {
|
||||||
|
if (!matchedStep) return;
|
||||||
|
navigateTo({
|
||||||
|
deepLinkId: SecurityPageName.landing,
|
||||||
|
path: `#${matchedStep.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
expandedCardsInitialStates,
|
||||||
|
getAllExpandedCardStepsFromStorage,
|
||||||
|
navigateTo,
|
||||||
|
state.expandedCardSteps,
|
||||||
|
stepIdFromHash,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/** Handle hash change and expand the target step.
|
||||||
|
** e.g.: https://localhost:5601/app/security/get_started#create_your_first_project
|
||||||
|
**/
|
||||||
|
if (stepIdFromHash) {
|
||||||
|
const { matchedCard, matchedStep, matchedSection } = findCardSectionByStepId(stepIdFromHash);
|
||||||
|
const hasStepContent = matchedStep && matchedStep.description;
|
||||||
|
if (hasStepContent && matchedCard && matchedStep && matchedSection) {
|
||||||
|
// If the step is already expanded, do nothing
|
||||||
|
if (state.expandedCardSteps[matchedCard.id]?.expandedSteps.includes(matchedStep.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Toggle step and sync the expanded card step to storage & reducer
|
||||||
|
onStepClicked({
|
||||||
|
stepId: matchedStep.id,
|
||||||
|
cardId: matchedCard.id,
|
||||||
|
sectionId: matchedSection.id,
|
||||||
|
isExpanded: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateTo({
|
||||||
|
deepLinkId: SecurityPageName.landing,
|
||||||
|
path: `#${matchedStep.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [navigateTo, onStepClicked, state.expandedCardSteps, stepIdFromHash]);
|
||||||
|
|
||||||
return { state, onStepClicked, toggleTaskCompleteStatus, onProductSwitchChanged };
|
return { state, onStepClicked, toggleTaskCompleteStatus, onProductSwitchChanged };
|
||||||
};
|
};
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -5,12 +5,12 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, useEuiTheme } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from '@emotion/react';
|
|
||||||
import type { ProductTier } from '../../common/product';
|
import type { ProductTier } from '../../common/product';
|
||||||
|
|
||||||
import { PROGRESS_TRACKER_LABEL } from './translations';
|
import { PROGRESS_TRACKER_LABEL } from './translations';
|
||||||
|
import { useProgressBarStyles } from './styles/progress_bar.style';
|
||||||
|
|
||||||
const ProgressComponent: React.FC<{
|
const ProgressComponent: React.FC<{
|
||||||
productTier: ProductTier | undefined;
|
productTier: ProductTier | undefined;
|
||||||
|
@ -19,7 +19,7 @@ const ProgressComponent: React.FC<{
|
||||||
}> = ({ productTier, totalActiveSteps, totalStepsLeft }) => {
|
}> = ({ productTier, totalActiveSteps, totalStepsLeft }) => {
|
||||||
const stepsDone =
|
const stepsDone =
|
||||||
totalActiveSteps != null && totalStepsLeft != null ? totalActiveSteps - totalStepsLeft : null;
|
totalActiveSteps != null && totalStepsLeft != null ? totalActiveSteps - totalStepsLeft : null;
|
||||||
const { euiTheme } = useEuiTheme();
|
const { textStyle } = useProgressBarStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||||
|
@ -31,19 +31,11 @@ const ProgressComponent: React.FC<{
|
||||||
size="m"
|
size="m"
|
||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
<span
|
<span css={textStyle}>{PROGRESS_TRACKER_LABEL}</span>
|
||||||
css={css`
|
|
||||||
font-size: 10.5px;
|
|
||||||
font-weight: ${euiTheme.font.weight.bold}}};
|
|
||||||
text-transform: uppercase;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{PROGRESS_TRACKER_LABEL}
|
|
||||||
</span>
|
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
valueText={<>{`${stepsDone}/${totalActiveSteps}`}</>}
|
valueText={<span css={textStyle}>{`${stepsDone}/${totalActiveSteps}`}</span>}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,11 +4,8 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import type { MutableRefObject } from 'react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { HttpSetup } from '@kbn/core/public';
|
|
||||||
import { ENABLED_FIELD } from '@kbn/security-solution-plugin/common';
|
|
||||||
import type { Step, StepId } from './types';
|
import type { Step, StepId } from './types';
|
||||||
import {
|
import {
|
||||||
SectionId,
|
SectionId,
|
||||||
|
@ -27,18 +24,21 @@ import * as i18n from './translations';
|
||||||
|
|
||||||
import { AddIntegrationButton } from './step_links/add_integration_button';
|
import { AddIntegrationButton } from './step_links/add_integration_button';
|
||||||
import { AlertsButton } from './step_links/alerts_link';
|
import { AlertsButton } from './step_links/alerts_link';
|
||||||
import connectToDataSources from './images/connect_to_existing_sources.png';
|
|
||||||
import enablePrebuiltRules from './images/enable_prebuilt_rules.png';
|
|
||||||
import createProjects from './images/create_projects.png';
|
|
||||||
import viewAlerts from './images/view_alerts.png';
|
|
||||||
import analyzeDataUsingDashboards from './images/analyze_data_using_dashboards.png';
|
|
||||||
import { AddElasticRulesButton } from './step_links/add_elastic_rules_button';
|
import { AddElasticRulesButton } from './step_links/add_elastic_rules_button';
|
||||||
import { DashboardButton } from './step_links/dashboard_button';
|
import { DashboardButton } from './step_links/dashboard_button';
|
||||||
import overviewVideo from './images/overview_video.svg';
|
import overviewVideo from './images/overview_video.svg';
|
||||||
import { Video } from './card_step/video';
|
import { Video } from './card_step/content/video';
|
||||||
import { fetchRuleManagementFilters } from './apis';
|
import { OverviewVideoDescription } from './card_step/content/overview_video_description';
|
||||||
import { OverviewVideoDescription } from './card_step/overview_video_description';
|
|
||||||
import { ManageProjectsButton } from './step_links/manage_projects_button';
|
import { ManageProjectsButton } from './step_links/manage_projects_button';
|
||||||
|
import { EnableRuleImage } from './card_step/content/enable_rule_image';
|
||||||
|
import {
|
||||||
|
autoCheckAddIntegrationsStepCompleted,
|
||||||
|
autoCheckPrebuildRuleStepCompleted,
|
||||||
|
} from './card_step/helpers';
|
||||||
|
import { ViewDashboardImage } from './card_step/content/view_dashboard_image';
|
||||||
|
import { AddIntegrationsImage } from './card_step/content/add_integration_image';
|
||||||
|
import { CreateProjectImage } from './card_step/content/create_project_step_image';
|
||||||
|
import { ViewAlertsImage } from './card_step/content/view_alerts_image';
|
||||||
|
|
||||||
export const createProjectSteps = [
|
export const createProjectSteps = [
|
||||||
{
|
{
|
||||||
|
@ -46,9 +46,7 @@ export const createProjectSteps = [
|
||||||
title: i18n.CREATE_PROJECT_TITLE,
|
title: i18n.CREATE_PROJECT_TITLE,
|
||||||
icon: { type: 'addDataApp', size: 'xl' as const },
|
icon: { type: 'addDataApp', size: 'xl' as const },
|
||||||
description: [i18n.CREATE_PROJECT_DESCRIPTION, <ManageProjectsButton />],
|
description: [i18n.CREATE_PROJECT_DESCRIPTION, <ManageProjectsButton />],
|
||||||
splitPanel: (
|
splitPanel: <CreateProjectImage />,
|
||||||
<img src={createProjects} alt={i18n.CREATE_PROJECT_TITLE} height="100%" width="100%" />
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export const overviewVideoSteps = [
|
export const overviewVideoSteps = [
|
||||||
|
@ -67,16 +65,8 @@ export const addIntegrationsSteps: Array<Step<AddIntegrationsSteps.connectToData
|
||||||
id: AddIntegrationsSteps.connectToDataSources,
|
id: AddIntegrationsSteps.connectToDataSources,
|
||||||
title: i18n.ADD_INTEGRATIONS_TITLE,
|
title: i18n.ADD_INTEGRATIONS_TITLE,
|
||||||
description: [i18n.ADD_INTEGRATIONS_DESCRIPTION, <AddIntegrationButton />],
|
description: [i18n.ADD_INTEGRATIONS_DESCRIPTION, <AddIntegrationButton />],
|
||||||
splitPanel: (
|
splitPanel: <AddIntegrationsImage />,
|
||||||
<img
|
autoCheckIfStepCompleted: autoCheckAddIntegrationsStepCompleted,
|
||||||
src={connectToDataSources}
|
|
||||||
alt={i18n.ADD_INTEGRATIONS_IMAGE_TITLE}
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
autoCheckIfStepCompleted: async ({ indicesExist }: { indicesExist: boolean }) =>
|
|
||||||
Promise.resolve(indicesExist),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -86,14 +76,7 @@ export const viewDashboardSteps = [
|
||||||
icon: { type: 'dashboardApp', size: 'xl' as const },
|
icon: { type: 'dashboardApp', size: 'xl' as const },
|
||||||
title: i18n.VIEW_DASHBOARDS,
|
title: i18n.VIEW_DASHBOARDS,
|
||||||
description: [i18n.VIEW_DASHBOARDS_DESCRIPTION, <DashboardButton />],
|
description: [i18n.VIEW_DASHBOARDS_DESCRIPTION, <DashboardButton />],
|
||||||
splitPanel: (
|
splitPanel: <ViewDashboardImage />,
|
||||||
<img
|
|
||||||
src={analyzeDataUsingDashboards}
|
|
||||||
alt={i18n.VIEW_DASHBOARDS_IMAGE_TITLE}
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -103,40 +86,8 @@ export const enablePrebuildRuleSteps: Array<Step<EnablePrebuiltRulesSteps.enable
|
||||||
icon: { type: 'advancedSettingsApp', size: 'xl' as const },
|
icon: { type: 'advancedSettingsApp', size: 'xl' as const },
|
||||||
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
|
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
|
||||||
description: [i18n.ENABLE_RULES_DESCRIPTION, <AddElasticRulesButton />],
|
description: [i18n.ENABLE_RULES_DESCRIPTION, <AddElasticRulesButton />],
|
||||||
splitPanel: (
|
splitPanel: <EnableRuleImage />,
|
||||||
<img src={enablePrebuiltRules} alt={i18n.ENABLE_RULES} height="100%" width="100%" />
|
autoCheckIfStepCompleted: autoCheckPrebuildRuleStepCompleted,
|
||||||
),
|
|
||||||
autoCheckIfStepCompleted: async ({
|
|
||||||
abortSignal,
|
|
||||||
kibanaServicesHttp,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
abortSignal: MutableRefObject<AbortController>;
|
|
||||||
kibanaServicesHttp: HttpSetup;
|
|
||||||
onError?: (e: Error) => void;
|
|
||||||
}) => {
|
|
||||||
// Check if there are any rules installed and enabled
|
|
||||||
try {
|
|
||||||
const data = await fetchRuleManagementFilters({
|
|
||||||
http: kibanaServicesHttp,
|
|
||||||
signal: abortSignal.current.signal,
|
|
||||||
query: {
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
sort_field: 'enabled',
|
|
||||||
sort_order: 'desc',
|
|
||||||
filter: `${ENABLED_FIELD}: true`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return data?.total > 0;
|
|
||||||
} catch (e) {
|
|
||||||
if (!abortSignal.current.signal.aborted) {
|
|
||||||
onError?.(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -146,7 +97,7 @@ export const viewAlertSteps = [
|
||||||
title: i18n.VIEW_ALERTS_TITLE,
|
title: i18n.VIEW_ALERTS_TITLE,
|
||||||
id: ViewAlertsSteps.viewAlerts,
|
id: ViewAlertsSteps.viewAlerts,
|
||||||
description: [i18n.VIEW_ALERTS_DESCRIPTION, <AlertsButton />],
|
description: [i18n.VIEW_ALERTS_DESCRIPTION, <AlertsButton />],
|
||||||
splitPanel: <img src={viewAlerts} alt={i18n.VIEW_ALERTS_TITLE} height="100%" width="100%" />,
|
splitPanel: <ViewAlertsImage />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -6,37 +6,27 @@
|
||||||
*/
|
*/
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { FormattedMessage } from '@kbn/i18n-react';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import { EuiCallOut, EuiIcon, useEuiTheme } from '@elastic/eui';
|
import { EuiCallOut, EuiIcon, EuiLink, useEuiTheme } from '@elastic/eui';
|
||||||
import { LinkAnchor } from '@kbn/security-solution-navigation/links';
|
|
||||||
|
|
||||||
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||||
import { useNavigateTo } from '@kbn/security-solution-navigation';
|
import { useNavigateTo } from '@kbn/security-solution-navigation';
|
||||||
|
|
||||||
import { useAddIntegrationsCalloutStyles } from '../styles/add_integrations_callout.styles';
|
import { useAddIntegrationsCalloutStyles } from '../styles/add_integrations_callout.styles';
|
||||||
import { ADD_INTEGRATIONS_STEP } from './translations';
|
import { ADD_INTEGRATIONS_STEP } from './translations';
|
||||||
import { AddAndValidateYourDataCardsId, AddIntegrationsSteps, SectionId } from '../types';
|
import { AddIntegrationsSteps } from '../types';
|
||||||
import { useStepContext } from '../context/step_context';
|
|
||||||
|
|
||||||
const AddIntegrationsCalloutComponent = ({ stepName }: { stepName?: string }) => {
|
const AddIntegrationsCalloutComponent = ({ stepName }: { stepName?: string }) => {
|
||||||
const { calloutWrapperStyles, calloutTitleStyles, calloutAnchorStyles } =
|
const { calloutWrapperStyles, calloutTitleStyles, calloutAnchorStyles } =
|
||||||
useAddIntegrationsCalloutStyles();
|
useAddIntegrationsCalloutStyles();
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
const { navigateTo } = useNavigateTo();
|
const { navigateTo } = useNavigateTo();
|
||||||
const { onStepClicked } = useStepContext();
|
|
||||||
|
|
||||||
const toggleStep = useCallback(() => {
|
const toggleStep = useCallback(() => {
|
||||||
onStepClicked({
|
|
||||||
stepId: AddIntegrationsSteps.connectToDataSources,
|
|
||||||
cardId: AddAndValidateYourDataCardsId.addIntegrations,
|
|
||||||
sectionId: SectionId.addAndValidateYourData,
|
|
||||||
isExpanded: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
navigateTo({
|
navigateTo({
|
||||||
deepLinkId: SecurityPageName.landing,
|
deepLinkId: SecurityPageName.landing,
|
||||||
path: `#${AddIntegrationsSteps.connectToDataSources}`,
|
path: `#${AddIntegrationsSteps.connectToDataSources}`,
|
||||||
});
|
});
|
||||||
}, [navigateTo, onStepClicked]);
|
}, [navigateTo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiCallOut
|
<EuiCallOut
|
||||||
|
@ -55,14 +45,10 @@ const AddIntegrationsCalloutComponent = ({ stepName }: { stepName?: string }) =>
|
||||||
defaultMessage="To {stepName} add integrations first {addIntegration}"
|
defaultMessage="To {stepName} add integrations first {addIntegration}"
|
||||||
values={{
|
values={{
|
||||||
addIntegration: (
|
addIntegration: (
|
||||||
<LinkAnchor
|
<EuiLink onClick={toggleStep} css={calloutAnchorStyles}>
|
||||||
id={SecurityPageName.landing}
|
|
||||||
onClick={toggleStep}
|
|
||||||
css={calloutAnchorStyles}
|
|
||||||
>
|
|
||||||
{ADD_INTEGRATIONS_STEP}
|
{ADD_INTEGRATIONS_STEP}
|
||||||
<EuiIcon type="arrowRight" size="s" css={calloutAnchorStyles} />
|
<EuiIcon type="arrowRight" size="s" css={calloutAnchorStyles} />
|
||||||
</LinkAnchor>
|
</EuiLink>
|
||||||
),
|
),
|
||||||
stepName: stepName ?? (
|
stepName: stepName ?? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEuiBackgroundColor, useEuiShadow, useEuiTheme } from '@elastic/eui';
|
import { useEuiShadow, useEuiTheme } from '@elastic/eui';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
export const SHADOW_ANIMATION_DURATION = 350;
|
export const SHADOW_ANIMATION_DURATION = 350;
|
||||||
|
@ -13,12 +13,10 @@ export const SHADOW_ANIMATION_DURATION = 350;
|
||||||
export const useCardItemStyles = () => {
|
export const useCardItemStyles = () => {
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
const shadow = useEuiShadow('l');
|
const shadow = useEuiShadow('l');
|
||||||
const iconHoveredBackgroundColor = useEuiBackgroundColor('success');
|
|
||||||
|
|
||||||
return css`
|
return css`
|
||||||
&.card-item {
|
&.card-item {
|
||||||
padding: ${euiTheme.size.base};
|
padding: ${euiTheme.size.base};
|
||||||
margin-bottom: ${euiTheme.size.xs};
|
|
||||||
border-radius: ${euiTheme.size.s};
|
border-radius: ${euiTheme.size.s};
|
||||||
border: 1px solid ${euiTheme.colors.lightShade};
|
border: 1px solid ${euiTheme.colors.lightShade};
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
|
@ -27,10 +25,6 @@ export const useCardItemStyles = () => {
|
||||||
&.card-expanded {
|
&.card-expanded {
|
||||||
${shadow};
|
${shadow};
|
||||||
transition: box-shadow ${SHADOW_ANIMATION_DURATION}ms ease-out;
|
transition: box-shadow ${SHADOW_ANIMATION_DURATION}ms ease-out;
|
||||||
|
|
||||||
.step-icon {
|
|
||||||
background-color: ${iconHoveredBackgroundColor};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.card-expanded {
|
&.card-expanded {
|
||||||
|
|
|
@ -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 { useEuiTheme } from '@elastic/eui';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useFooterStyles = () => {
|
||||||
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
|
const footerStyles = useMemo(
|
||||||
|
() => ({
|
||||||
|
wrapperStyle: css`
|
||||||
|
padding: ${euiTheme.size.xl} ${euiTheme.size.l} ${euiTheme.base * 4.5}px;
|
||||||
|
gap: ${euiTheme.base * 3.75}px;
|
||||||
|
`,
|
||||||
|
titleStyle: css`
|
||||||
|
font-size: ${euiTheme.base * 0.875}px;
|
||||||
|
font-weight: ${euiTheme.font.weight.semiBold};
|
||||||
|
line-height: ${euiTheme.size.l};
|
||||||
|
color: ${euiTheme.colors.title};
|
||||||
|
`,
|
||||||
|
descriptionStyle: css`
|
||||||
|
font-size: 12.25px;
|
||||||
|
font-weight: ${euiTheme.font.weight.regular};
|
||||||
|
line-height: ${euiTheme.base * 1.25}px;
|
||||||
|
color: ${euiTheme.colors.darkestShade};
|
||||||
|
`,
|
||||||
|
linkStyle: css`
|
||||||
|
font-size: ${euiTheme.size.m};
|
||||||
|
font-weight: ${euiTheme.font.weight.medium};
|
||||||
|
line-height: ${euiTheme.size.base};
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
euiTheme.base,
|
||||||
|
euiTheme.colors.darkestShade,
|
||||||
|
euiTheme.colors.title,
|
||||||
|
euiTheme.font.weight.medium,
|
||||||
|
euiTheme.font.weight.regular,
|
||||||
|
euiTheme.font.weight.semiBold,
|
||||||
|
euiTheme.size.base,
|
||||||
|
euiTheme.size.l,
|
||||||
|
euiTheme.size.m,
|
||||||
|
euiTheme.size.xl,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return footerStyles;
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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 { useEuiTheme } from '@elastic/eui';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useProgressBarStyles = () => {
|
||||||
|
const { euiTheme } = useEuiTheme();
|
||||||
|
const progressBarStyles = useMemo(
|
||||||
|
() => ({
|
||||||
|
textStyle: css`
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: ${euiTheme.font.weight.bold};
|
||||||
|
text-transform: uppercase;
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
[euiTheme.font.weight.bold]
|
||||||
|
);
|
||||||
|
return progressBarStyles;
|
||||||
|
};
|
|
@ -55,12 +55,19 @@ export const useStepContentStyles = () => {
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
rightPanelContentStyles: css`
|
rightPanelContentStyles: css`
|
||||||
&.right-content-panel {
|
&.right-panel-wrapper {
|
||||||
height: ${RIGHT_CONTENT_HEIGHT}px;
|
height: ${RIGHT_CONTENT_HEIGHT}px;
|
||||||
width: ${RIGHT_CONTENT_WIDTH}px;
|
width: ${RIGHT_CONTENT_WIDTH}px;
|
||||||
border-radius: ${euiTheme.border.radius.medium};
|
}
|
||||||
|
`,
|
||||||
|
getRightContentStyles: ({ shadow }: { shadow: boolean }) => css`
|
||||||
|
&.right-panel-content {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
${imageShadow};
|
${shadow ? imageShadow : ''}
|
||||||
|
border-radius: ${euiTheme.border.radius.medium};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const useWelcomeHeaderStyles = () => {
|
||||||
line-height: ${euiTheme.size.l};
|
line-height: ${euiTheme.size.l};
|
||||||
`,
|
`,
|
||||||
headerSubtitleStyles: css`
|
headerSubtitleStyles: css`
|
||||||
font-size: ${euiTheme.size.l};
|
font-size: ${euiTheme.base * 2.125}px;
|
||||||
color: ${euiTheme.colors.title};
|
color: ${euiTheme.colors.title};
|
||||||
font-weight: ${euiTheme.font.weight.bold};
|
font-weight: ${euiTheme.font.weight.bold};
|
||||||
`,
|
`,
|
||||||
|
@ -44,7 +44,7 @@ export const useWelcomeHeaderStyles = () => {
|
||||||
currentPlanWrapperStyles: css`
|
currentPlanWrapperStyles: css`
|
||||||
background-color: ${euiTheme.colors.lightestShade};
|
background-color: ${euiTheme.colors.lightestShade};
|
||||||
border-radius: 56px;
|
border-radius: 56px;
|
||||||
padding: ${euiTheme.size.xs} ${euiTheme.size.xs} ${euiTheme.size.xs} ${euiTheme.size.s};
|
padding: ${euiTheme.size.xs} ${euiTheme.size.s} ${euiTheme.size.xs} ${euiTheme.size.m};
|
||||||
height: ${euiTheme.size.xl};
|
height: ${euiTheme.size.xl};
|
||||||
`,
|
`,
|
||||||
currentPlanTextStyles: css`
|
currentPlanTextStyles: css`
|
||||||
|
|
|
@ -28,7 +28,6 @@ const TogglePanelComponent: React.FC<{
|
||||||
|
|
||||||
const { setUpSections } = useSetUpSections({ euiTheme });
|
const { setUpSections } = useSetUpSections({ euiTheme });
|
||||||
const sectionNodes = setUpSections({
|
const sectionNodes = setUpSections({
|
||||||
activeProducts,
|
|
||||||
activeSections,
|
activeSections,
|
||||||
expandedCardSteps,
|
expandedCardSteps,
|
||||||
finishedSteps,
|
finishedSteps,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue