[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
![image
(5)](d45c4ef3-15b9-454a-8b20-4d271e624d74)

5. Remove shadow on create project image:
![image
(4)](a57b4de1-9d58-4983-9d53-1cb13b61e66e)

6. Change the gab between task to 16px:
![image
(3)](0704401f-b931-40c4-8720-110ed77dab72)

7. Apply **bold** to completed task counts:
![image
(2)](44611911-f482-447a-b525-66698f5ca2f2)

8. Update badge padding:
![image
(1)](44c0e854-7938-43fb-adb6-d75f8e51c1a2)


### 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:
Angela Chuang 2023-12-06 16:26:25 +00:00 committed by GitHub
parent 9034cb6181
commit 45cbd2b743
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 959 additions and 261 deletions

View 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.
*/
import React from 'react';
export const ContentWrapper = ({ children }: { children: React.ReactElement }) => <>{children}</>;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { WATCH_VIDEO_DESCRIPTION1, WATCH_VIDEO_DESCRIPTION2 } from '../translations';
import { WATCH_VIDEO_DESCRIPTION1, WATCH_VIDEO_DESCRIPTION2 } from '../../translations';
const OverviewVideoDescriptionComponent = () => (
<>

View file

@ -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();
});
});

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);
});
});

View file

@ -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);

View file

@ -77,18 +77,19 @@ const CardStepComponent: React.FC<{
const toggleStep = useCallback(
(e) => {
e.preventDefault();
const newStatus = !isExpandedStep;
if (hasStepContent) {
// Toggle step and sync the expanded card step to storage & reducer
onStepClicked({ stepId, cardId, sectionId, isExpanded: !isExpandedStep });
onStepClicked({ stepId, cardId, sectionId, isExpanded: newStatus });
navigateTo({
deepLinkId: SecurityPageName.landing,
path: `#${stepId}`,
path: newStatus ? `#${stepId}` : undefined,
});
}
},
[hasStepContent, onStepClicked, stepId, cardId, sectionId, isExpandedStep, navigateTo]
[isExpandedStep, hasStepContent, onStepClicked, stepId, cardId, sectionId, navigateTo]
);
const {
@ -170,6 +171,4 @@ const CardStepComponent: React.FC<{
);
};
CardStepComponent.displayName = 'CardStepComponent';
export const CardStep = React.memo(CardStepComponent);

View file

@ -84,7 +84,7 @@ const StepContentComponent = ({
css={rightPanelStyles}
>
{splitPanel && (
<div className="right-content-panel" css={rightPanelContentStyles}>
<div className="right-panel-wrapper" css={rightPanelContentStyles}>
{splitPanel}
</div>
)}

View file

@ -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);

View file

@ -9,19 +9,17 @@ import React from 'react';
import { defaultExpandedCards } from '../../storage';
import { CreateProjectSteps, QuickStartSectionCardsId } from '../../types';
export const mockOnStepClicked = jest.fn();
export const mockToggleTaskCompleteStatus = jest.fn();
export const StepContextProvider = ({ children }: { children: React.ReactElement }) => (
<>{children}</>
);
export const useStepContext = () => {
return {
expandedCardSteps: defaultExpandedCards,
finishedSteps: {
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
},
onStepClicked: jest.fn(),
toggleTaskCompleteStatus: jest.fn(),
};
};
export const useStepContext = jest.fn(() => ({
expandedCardSteps: defaultExpandedCards,
finishedSteps: {
[QuickStartSectionCardsId.createFirstProject]: new Set([CreateProjectSteps.createFirstProject]),
},
onStepClicked: mockOnStepClicked,
toggleTaskCompleteStatus: mockToggleTaskCompleteStatus,
}));

View file

@ -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;

View file

@ -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);

View file

@ -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',
}
);

View file

@ -18,6 +18,8 @@ import { Progress } from './progress_bar';
import { StepContextProvider } from './context/step_context';
import { CONTENT_WIDTH } from './helpers';
import { WelcomeHeader } from './welcome_header';
import { Footer } from './footer';
import { useScrollToHash } from './hooks/use_scroll';
export interface GetStartedProps {
indicesExist?: boolean;
@ -42,6 +44,8 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes, i
(product) => product.product_line === ProductLine.security
)?.product_tier;
useScrollToHash();
return (
<KibanaPageTemplate
restrictWidth={false}
@ -88,7 +92,7 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes, i
restrictWidth={CONTENT_WIDTH}
paddingSize="none"
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};
`}
>
@ -102,6 +106,9 @@ export const GetStartedComponent: React.FC<GetStartedProps> = ({ productTypes, i
<TogglePanel activeProducts={activeProducts} activeSections={activeSections} />
</StepContextProvider>
</KibanaPageTemplate.Section>
<KibanaPageTemplate.Section grow={true} restrictWidth={CONTENT_WIDTH} paddingSize="none">
<Footer />
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);
};

View file

@ -7,7 +7,16 @@
import type { ProductLine } from '../../common/product';
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';
export const CONTENT_WIDTH = 1150;
@ -53,14 +62,30 @@ const getfinishedActiveSteps = (
return new Set(finishedActiveSteps);
};
export const findCardByStepId = (
export const findCardSectionByStepId = (
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 matchedStep: Step | null = null;
const matchedCard = cards.find((c) => !!c.steps?.find((step) => stepId === step.id)) ?? null;
let matchedStep: Step | null = 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 }) => {

View file

@ -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,
});
});
});
});

View file

@ -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]);
};

View file

@ -11,8 +11,6 @@ import { useSetUpSections } from './use_setup_sections';
import type { ActiveSections, CardId, ExpandedCardSteps, StepId } from '../types';
import { CreateProjectSteps, QuickStartSectionCardsId, SectionId } from '../types';
import { ProductLine } from '../../../common/product';
const mockEuiTheme: EuiThemeComputed = {
size: {
l: '16px',
@ -44,7 +42,6 @@ describe('useSetUpSections', () => {
} as ActiveSections;
const sections = result.current.setUpSections({
activeProducts: new Set([ProductLine.security]),
activeSections,
expandedCardSteps: {} as ExpandedCardSteps,
onStepClicked,
@ -62,7 +59,6 @@ describe('useSetUpSections', () => {
const sections = result.current.setUpSections({
activeSections,
activeProducts: new Set([ProductLine.security]),
expandedCardSteps: {} as ExpandedCardSteps,
onStepClicked,
toggleTaskCompleteStatus,

View file

@ -21,12 +21,10 @@ import type {
import { CardItem } from '../card_item';
import { getSections } from '../sections';
import type { ProductLine } from '../../../common/product';
export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => {
const setUpCards = useCallback(
({
activeProducts,
activeSections,
expandedCardSteps,
finishedSteps,
@ -34,7 +32,6 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
onStepClicked,
sectionId,
}: {
activeProducts: Set<ProductLine>;
activeSections: ActiveSections | null;
expandedCardSteps: ExpandedCardSteps;
finishedSteps: Record<CardId, Set<StepId>>;
@ -65,14 +62,12 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
const setUpSections = useCallback(
({
activeProducts,
activeSections,
expandedCardSteps,
finishedSteps,
toggleTaskCompleteStatus,
onStepClicked,
}: {
activeProducts: Set<ProductLine>;
activeSections: ActiveSections | null;
expandedCardSteps: ExpandedCardSteps;
finishedSteps: Record<CardId, Set<StepId>>;
@ -81,7 +76,6 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
}) =>
getSections().reduce<React.ReactNode[]>((acc, currentSection) => {
const cardNodes = setUpCards({
activeProducts,
activeSections,
expandedCardSteps,
finishedSteps,
@ -117,10 +111,10 @@ export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) =
</span>
<EuiSpacer size="l" />
<EuiFlexGroup
gutterSize="m"
gutterSize="none"
direction="column"
css={css`
${euiTheme.size.base}
gap: ${euiTheme.size.base};
`}
>
{cardNodes}

View file

@ -27,10 +27,10 @@ import type {
Switch,
} from '../types';
import { GetStartedPageActions } from '../types';
import { findCardByStepId } from '../helpers';
import { findCardSectionByStepId } from '../helpers';
const syncExpandedCardStepsToStorageFromURL = (maybeStepId: string) => {
const { matchedCard, matchedStep } = findCardByStepId(maybeStepId);
const { matchedCard, matchedStep } = findCardSectionByStepId(maybeStepId);
const hasStepContent = matchedStep && matchedStep.description;
if (matchedCard && matchedStep && hasStepContent) {
@ -54,7 +54,9 @@ const syncExpandedCardStepsFromStorageToURL = (
);
if (expandedCardStep?.expandedSteps[0]) {
const { matchedCard, matchedStep } = findCardByStepId(expandedCardStep?.expandedSteps[0]);
const { matchedCard, matchedStep } = findCardSectionByStepId(
expandedCardStep?.expandedSteps[0]
);
callback?.({ matchedCard, matchedStep });
}
@ -114,28 +116,6 @@ export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProduct
return getAllExpandedCardStepsFromStorage();
}, [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(
({ stepId, cardId, isExpanded }) => {
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(
({ stepId, cardId, sectionId, undo }) => {
dispatch({
@ -182,5 +171,64 @@ export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProduct
[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 };
};

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

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, useEuiTheme } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { css } from '@emotion/react';
import type { ProductTier } from '../../common/product';
import { PROGRESS_TRACKER_LABEL } from './translations';
import { useProgressBarStyles } from './styles/progress_bar.style';
const ProgressComponent: React.FC<{
productTier: ProductTier | undefined;
@ -19,7 +19,7 @@ const ProgressComponent: React.FC<{
}> = ({ productTier, totalActiveSteps, totalStepsLeft }) => {
const stepsDone =
totalActiveSteps != null && totalStepsLeft != null ? totalActiveSteps - totalStepsLeft : null;
const { euiTheme } = useEuiTheme();
const { textStyle } = useProgressBarStyles();
return (
<EuiFlexGroup alignItems="center" gutterSize="s">
@ -31,19 +31,11 @@ const ProgressComponent: React.FC<{
size="m"
label={
<span>
<span
css={css`
font-size: 10.5px;
font-weight: ${euiTheme.font.weight.bold}}};
text-transform: uppercase;
`}
>
{PROGRESS_TRACKER_LABEL}
</span>
<span css={textStyle}>{PROGRESS_TRACKER_LABEL}</span>
<EuiSpacer size="s" />
</span>
}
valueText={<>{`${stepsDone}/${totalActiveSteps}`}</>}
valueText={<span css={textStyle}>{`${stepsDone}/${totalActiveSteps}`}</span>}
/>
</EuiFlexItem>
)}

View file

@ -4,11 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { MutableRefObject } 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 {
SectionId,
@ -27,18 +24,21 @@ import * as i18n from './translations';
import { AddIntegrationButton } from './step_links/add_integration_button';
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 { DashboardButton } from './step_links/dashboard_button';
import overviewVideo from './images/overview_video.svg';
import { Video } from './card_step/video';
import { fetchRuleManagementFilters } from './apis';
import { OverviewVideoDescription } from './card_step/overview_video_description';
import { Video } from './card_step/content/video';
import { OverviewVideoDescription } from './card_step/content/overview_video_description';
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 = [
{
@ -46,9 +46,7 @@ export const createProjectSteps = [
title: i18n.CREATE_PROJECT_TITLE,
icon: { type: 'addDataApp', size: 'xl' as const },
description: [i18n.CREATE_PROJECT_DESCRIPTION, <ManageProjectsButton />],
splitPanel: (
<img src={createProjects} alt={i18n.CREATE_PROJECT_TITLE} height="100%" width="100%" />
),
splitPanel: <CreateProjectImage />,
},
];
export const overviewVideoSteps = [
@ -67,16 +65,8 @@ export const addIntegrationsSteps: Array<Step<AddIntegrationsSteps.connectToData
id: AddIntegrationsSteps.connectToDataSources,
title: i18n.ADD_INTEGRATIONS_TITLE,
description: [i18n.ADD_INTEGRATIONS_DESCRIPTION, <AddIntegrationButton />],
splitPanel: (
<img
src={connectToDataSources}
alt={i18n.ADD_INTEGRATIONS_IMAGE_TITLE}
height="100%"
width="100%"
/>
),
autoCheckIfStepCompleted: async ({ indicesExist }: { indicesExist: boolean }) =>
Promise.resolve(indicesExist),
splitPanel: <AddIntegrationsImage />,
autoCheckIfStepCompleted: autoCheckAddIntegrationsStepCompleted,
},
];
@ -86,14 +76,7 @@ export const viewDashboardSteps = [
icon: { type: 'dashboardApp', size: 'xl' as const },
title: i18n.VIEW_DASHBOARDS,
description: [i18n.VIEW_DASHBOARDS_DESCRIPTION, <DashboardButton />],
splitPanel: (
<img
src={analyzeDataUsingDashboards}
alt={i18n.VIEW_DASHBOARDS_IMAGE_TITLE}
height="100%"
width="100%"
/>
),
splitPanel: <ViewDashboardImage />,
},
];
@ -103,40 +86,8 @@ export const enablePrebuildRuleSteps: Array<Step<EnablePrebuiltRulesSteps.enable
icon: { type: 'advancedSettingsApp', size: 'xl' as const },
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
description: [i18n.ENABLE_RULES_DESCRIPTION, <AddElasticRulesButton />],
splitPanel: (
<img src={enablePrebuiltRules} alt={i18n.ENABLE_RULES} height="100%" width="100%" />
),
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;
}
},
splitPanel: <EnableRuleImage />,
autoCheckIfStepCompleted: autoCheckPrebuildRuleStepCompleted,
},
];
@ -146,7 +97,7 @@ export const viewAlertSteps = [
title: i18n.VIEW_ALERTS_TITLE,
id: ViewAlertsSteps.viewAlerts,
description: [i18n.VIEW_ALERTS_DESCRIPTION, <AlertsButton />],
splitPanel: <img src={viewAlerts} alt={i18n.VIEW_ALERTS_TITLE} height="100%" width="100%" />,
splitPanel: <ViewAlertsImage />,
},
];

View file

@ -6,37 +6,27 @@
*/
import React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiIcon, useEuiTheme } from '@elastic/eui';
import { LinkAnchor } from '@kbn/security-solution-navigation/links';
import { EuiCallOut, EuiIcon, EuiLink, useEuiTheme } from '@elastic/eui';
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
import { useNavigateTo } from '@kbn/security-solution-navigation';
import { useAddIntegrationsCalloutStyles } from '../styles/add_integrations_callout.styles';
import { ADD_INTEGRATIONS_STEP } from './translations';
import { AddAndValidateYourDataCardsId, AddIntegrationsSteps, SectionId } from '../types';
import { useStepContext } from '../context/step_context';
import { AddIntegrationsSteps } from '../types';
const AddIntegrationsCalloutComponent = ({ stepName }: { stepName?: string }) => {
const { calloutWrapperStyles, calloutTitleStyles, calloutAnchorStyles } =
useAddIntegrationsCalloutStyles();
const { euiTheme } = useEuiTheme();
const { navigateTo } = useNavigateTo();
const { onStepClicked } = useStepContext();
const toggleStep = useCallback(() => {
onStepClicked({
stepId: AddIntegrationsSteps.connectToDataSources,
cardId: AddAndValidateYourDataCardsId.addIntegrations,
sectionId: SectionId.addAndValidateYourData,
isExpanded: true,
});
navigateTo({
deepLinkId: SecurityPageName.landing,
path: `#${AddIntegrationsSteps.connectToDataSources}`,
});
}, [navigateTo, onStepClicked]);
}, [navigateTo]);
return (
<EuiCallOut
@ -55,14 +45,10 @@ const AddIntegrationsCalloutComponent = ({ stepName }: { stepName?: string }) =>
defaultMessage="To {stepName} add integrations first {addIntegration}"
values={{
addIntegration: (
<LinkAnchor
id={SecurityPageName.landing}
onClick={toggleStep}
css={calloutAnchorStyles}
>
<EuiLink onClick={toggleStep} css={calloutAnchorStyles}>
{ADD_INTEGRATIONS_STEP}
<EuiIcon type="arrowRight" size="s" css={calloutAnchorStyles} />
</LinkAnchor>
</EuiLink>
),
stepName: stepName ?? (
<FormattedMessage

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useEuiBackgroundColor, useEuiShadow, useEuiTheme } from '@elastic/eui';
import { useEuiShadow, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
export const SHADOW_ANIMATION_DURATION = 350;
@ -13,12 +13,10 @@ export const SHADOW_ANIMATION_DURATION = 350;
export const useCardItemStyles = () => {
const { euiTheme } = useEuiTheme();
const shadow = useEuiShadow('l');
const iconHoveredBackgroundColor = useEuiBackgroundColor('success');
return css`
&.card-item {
padding: ${euiTheme.size.base};
margin-bottom: ${euiTheme.size.xs};
border-radius: ${euiTheme.size.s};
border: 1px solid ${euiTheme.colors.lightShade};
box-sizing: content-box;
@ -27,10 +25,6 @@ export const useCardItemStyles = () => {
&.card-expanded {
${shadow};
transition: box-shadow ${SHADOW_ANIMATION_DURATION}ms ease-out;
.step-icon {
background-color: ${iconHoveredBackgroundColor};
}
}
&.card-expanded {

View file

@ -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;
};

View file

@ -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;
};

View file

@ -55,12 +55,19 @@ export const useStepContentStyles = () => {
}
`,
rightPanelContentStyles: css`
&.right-content-panel {
&.right-panel-wrapper {
height: ${RIGHT_CONTENT_HEIGHT}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;
${imageShadow};
${shadow ? imageShadow : ''}
border-radius: ${euiTheme.border.radius.medium};
}
`,
}),

View file

@ -31,7 +31,7 @@ export const useWelcomeHeaderStyles = () => {
line-height: ${euiTheme.size.l};
`,
headerSubtitleStyles: css`
font-size: ${euiTheme.size.l};
font-size: ${euiTheme.base * 2.125}px;
color: ${euiTheme.colors.title};
font-weight: ${euiTheme.font.weight.bold};
`,
@ -44,7 +44,7 @@ export const useWelcomeHeaderStyles = () => {
currentPlanWrapperStyles: css`
background-color: ${euiTheme.colors.lightestShade};
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};
`,
currentPlanTextStyles: css`

View file

@ -28,7 +28,6 @@ const TogglePanelComponent: React.FC<{
const { setUpSections } = useSetUpSections({ euiTheme });
const sectionNodes = setUpSections({
activeProducts,
activeSections,
expandedCardSteps,
finishedSteps,