Skip to content

Commit

Permalink
[SecuritySolution] Get Started page UI (elastic#172616)
Browse files Browse the repository at this point in the history
## Summary


Fix up for elastic#171078

Test Env:
https://p.elstc.co/paste/vmb8YG18#nCnDFTVE4HZxFK9M4TyHii3Gt4rq0YV25LQK33PqNly


<img width="2556" alt="Screenshot 2023-12-05 at 19 00 06"
src="https://github.com/elastic/kibana/assets/6295984/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="https://github.com/elastic/kibana/assets/6295984/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.




https://github.com/elastic/kibana/assets/6295984/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)](https://github.com/elastic/kibana/assets/6295984/d45c4ef3-15b9-454a-8b20-4d271e624d74)

5. Remove shadow on create project image:
![image
(4)](https://github.com/elastic/kibana/assets/6295984/a57b4de1-9d58-4983-9d53-1cb13b61e66e)

6. Change the gab between task to 16px:
![image
(3)](https://github.com/elastic/kibana/assets/6295984/0704401f-b931-40c4-8720-110ed77dab72)

7. Apply **bold** to completed task counts:
![image
(2)](https://github.com/elastic/kibana/assets/6295984/44611911-f482-447a-b525-66698f5ca2f2)

8. Update badge padding:
![image
(1)](https://github.com/elastic/kibana/assets/6295984/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
  • Loading branch information
angorayc authored Dec 6, 2023
1 parent 9034cb6 commit 45cbd2b
Show file tree
Hide file tree
Showing 39 changed files with 959 additions and 261 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 ContentWrapper = ({ children }: { children: React.ReactElement }) => <>{children}</>;
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit 45cbd2b

Please sign in to comment.