Skip to content

Commit

Permalink
[Security Solution] Data ingestion hub header cards (elastic#190696)
Browse files Browse the repository at this point in the history
## Summary

This PR is one of the tasks on the issue
[elastic#189487](elastic#189487).
I've created this PR that contains the new cards that will be placed in
the new header under the new **Data Ingestion Hub**.

Cards:

<img width="1293" alt="Screenshot 2024-09-02 at 14 32 55"
src="https://github.com/user-attachments/assets/8e87803d-1445-40f6-aea5-e8706c2bf690">
<img width="1202" alt="Screenshot 2024-09-02 at 14 42 13"
src="https://github.com/user-attachments/assets/8df13332-f14c-4ac1-adb5-9c7ec5b70b03">

**Summary:**

- Card 1: On click `Watch video` → Open modal with current Get Started
video.
- Card 2: On click `Add users` → `ESS:
http://localhost:5601/app/management/security/users` `serverless:
https://cloud.elastic.co/account/members`.
- Card 3: On click `Explore Demo` → navigate to
`https://www.elastic.co/demo-gallery/security-overview`

Behavior of each card:


https://github.com/user-attachments/assets/fabdb807-5442-42c9-84c7-6bbc0084e7a1


** UPDATE:
@paulewing I've removed the feature flag and render directly the new
header, also I've removed the card that renders the same video we are
showing on the new header (first card).

<img width="1273" alt="Screenshot 2024-09-09 at 10 11 10"
src="https://github.com/user-attachments/assets/21851edd-b9dc-480a-9423-88aed0a62be7">

### Checklist

Delete any items that are not applicable to this PR.

- [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

---------

Co-authored-by: Elastic Machine <[email protected]>
Co-authored-by: Angela Chuang <[email protected]>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent cd970c6 commit 756cd63
Show file tree
Hide file tree
Showing 34 changed files with 846 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,26 @@ import type { StepId } from '../../../common/components/landing_page/onboarding/
export class OnboardingPageService {
private productTypesSubject$: BehaviorSubject<SecurityProductTypes | undefined>;
private projectsUrlSubject$: BehaviorSubject<string | undefined>;
private usersUrlSubject$: BehaviorSubject<string | undefined>;
private projectFeaturesUrlSubject$: BehaviorSubject<string | undefined>;
private availableStepsSubject$: BehaviorSubject<StepId[]>;

public productTypes$: Observable<SecurityProductTypes | undefined>;
public projectsUrl$: Observable<string | undefined>;
public usersUrl$: Observable<string | undefined>;
public projectFeaturesUrl$: Observable<string | undefined>;
public availableSteps$: Observable<StepId[]>;

constructor() {
this.productTypesSubject$ = new BehaviorSubject<SecurityProductTypes | undefined>(undefined);
this.projectsUrlSubject$ = new BehaviorSubject<string | undefined>(undefined);
this.usersUrlSubject$ = new BehaviorSubject<string | undefined>(undefined);
this.projectFeaturesUrlSubject$ = new BehaviorSubject<string | undefined>(undefined);
this.availableStepsSubject$ = new BehaviorSubject<StepId[]>([]);

this.productTypes$ = this.productTypesSubject$.asObservable();
this.projectsUrl$ = this.projectsUrlSubject$.asObservable();
this.usersUrl$ = this.usersUrlSubject$.asObservable();
this.projectFeaturesUrl$ = this.projectFeaturesUrlSubject$.asObservable();
this.availableSteps$ = this.availableStepsSubject$.asObservable();
}
Expand All @@ -39,6 +43,9 @@ export class OnboardingPageService {
setProjectFeaturesUrl(projectFeaturesUrl: string | undefined) {
this.projectFeaturesUrlSubject$.next(projectFeaturesUrl);
}
setUsersUrl(userUrl: string | undefined) {
this.usersUrlSubject$.next(userUrl);
}
setProjectsUrl(projectsUrl: string | undefined) {
this.projectsUrlSubject$.next(projectsUrl);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { render } from '@testing-library/react';
import { CardItem } from './card_item';
import type { ExpandedCardSteps, StepId } from './types';

import { QuickStartSectionCardsId, SectionId, OverviewSteps } from './types';
import { SectionId, ViewDashboardSteps, AddAndValidateYourDataCardsId } from './types';
jest.mock('./card_step');

describe('CardItemComponent', () => {
const finishedSteps = new Set([]) as Set<StepId>;
const onStepClicked = jest.fn();
const toggleTaskCompleteStatus = jest.fn();
const expandedCardSteps = {
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
[AddAndValidateYourDataCardsId.viewDashboards]: {
isExpanded: false,
expandedSteps: [] as StepId[],
},
Expand All @@ -26,30 +26,30 @@ describe('CardItemComponent', () => {
it('should render card', () => {
const { getByTestId } = render(
<CardItem
activeStepIds={[OverviewSteps.getToKnowElasticSecurity]}
cardId={QuickStartSectionCardsId.watchTheOverviewVideo}
activeStepIds={[ViewDashboardSteps.analyzeData]}
cardId={AddAndValidateYourDataCardsId.viewDashboards}
expandedCardSteps={expandedCardSteps}
finishedSteps={finishedSteps}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
onStepClicked={onStepClicked}
sectionId={SectionId.quickStart}
sectionId={SectionId.addAndValidateYourData}
/>
);

const cardTitle = getByTestId(QuickStartSectionCardsId.watchTheOverviewVideo);
const cardTitle = getByTestId(AddAndValidateYourDataCardsId.viewDashboards);
expect(cardTitle).toBeInTheDocument();
});

it('should not render card when no active steps', () => {
const { queryByText } = render(
<CardItem
activeStepIds={[]}
cardId={QuickStartSectionCardsId.watchTheOverviewVideo}
cardId={AddAndValidateYourDataCardsId.viewDashboards}
expandedCardSteps={expandedCardSteps}
finishedSteps={new Set([])}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
onStepClicked={onStepClicked}
sectionId={SectionId.quickStart}
sectionId={SectionId.addAndValidateYourData}
/>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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 } from 'react';
import { INGESTION_HUB_VIDEO_SOURCE } from '../../../../../constants';
import { WATCH_VIDEO_BUTTON_TITLE } from '../../translations';

const VIDEO_CONTENT_HEIGHT = 309;

const DataIngestionHubVideoComponent: React.FC = () => {
const ref = React.useRef<HTMLIFrameElement>(null);
const [isVideoPlaying, setIsVideoPlaying] = React.useState(false);
const { euiTheme } = useEuiTheme();

const onVideoClicked = useCallback(() => {
setIsVideoPlaying(true);
}, []);

return (
<div
css={css`
border-radius: 0px;
`}
>
<div
css={css`
height: ${VIDEO_CONTENT_HEIGHT}px;
`}
>
{isVideoPlaying && (
<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 && (
<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={`${INGESTION_HUB_VIDEO_SOURCE}${isVideoPlaying ? '?autoplay=1' : ''}`}
title={WATCH_VIDEO_BUTTON_TITLE}
/>
)}
</div>
</div>
);
};

export const DataIngestionHubVideo = React.memo(DataIngestionHubVideoComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,52 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render } from '@testing-library/react';
import { StepContent } from './step_content';
import { QuickStartSectionCardsId, SectionId } from '../types';
import { overviewVideoSteps } from '../sections';
import { AddAndValidateYourDataCardsId, SectionId } from '../types';
import { viewDashboardSteps } from '../sections';
import { mountWithIntl } from '@kbn/test-jest-helpers';

jest.mock('../context/step_context');
jest.mock('../../../../lib/kibana');
jest.mock('@kbn/security-solution-navigation/src/context');
jest.mock('../../../../lib/kibana', () => ({
useKibana: () => ({
services: {
notifications: {
toasts: {
addError: jest.fn(),
},
},
},
}),
}));

describe('StepContent', () => {
const toggleTaskCompleteStatus = jest.fn();

const props = {
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
cardId: AddAndValidateYourDataCardsId.viewDashboards,
indicesExist: false,
sectionId: SectionId.quickStart,
step: overviewVideoSteps[0],
sectionId: SectionId.addAndValidateYourData,
step: viewDashboardSteps[0],
toggleTaskCompleteStatus,
};

it('renders step content when hasStepContent is true and isExpandedStep is true', () => {
const mockProps = { ...props, hasStepContent: true, isExpandedStep: true };
const { getByTestId, getByText } = render(<StepContent {...mockProps} />);
const wrapper = mountWithIntl(<StepContent {...mockProps} />);

const splitPanelElement = getByTestId('split-panel');
const splitPanelElement = wrapper.find('[data-test-subj="split-panel"]');

expect(
getByText(
'Elastic Security unifies analytics, EDR, cloud security capabilities, and more into a SaaS solution that helps you improve your organization’s security posture, defend against a wide range of threats, and prevent breaches.'
)
).toBeInTheDocument();
expect(
getByText('To explore the platform’s core features, watch the video:')
).toBeInTheDocument();
expect(splitPanelElement.exists()).toBe(true);

expect(splitPanelElement).toBeInTheDocument();
expect(
wrapper
.text()
.includes(
'Use dashboards to visualize data and stay up-to-date with key information. Create your own, or use Elastic’s default dashboards — including alerts, user authentication events, known vulnerabilities, and more.'
)
).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -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 { COLOR_MODES_STANDARD, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';

export const useCardStyles = () => {
const { euiTheme, colorMode } = useEuiTheme();
const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;

return css`
min-width: 315px;
&.headerCard:hover {
*:not(.headerCardContent) {
text-decoration: none;
}
.headerCardContent,
.headerCardContent * {
text-decoration: underline;
text-decoration-color: ${euiTheme.colors.primaryText};
}
}
${isDarkMode
? `
background-color: ${euiTheme.colors.lightestShade};
box-shadow: none;
border: 1px solid ${euiTheme.colors.mediumShade};
`
: ''}
.headerCardTitle {
font-size: ${euiTheme.base * 0.875}px;
font-weight: ${euiTheme.font.weight.semiBold};
line-height: ${euiTheme.size.l};
color: ${euiTheme.colors.title};
text-decoration: none;
}
.headerCardImage {
width: 64px;
height: 64px;
}
.headerCardDescription {
font-size: 12.25px;
font-weight: ${euiTheme.font.weight.regular};
line-height: ${euiTheme.base * 1.25}px;
color: ${euiTheme.colors.darkestShade};
}
`;
};
Original file line number Diff line number Diff line change
@@ -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 React from 'react';
import { render } from '@testing-library/react';
import { Card } from './card';

jest.mock('../../../../../lib/kibana', () => ({
useEuiTheme: jest.fn(() => ({ euiTheme: { colorTheme: 'DARK' } })),
}));

describe('DataIngestionHubHeaderCardComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render the title, description, and icon', () => {
const { getByTestId, getByText } = render(
<Card icon={'mockIcon.png'} title={'Mock Title'} description={'Mock Description'}>
<div>{'test'}</div>
</Card>
);

expect(getByText('Mock Title')).toBeInTheDocument();
expect(getByText('Mock Description')).toBeInTheDocument();
expect(getByTestId('data-ingestion-header-card-icon')).toHaveAttribute('src', 'mockIcon.png');
});

it('should apply dark mode styles when color mode is DARK', () => {
const { container } = render(
<Card icon={'mockIcon.png'} title={'Mock Title'} description={'Mock Description'}>
<div>{'test'}</div>
</Card>
);
const cardElement = container.querySelector('.euiCard');
expect(cardElement).toHaveStyle('background-color:rgb(255, 255, 255)');
});
});
Loading

0 comments on commit 756cd63

Please sign in to comment.