Skip to content

Commit

Permalink
[SecuritySolution][Onboarding] UI update (elastic#176653)
Browse files Browse the repository at this point in the history
## Summary

1. Use the old landing page as the no data prompt. It goes to the
landing page and expands the **add integration step** when clicking the
add integration button.



https://github.com/elastic/kibana/assets/6295984/de0a2d95-507f-4d12-892a-e5593c8aa194


2. Retrieve finished steps by spaceId. Each finished steps should be
stored separately in the storage.
3. Update header image and wording.
[design](https://www.figma.com/file/07wil4wWtUy90m4NTBxZxG/Updated-Security-GSH-Flows%3A?type=design&node-id=1739-147377&mode=design&t=kFKW1s1vKYOu14Y5-0)


![onboarding](https://github.com/elastic/kibana/assets/6295984/090b290f-1cf4-448c-8e5a-036217380a65)


### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
  • Loading branch information
angorayc authored Feb 14, 2024
1 parent 9992227 commit 9f7ed88
Show file tree
Hide file tree
Showing 39 changed files with 721 additions and 158 deletions.
Original file line number Diff line number Diff line change
@@ -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 EmptyPrompt = () => <div data-test-subj="empty-prompt" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { EmptyPromptComponent } from './empty_prompt';
import { SecurityPageName } from '../../../../common';
import { useNavigateTo } from '../../lib/kibana';
import { AddIntegrationsSteps } from '../landing_page/onboarding/types';

const mockNavigateTo = jest.fn();
const mockUseNavigateTo = useNavigateTo as jest.Mock;

jest.mock('../../lib/kibana', () => ({
useNavigateTo: jest.fn(),
}));

describe('EmptyPromptComponent component', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseNavigateTo.mockImplementation(() => ({ navigateTo: mockNavigateTo }));
});

it('has add data links', () => {
const { getAllByText } = render(<EmptyPromptComponent />);
expect(getAllByText('Add security integrations')).toHaveLength(2);
});

describe.each(['header', 'footer'])('URLs at the %s', (place) => {
it('points to the default Add data URL', () => {
const { getByTestId } = render(<EmptyPromptComponent />);
const link = getByTestId(`add-integrations-${place}`);
fireEvent.click(link);
expect(mockNavigateTo).toBeCalledWith({
deepLinkId: SecurityPageName.landing,
path: `#${AddIntegrationsSteps.connectToDataSources}`,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { memo, useCallback } from 'react';
import {
EuiButton,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiPageHeader,
useEuiTheme,
type EuiThemeComputed,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { SecurityPageName } from '../../../../common';

import * as i18n from './translations';
import endpointSvg from './images/endpoint1.svg';
import cloudSvg from './images/cloud1.svg';
import siemSvg from './images/siem1.svg';
import { useNavigateTo } from '../../lib/kibana';
import { VIDEO_SOURCE } from './constants';
import { AddIntegrationsSteps } from '../landing_page/onboarding/types';

const imgUrls = {
cloud: cloudSvg,
siem: siemSvg,
endpoint: endpointSvg,
};

const headerCardStyles = css`
span.euiTitle {
font-size: 36px;
line-height: 100%;
}
`;

const pageHeaderStyles = css`
h1 {
font-size: 18px;
}
`;

const getFlexItemStyles = (euiTheme: EuiThemeComputed) => css`
background: ${euiTheme.colors.lightestShade};
padding: 20px;
`;

const cardStyles = css`
img {
margin-top: 20px;
max-width: 400px;
}
`;

const footerStyles = css`
span.euiTitle {
font-size: 36px;
line-height: 100%;
}
max-width: 600px;
display: block;
margin: 20px auto 0;
`;

export const EmptyPromptComponent: React.FC = memo(() => {
const { euiTheme } = useEuiTheme();

const { navigateTo } = useNavigateTo();

const navigateToAddIntegrations = useCallback(() => {
navigateTo({
deepLinkId: SecurityPageName.landing,
path: `#${AddIntegrationsSteps.connectToDataSources}`,
});
}, [navigateTo]);

const onClick = useCallback(() => {
navigateToAddIntegrations();
}, [navigateToAddIntegrations]);

return (
<EuiFlexGroup data-test-subj="siem-landing-page" direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiPageHeader
pageTitle={i18n.SIEM_HEADER}
iconType="logoSecurity"
css={pageHeaderStyles}
/>
<EuiCard
display="plain"
description={i18n.SIEM_DESCRIPTION}
textAlign="left"
title={i18n.SIEM_TITLE}
footer={
<EuiButton data-test-subj="add-integrations-header" onClick={onClick}>
{i18n.SIEM_CTA}
</EuiButton>
}
css={headerCardStyles}
/>
</EuiFlexItem>
<EuiFlexItem>
<iframe
allowFullScreen
className="vidyard_iframe"
frameBorder="0"
height="100%"
referrerPolicy="no-referrer"
sandbox="allow-scripts allow-same-origin"
scrolling="no"
src={VIDEO_SOURCE}
title={i18n.SIEM_HEADER}
width="100%"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem css={getFlexItemStyles(euiTheme)}>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<EuiCard
hasBorder
description={i18n.SIEM_CARD_DESCRIPTION}
image={imgUrls.siem}
textAlign="center"
title={i18n.SIEM_CARD_TITLE}
css={cardStyles}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
hasBorder
description={i18n.ENDPOINT_DESCRIPTION}
image={imgUrls.endpoint}
textAlign="center"
title={i18n.ENDPOINT_TITLE}
css={cardStyles}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
hasBorder
description={i18n.CLOUD_CARD_DESCRIPTION}
image={imgUrls.cloud}
textAlign="center"
title={i18n.CLOUD_CARD_TITLE}
css={cardStyles}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
display="plain"
description={i18n.UNIFY_DESCRIPTION}
paddingSize="l"
textAlign="center"
title={i18n.UNIFY_TITLE}
footer={
<EuiButton data-test-subj="add-integrations-footer" onClick={onClick}>
{i18n.SIEM_CTA}
</EuiButton>
}
css={footerStyles}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
});
EmptyPromptComponent.displayName = 'EmptyPromptComponent';

// eslint-disable-next-line import/no-default-export
export default EmptyPromptComponent;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { lazy, Suspense } from 'react';
import { EuiLoadingLogo } from '@elastic/eui';

const EmptyPromptLazy = lazy(() => import('./empty_prompt'));

const centerLogoStyle = { display: 'flex', margin: 'auto' };

export const EmptyPrompt = () => (
<Suspense fallback={<EuiLoadingLogo logo="logoSecurity" size="xl" style={centerLogoStyle} />}>
<EmptyPromptLazy />
</Suspense>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 SIEM_HEADER = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.siem.header',
{
defaultMessage: 'Elastic Security',
}
);

export const SIEM_TITLE = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.siem.title',
{
defaultMessage: 'Security at the speed of Elastic',
}
);
export const SIEM_DESCRIPTION = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.siem.desc',
{
defaultMessage:
'Elastic Security equips teams to prevent, detect, and respond to threats at cloud speed and scale — securing business operations with a unified, open platform.',
}
);
export const SIEM_CTA = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.siem.cta',
{
defaultMessage: 'Add security integrations',
}
);

export const ENDPOINT_TITLE = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.endpoint.title',
{
defaultMessage: 'Endpoint security at scale',
}
);
export const ENDPOINT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.endpoint.desc',
{
defaultMessage: 'Prevent, collect, detect and respond — all with Elastic Agent.',
}
);

export const SIEM_CARD_TITLE = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.siemCard.title',
{
defaultMessage: 'SIEM for the modern SOC',
}
);
export const SIEM_CARD_DESCRIPTION = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.siemCard.desc',
{
defaultMessage: 'Detect, investigate, and respond to evolving threats in your environment.',
}
);

export const CLOUD_CARD_TITLE = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.cloudCard.title',
{
defaultMessage: 'Cloud protection end-to-end',
}
);
export const CLOUD_CARD_DESCRIPTION = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.cloudCard.desc',
{
defaultMessage: 'Assess your Cloud Posture and protect your workloads from attacks.',
}
);

export const UNIFY_TITLE = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.unify.title',
{
defaultMessage: 'Unify SIEM, endpoint security, and cloud security',
}
);
export const UNIFY_DESCRIPTION = i18n.translate(
'xpack.securitySolution.getStarted.landingCards.box.unify.desc',
{
defaultMessage:
'Elastic Security modernizes security operations — enabling analytics across years of data, automating key processes, and protecting every host.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
* 2.0.
*/

export const onboardingStorage = {
getAllFinishedStepsFromStorage: jest.fn(() => ({})),
getFinishedStepsFromStorageByCardId: jest.fn(() => []),
getActiveProductsFromStorage: jest.fn(() => []),
toggleActiveProductsInStorage: jest.fn(() => []),
resetAllExpandedCardStepsToStorage: jest.fn(),
addFinishedStepToStorage: jest.fn(),
removeFinishedStepFromStorage: jest.fn(),
addExpandedCardStepToStorage: jest.fn(),
removeExpandedCardStepFromStorage: jest.fn(),
getAllExpandedCardStepsFromStorage: jest.fn(() => ({})),
};
export const mockGetAllFinishedStepsFromStorage = jest.fn(() => ({}));
export const mockGetFinishedStepsFromStorageByCardId = jest.fn(() => []);
export const mockGetActiveProductsFromStorage = jest.fn(() => []);
export const mockToggleActiveProductsInStorage = jest.fn();
export const mockResetAllExpandedCardStepsToStorage = jest.fn();
export const mockAddFinishedStepToStorage = jest.fn();
export const mockRemoveFinishedStepFromStorage = jest.fn();
export const mockAddExpandedCardStepToStorage = jest.fn();
export const mockRemoveExpandedCardStepFromStorage = jest.fn();
export const mockGetAllExpandedCardStepsFromStorage = jest.fn(() => ({}));
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback, useMemo } from 'react';
import { VIDEO_SOURCE } from '../../../../empty_prompt/constants';
import { useStepContext } from '../../context/step_context';
import { WATCH_VIDEO_BUTTON_TITLE } from '../../translations';
import { OverviewSteps, QuickStartSectionCardsId, SectionId } from '../../types';
Expand Down Expand Up @@ -69,9 +70,7 @@ const VideoComponent: React.FC = () => {
sandbox="allow-scripts allow-same-origin"
scrolling="no"
allow={isVideoPlaying ? 'autoplay;' : undefined}
src={`//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html${
isVideoPlaying ? '?autoplay=1' : ''
}`}
src={`${VIDEO_SOURCE}${isVideoPlaying ? '?autoplay=1' : ''}`}
title={WATCH_VIDEO_BUTTON_TITLE}
/>
)}
Expand Down
Loading

0 comments on commit 9f7ed88

Please sign in to comment.