Skip to content

Commit

Permalink
[SecuritySolution] Share getStartedPage between ESS and serverless (e…
Browse files Browse the repository at this point in the history
…lastic#174867)

## Summary

elastic#174742

This PR move the get_started component from
`security_solution_serverless` plugin to `security_solution` plugin, so
we can share the same UI between ESS and serverless.

Parameters are set via
`x-pack/plugins/security_solution/public/contract_get_started_page.ts`
1. productTypes - set by serverless only
2. projectsUrl - set by serverless only (when running serverless
locally, this value is empty)
3. projectFeaturesUrl - set by serverless only (when running serverless
locally, this value is empty)
4. availableSteps - set by both serverless and ESS (ESS doesn't contain
`create your first project` step)

Known issue: elastic#175296

---

#### Serverless: 6 steps in total + the first step is finished by
default


![serverless](https://github.com/elastic/kibana/assets/6295984/8bbf6557-8c8e-42c6-843b-fc24ac1dd178)


#### ESS: 5 steps in total

![Screenshot 2024-01-24 at 20 04
19](https://github.com/elastic/kibana/assets/6295984/6486916a-9976-4fb5-bf07-721ba4d411aa)

### Checklist



- [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: kibanamachine <[email protected]>
  • Loading branch information
2 people authored and CoenWarmer committed Feb 15, 2024
1 parent 52c42ff commit fffb572
Show file tree
Hide file tree
Showing 131 changed files with 1,289 additions and 1,381 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import type { SecurityProductTypes } from '../../../common/components/landing_page/onboarding/configs';
import type { StepId } from '../../../common/components/landing_page/onboarding/types';

export class OnboardingPageService {
private productTypesSubject$: BehaviorSubject<SecurityProductTypes | undefined>;
private projectsUrlSubject$: BehaviorSubject<string | undefined>;
private projectFeaturesUrlSubject$: BehaviorSubject<string | undefined>;
private availableStepsSubject$: BehaviorSubject<StepId[]>;

public productTypes$: Observable<SecurityProductTypes | undefined>;
public projectsUrl$: 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.projectFeaturesUrlSubject$ = new BehaviorSubject<string | undefined>(undefined);
this.availableStepsSubject$ = new BehaviorSubject<StepId[]>([]);

this.productTypes$ = this.productTypesSubject$.asObservable();
this.projectsUrl$ = this.projectsUrlSubject$.asObservable();
this.projectFeaturesUrl$ = this.projectFeaturesUrlSubject$.asObservable();
this.availableSteps$ = this.availableStepsSubject$.asObservable();
}

setProductTypes(productTypes: SecurityProductTypes) {
this.productTypesSubject$.next(productTypes);
}
setProjectFeaturesUrl(projectFeaturesUrl: string | undefined) {
this.projectFeaturesUrlSubject$.next(projectFeaturesUrl);
}
setProjectsUrl(projectsUrl: string | undefined) {
this.projectsUrlSubject$.next(projectsUrl);
}
setAvailableSteps(availableSteps: StepId[]) {
this.availableStepsSubject$.next(availableSteps);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,19 @@ import React from 'react';
import { render } from '@testing-library/react';
import { LandingPageComponent } from '.';

const mockUseContractComponents = jest.fn(() => ({}));
jest.mock('../../hooks/use_contract_component', () => ({
useContractComponents: () => mockUseContractComponents(),
}));
jest.mock('../../containers/sourcerer', () => ({
useSourcererDataView: jest.fn().mockReturnValue({ indicesExist: false }),
}));
jest.mock('./onboarding');

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

it('renders the get started component', () => {
const GetStarted = () => <div data-test-subj="get-started-mock" />;
mockUseContractComponents.mockReturnValue({ GetStarted });
it('renders the onboarding component', () => {
const { queryByTestId } = render(<LandingPageComponent />);

expect(queryByTestId('get-started-mock')).toBeInTheDocument();
expect(queryByTestId('onboarding-with-settings')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* 2.0.
*/

import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import { useSourcererDataView } from '../../containers/sourcerer';
import { useContractComponents } from '../../hooks/use_contract_component';
import { getOnboardingComponent } from './onboarding';

export const LandingPageComponent = memo(() => {
const { GetStarted } = useContractComponents();
const { indicesExist } = useSourcererDataView();
return GetStarted ? <GetStarted indicesExist={indicesExist} /> : null;
const OnBoarding = useMemo(() => getOnboardingComponent(), []);
return <OnBoarding indicesExist={indicesExist} />;
});

LandingPageComponent.displayName = 'LandingPageComponent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 getOnboardingComponent = jest
.fn()
.mockReturnValue(() => <div data-test-subj="onboarding-with-settings" />);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 OnboardingWithSettingsComponent = () => (
<div data-test-subj="onboarding-with-settings" />
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

export const getStartedStorage = {
export const onboardingStorage = {
getAllFinishedStepsFromStorage: jest.fn(() => ({})),
getFinishedStepsFromStorageByCardId: jest.fn(() => []),
getActiveProductsFromStorage: jest.fn(() => []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* 2.0.
*/

import type { FetchRulesResponse } from '@kbn/security-solution-plugin/public';
import { DETECTION_ENGINE_RULES_URL_FIND } from '@kbn/security-solution-plugin/common';
import type { HttpSetup } from '@kbn/core/public';
import { DETECTION_ENGINE_RULES_URL_FIND } from '../../../../../../common/constants';
import type { FetchRulesResponse } from '../../../../../detection_engine/rule_management/logic/types';

export const fetchRuleManagementFilters = async ({
http,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { ProductLine } from '../../common/product';
import { ProductLine } from './configs';
import { PRODUCT_BADGE_ANALYTICS, PRODUCT_BADGE_CLOUD, PRODUCT_BADGE_EDR } from './translations';
import type { Badge } from './types';
import { BadgeId } from './types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* 2.0.
*/

import { ProductLine } from '../../common/product';
import { analyticsBadge, cloudBadge, edrBadge, getProductBadges } from './badge';
import { ProductLine } from './configs';

describe('getProductBadges', () => {
test('should return all badges if no productLineRequired is passed', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';

import React, { useMemo, useCallback } from 'react';

import classnames from 'classnames';
import type {
CardId,
Expand All @@ -20,6 +22,8 @@ import { getCard } from './helpers';
import { CardStep } from './card_step';
import { useCardItemStyles } from './styles/card_item.styles';

export const SHADOW_ANIMATION_DURATION = 350;

const CardItemComponent: React.FC<{
activeStepIds: StepId[] | undefined;
cardId: CardId;
Expand All @@ -44,12 +48,16 @@ const CardItemComponent: React.FC<{
() => new Set(expandedCardSteps[cardId]?.expandedSteps ?? []),
[cardId, expandedCardSteps]
);
const cardItemPanelStyle = useCardItemStyles();

const cardClassNames = classnames('card-item', {
'card-expanded': isExpandedCard,
});
const cardClassNames = classnames(
'card-item',
{
'card-expanded': isExpandedCard,
},
cardItemPanelStyle
);

const cardItemPanelStyle = useCardItemStyles();
const getCardStep = useCallback(
(stepId: StepId) => cardItem?.steps?.find((step) => step.id === stepId),
[cardItem?.steps]
Expand Down Expand Up @@ -91,7 +99,6 @@ const CardItemComponent: React.FC<{
className={cardClassNames}
hasBorder
paddingSize="none"
css={cardItemPanelStyle}
borderRadius="none"
data-test-subj={cardId}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import classnames from 'classnames';
import React from 'react';
import { useStepContentStyles } from '../../styles/step_content.styles';

Expand All @@ -14,12 +15,8 @@ const ContentWrapperComponent: React.FC<{ children: React.ReactElement; shadow?:
}) => {
const { getRightContentStyles } = useStepContentStyles();
const rightContentStyles = getRightContentStyles({ shadow });

return (
<div className="right-panel-content" css={rightContentStyles}>
{children}
</div>
);
const rightPanelContentClassNames = classnames('right-panel-content', rightContentStyles);
return <div className={rightPanelContentClassNames}>{children}</div>;
};

export const ContentWrapper = React.memo(ContentWrapperComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jest.mock('@elastic/eui', () => ({
EuiFlexItem: ({ children }: { children: React.ReactElement }) => <div>{children}</div>,
EuiIcon: () => <span data-test-subj="mock-play-icon" />,
useEuiTheme: () => ({ euiTheme: { colors: { fullShade: '#000', emptyShade: '#fff' } } }),
EuiCodeBlock: () => <span data-test-subj="mock-code-block" />,
}));

describe('Video Component', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

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';
import { ENABLED_FIELD } from '../../../../../../common/detection_engine/rule_management/rule_fields';

export const autoCheckPrebuildRuleStepCompleted = async ({
abortSignal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ jest.mock('./step_content', () => ({
jest.mock('../context/step_context');
jest.mock('../apis');

jest.mock('../../common/services');

jest.mock('@kbn/security-solution-plugin/public', () => ({
useSourcererDataView: jest.fn().mockReturnValue({ indicesExist: false }),
}));
jest.mock('../../../../lib/kibana');

jest.mock('@kbn/security-solution-navigation', () => ({
useNavigateTo: jest.fn().mockReturnValue({ navigateTo: jest.fn() }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,21 @@ const CardStepComponent: React.FC<{
} = useCardStepStyles();
const stepGroundStyles = getStepGroundStyles({ hasStepContent });

const panelClassNames = classnames({
'step-panel-collapsed': !isExpandedStep,
});
const panelClassNames = classnames(
{
'step-panel-collapsed': !isExpandedStep,
},
stepPanelStyles
);

const stepIconClassNames = classnames('step-icon', {
'step-icon-done': isDone,
stepIconStyles,
});

const stepTitleClassNames = classnames('step-title', stepTitleStyles);
const allDoneTextNames = classnames('all-done-badge', allDoneTextStyles);

return (
<EuiPanel
color="plain"
Expand All @@ -120,23 +127,20 @@ const CardStepComponent: React.FC<{
paddingSize="none"
className={panelClassNames}
id={stepId}
css={stepPanelStyles}
>
<EuiFlexGroup gutterSize="none" css={stepGroundStyles}>
<EuiFlexItem grow={false} onClick={toggleStep} css={stepItemStyles}>
<span className={stepIconClassNames} css={stepIconStyles}>
<EuiFlexGroup gutterSize="none" className={stepGroundStyles}>
<EuiFlexItem grow={false} onClick={toggleStep} className={stepItemStyles}>
<span className={stepIconClassNames}>
{icon && <EuiIcon {...icon} size="l" className="eui-alignMiddle" />}
</span>
</EuiFlexItem>
<EuiFlexItem grow={1} onClick={toggleStep} css={stepItemStyles}>
<span className="step-title" css={stepTitleStyles}>
{title}
</span>
<EuiFlexItem grow={1} onClick={toggleStep} className={stepItemStyles}>
<span className={stepTitleClassNames}>{title}</span>
</EuiFlexItem>
<EuiFlexItem grow={false} css={stepItemStyles}>
<EuiFlexItem grow={false} className={stepItemStyles}>
<div>
{isDone && (
<EuiBadge className="all-done-badge" css={allDoneTextStyles} color="success">
<EuiBadge className={allDoneTextNames} color="success">
{ALL_DONE_TEXT}
</EuiBadge>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { QuickStartSectionCardsId, SectionId } from '../types';
import { overviewVideoSteps } from '../sections';

jest.mock('../context/step_context');
jest.mock('../../common/services');
jest.mock('../../../../lib/kibana');

describe('StepContent', () => {
const toggleTaskCompleteStatus = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import classnames from 'classnames';
import React from 'react';

import { useCheckStepCompleted } from '../hooks/use_check_step_completed';
import { useStepContentStyles } from '../styles/step_content.styles';
import type {
Expand Down Expand Up @@ -51,24 +53,34 @@ const StepContentComponent = ({
toggleTaskCompleteStatus,
});

const stepContentGroupClassName = classnames('step-content-group', stepContentGroupStyles);
const leftContentClassNames = classnames('left-panel', leftContentStyles);

const descriptionClassNames = classnames(
'step-content-description',
'eui-displayBlock',
descriptionStyles
);

const rightPanelClassNames = classnames('right-panel', rightPanelStyles);

const rightPanelContentClassNames = classnames('right-panel-wrapper', rightPanelContentStyles);
return (
<EuiFlexGroup
color="plain"
className="step-content-group"
css={stepContentGroupStyles}
className={stepContentGroupClassName}
data-test-subj={`${stepId}-content`}
direction="row"
gutterSize="none"
>
{step.description && (
<EuiFlexItem grow={false} css={leftContentStyles} className="left-panel">
<EuiFlexItem grow={false} className={leftContentClassNames}>
<EuiText size="s">
{step.description.map((desc, index) => (
<div
data-test-subj={`${stepId}-description-${index}`}
key={`${stepId}-description-${index}`}
className="eui-displayBlock step-content-description"
css={descriptionStyles}
className={descriptionClassNames}
>
{desc}
</div>
Expand All @@ -77,17 +89,8 @@ const StepContentComponent = ({
</EuiFlexItem>
)}
{splitPanel && (
<EuiFlexItem
grow={false}
data-test-subj="split-panel"
className="right-panel"
css={rightPanelStyles}
>
{splitPanel && (
<div className="right-panel-wrapper" css={rightPanelContentStyles}>
{splitPanel}
</div>
)}
<EuiFlexItem grow={false} data-test-subj="split-panel" className={rightPanelClassNames}>
{splitPanel && <div className={rightPanelContentClassNames}>{splitPanel}</div>}
</EuiFlexItem>
)}
</EuiFlexGroup>
Expand Down
Loading

0 comments on commit fffb572

Please sign in to comment.