[8.x] [Security Solution] Onboarding redesign (#192247) (#195979)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Onboarding redesign
(#192247)](https://github.com/elastic/kibana/pull/192247)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Sergi
Massaneda","email":"sergi.massaneda@elastic.co"},"sourceCommit":{"committedDate":"2024-10-11T19:00:26Z","message":"[Security
Solution] Onboarding redesign (#192247)\n\n## Summary\r\n\r\nIssue:
https://github.com/elastic/kibana/issues/189487?reload=1?reload=1\r\n\r\nThis
PR is the final implementation of the Onboarding page
redesign.\r\n\r\nIt has been developed in collaboration with @angorayc
and\r\n@agusruidiazgd, using this branch as a feature branch.\r\nIt
already includes 2 smaller PRs that have already been reviewed
and\r\napproved by the @elastic/security-threat-hunting-explore
team:\r\n- https://github.com/semd/kibana/pull/8\r\n-
https://github.com/semd/kibana/pull/9\r\n\r\n### Changes\r\n- Onboarding
page architecture
refactor\r\n([issue](https://github.com/elastic/kibana/issues/174766))\r\n-
Fixes https://github.com/elastic/kibana/issues/183765 (from [this
Meta\r\nissue](https://github.com/elastic/kibana/issues/183760))\r\n\r\n---\r\n\r\n-
The progress bar has been removed\r\n<img width=\"903\" alt=\"progress
bar\"\r\nsrc=\"https://github.com/user-attachments/assets/f16f3b6d-609f-4178-b83e-3b2106ba56ea\">\r\n\r\n---\r\n\r\n-
Card styles updated:\r\n - Icons updated to custom SVGs to have the
correct color\r\n - Icon with circle background\r\n - Card internal
content border removed\r\n\r\n| Old | New |\r\n| - | - |\r\n|<img
width=\"1172\" alt=\"Old
styles\"\r\nsrc=\"https://github.com/user-attachments/assets/5a75cd84-a30d-4621-88e3-17d837165016\">|<img\r\nwidth=\"1172\"
alt=\"New
styles\"\r\nsrc=\"https://github.com/user-attachments/assets/8059bcbc-2f3d-4c7e-ba4f-041a58b51372\">|\r\n\r\n---\r\n\r\n-
Completed card styles updated:\r\n - Icon with green circle
background\r\n\r\n| Old | New |\r\n| - | - |\r\n|<img width=\"1172\"
alt=\"Old styles
complete\"\r\nsrc=\"https://github.com/user-attachments/assets/3258c7be-4ffe-4d25-9cdb-d4fce66ce451\">|<img\r\nwidth=\"1172\"
alt=\"New styles
complete\"\r\nsrc=\"https://github.com/user-attachments/assets/7dac6ec0-d78d-4881-ae84-3b46562c6d7d\">|\r\n\r\n---\r\n\r\n-
Improved \"Add data with integrations\" card\r\n\r\n| Old | New |\r\n| -
| - |\r\n|<img width=\"1174\" alt=\"old integrations
card\"\r\nsrc=\"https://github.com/user-attachments/assets/3c65c4f5-004b-4619-aa92-26ec0656a8e5\">|<img\r\nwidth=\"1174\"
alt=\"new integrations
card\"\r\nsrc=\"https://github.com/user-attachments/assets/634e5249-b169-4c93-865e-b82453db90bf\">|\r\n\r\n---\r\n\r\n-
New \"Configure AI assistant\" card in a new \"Discover Elastic AI\"
group\r\n\r\n\r\nhttps://github.com/user-attachments/assets/39bd0dd4-88ba-47df-a77b-6b9b2a185cef\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Angela Chuang <yi-chun.chuang@elastic.co>\r\nCo-authored-by: Agustina
Nahir Ruidiaz
<agustina.ruidiaz@elastic.co>","sha":"d39c75a837e705637adc329887bc3b30ad90e79c","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","v9.0.0","Team:
SecuritySolution","Team:Threat
Hunting:Explore","backport:prev-minor","ci:cloud-deploy","ci:cloud-persist-deployment","v8.16.0"],"title":"[Security
Solution] Onboarding
redesign","number":192247,"url":"https://github.com/elastic/kibana/pull/192247","mergeCommit":{"message":"[Security
Solution] Onboarding redesign (#192247)\n\n## Summary\r\n\r\nIssue:
https://github.com/elastic/kibana/issues/189487?reload=1?reload=1\r\n\r\nThis
PR is the final implementation of the Onboarding page
redesign.\r\n\r\nIt has been developed in collaboration with @angorayc
and\r\n@agusruidiazgd, using this branch as a feature branch.\r\nIt
already includes 2 smaller PRs that have already been reviewed
and\r\napproved by the @elastic/security-threat-hunting-explore
team:\r\n- https://github.com/semd/kibana/pull/8\r\n-
https://github.com/semd/kibana/pull/9\r\n\r\n### Changes\r\n- Onboarding
page architecture
refactor\r\n([issue](https://github.com/elastic/kibana/issues/174766))\r\n-
Fixes https://github.com/elastic/kibana/issues/183765 (from [this
Meta\r\nissue](https://github.com/elastic/kibana/issues/183760))\r\n\r\n---\r\n\r\n-
The progress bar has been removed\r\n<img width=\"903\" alt=\"progress
bar\"\r\nsrc=\"https://github.com/user-attachments/assets/f16f3b6d-609f-4178-b83e-3b2106ba56ea\">\r\n\r\n---\r\n\r\n-
Card styles updated:\r\n - Icons updated to custom SVGs to have the
correct color\r\n - Icon with circle background\r\n - Card internal
content border removed\r\n\r\n| Old | New |\r\n| - | - |\r\n|<img
width=\"1172\" alt=\"Old
styles\"\r\nsrc=\"https://github.com/user-attachments/assets/5a75cd84-a30d-4621-88e3-17d837165016\">|<img\r\nwidth=\"1172\"
alt=\"New
styles\"\r\nsrc=\"https://github.com/user-attachments/assets/8059bcbc-2f3d-4c7e-ba4f-041a58b51372\">|\r\n\r\n---\r\n\r\n-
Completed card styles updated:\r\n - Icon with green circle
background\r\n\r\n| Old | New |\r\n| - | - |\r\n|<img width=\"1172\"
alt=\"Old styles
complete\"\r\nsrc=\"https://github.com/user-attachments/assets/3258c7be-4ffe-4d25-9cdb-d4fce66ce451\">|<img\r\nwidth=\"1172\"
alt=\"New styles
complete\"\r\nsrc=\"https://github.com/user-attachments/assets/7dac6ec0-d78d-4881-ae84-3b46562c6d7d\">|\r\n\r\n---\r\n\r\n-
Improved \"Add data with integrations\" card\r\n\r\n| Old | New |\r\n| -
| - |\r\n|<img width=\"1174\" alt=\"old integrations
card\"\r\nsrc=\"https://github.com/user-attachments/assets/3c65c4f5-004b-4619-aa92-26ec0656a8e5\">|<img\r\nwidth=\"1174\"
alt=\"new integrations
card\"\r\nsrc=\"https://github.com/user-attachments/assets/634e5249-b169-4c93-865e-b82453db90bf\">|\r\n\r\n---\r\n\r\n-
New \"Configure AI assistant\" card in a new \"Discover Elastic AI\"
group\r\n\r\n\r\nhttps://github.com/user-attachments/assets/39bd0dd4-88ba-47df-a77b-6b9b2a185cef\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Angela Chuang <yi-chun.chuang@elastic.co>\r\nCo-authored-by: Agustina
Nahir Ruidiaz
<agustina.ruidiaz@elastic.co>","sha":"d39c75a837e705637adc329887bc3b30ad90e79c"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/192247","number":192247,"mergeCommit":{"message":"[Security
Solution] Onboarding redesign (#192247)\n\n## Summary\r\n\r\nIssue:
https://github.com/elastic/kibana/issues/189487?reload=1?reload=1\r\n\r\nThis
PR is the final implementation of the Onboarding page
redesign.\r\n\r\nIt has been developed in collaboration with @angorayc
and\r\n@agusruidiazgd, using this branch as a feature branch.\r\nIt
already includes 2 smaller PRs that have already been reviewed
and\r\napproved by the @elastic/security-threat-hunting-explore
team:\r\n- https://github.com/semd/kibana/pull/8\r\n-
https://github.com/semd/kibana/pull/9\r\n\r\n### Changes\r\n- Onboarding
page architecture
refactor\r\n([issue](https://github.com/elastic/kibana/issues/174766))\r\n-
Fixes https://github.com/elastic/kibana/issues/183765 (from [this
Meta\r\nissue](https://github.com/elastic/kibana/issues/183760))\r\n\r\n---\r\n\r\n-
The progress bar has been removed\r\n<img width=\"903\" alt=\"progress
bar\"\r\nsrc=\"https://github.com/user-attachments/assets/f16f3b6d-609f-4178-b83e-3b2106ba56ea\">\r\n\r\n---\r\n\r\n-
Card styles updated:\r\n - Icons updated to custom SVGs to have the
correct color\r\n - Icon with circle background\r\n - Card internal
content border removed\r\n\r\n| Old | New |\r\n| - | - |\r\n|<img
width=\"1172\" alt=\"Old
styles\"\r\nsrc=\"https://github.com/user-attachments/assets/5a75cd84-a30d-4621-88e3-17d837165016\">|<img\r\nwidth=\"1172\"
alt=\"New
styles\"\r\nsrc=\"https://github.com/user-attachments/assets/8059bcbc-2f3d-4c7e-ba4f-041a58b51372\">|\r\n\r\n---\r\n\r\n-
Completed card styles updated:\r\n - Icon with green circle
background\r\n\r\n| Old | New |\r\n| - | - |\r\n|<img width=\"1172\"
alt=\"Old styles
complete\"\r\nsrc=\"https://github.com/user-attachments/assets/3258c7be-4ffe-4d25-9cdb-d4fce66ce451\">|<img\r\nwidth=\"1172\"
alt=\"New styles
complete\"\r\nsrc=\"https://github.com/user-attachments/assets/7dac6ec0-d78d-4881-ae84-3b46562c6d7d\">|\r\n\r\n---\r\n\r\n-
Improved \"Add data with integrations\" card\r\n\r\n| Old | New |\r\n| -
| - |\r\n|<img width=\"1174\" alt=\"old integrations
card\"\r\nsrc=\"https://github.com/user-attachments/assets/3c65c4f5-004b-4619-aa92-26ec0656a8e5\">|<img\r\nwidth=\"1174\"
alt=\"new integrations
card\"\r\nsrc=\"https://github.com/user-attachments/assets/634e5249-b169-4c93-865e-b82453db90bf\">|\r\n\r\n---\r\n\r\n-
New \"Configure AI assistant\" card in a new \"Discover Elastic AI\"
group\r\n\r\n\r\nhttps://github.com/user-attachments/assets/39bd0dd4-88ba-47df-a77b-6b9b2a185cef\r\n\r\n---------\r\n\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Angela Chuang <yi-chun.chuang@elastic.co>\r\nCo-authored-by: Agustina
Nahir Ruidiaz
<agustina.ruidiaz@elastic.co>","sha":"d39c75a837e705637adc329887bc3b30ad90e79c"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Sergi Massaneda <sergi.massaneda@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-12 08:24:14 +11:00 committed by GitHub
parent df830eec60
commit a75861e434
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
272 changed files with 5949 additions and 8476 deletions

View file

@ -27,8 +27,9 @@ export const APP_NAME = 'Security' as const;
export const APP_ICON = 'securityAnalyticsApp' as const;
export const APP_ICON_SOLUTION = 'logoSecurity' as const;
export const APP_PATH = `/app/security` as const;
export const ADD_DATA_PATH = `/app/integrations/browse/security`;
export const ADD_THREAT_INTELLIGENCE_DATA_PATH = `/app/integrations/browse/threat_intel`;
export const APP_INTEGRATIONS_PATH = `/app/integrations` as const;
export const ADD_DATA_PATH = `${APP_INTEGRATIONS_PATH}/browse/security`;
export const ADD_THREAT_INTELLIGENCE_DATA_PATH = `${APP_INTEGRATIONS_PATH}/browse/threat_intel`;
export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern' as const;
export const DEFAULT_DATE_FORMAT = 'dateFormat' as const;
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const;
@ -85,7 +86,7 @@ export const MANAGE_PATH = '/manage' as const;
export const TIMELINES_PATH = '/timelines' as const;
export const CASES_PATH = '/cases' as const;
export const OVERVIEW_PATH = '/overview' as const;
export const LANDING_PATH = '/get_started' as const;
export const ONBOARDING_PATH = '/get_started' as const;
export const DATA_QUALITY_PATH = '/data_quality' as const;
export const DETECTION_RESPONSE_PATH = '/detection_response' as const;
export const DETECTIONS_PATH = '/detections' as const;

View file

@ -10,7 +10,12 @@ import type { RouteProps } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import { Routes, Route } from '@kbn/shared-ux-router';
import type { Capabilities } from '@kbn/core/public';
import { CASES_FEATURE_ID, CASES_PATH, LANDING_PATH, SERVER_APP_ID } from '../../common/constants';
import {
CASES_FEATURE_ID,
CASES_PATH,
ONBOARDING_PATH,
SERVER_APP_ID,
} from '../../common/constants';
import { NotFoundPage } from './404';
import type { StartServices } from '../types';
@ -33,7 +38,7 @@ AppRoutes.displayName = 'AppRoutes';
export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(({ capabilities }) => {
if (capabilities[SERVER_APP_ID].show === true) {
return <Redirect to={LANDING_PATH} />;
return <Redirect to={ONBOARDING_PATH} />;
}
if (capabilities[CASES_FEATURE_ID].read_cases === true) {
return <Redirect to={CASES_PATH} />;

View file

@ -1,55 +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 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 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();
}
setProductTypes(productTypes: SecurityProductTypes) {
this.productTypesSubject$.next(productTypes);
}
setProjectFeaturesUrl(projectFeaturesUrl: string | undefined) {
this.projectFeaturesUrlSubject$.next(projectFeaturesUrl);
}
setUsersUrl(userUrl: string | undefined) {
this.usersUrlSubject$.next(userUrl);
}
setProjectsUrl(projectsUrl: string | undefined) {
this.projectsUrlSubject$.next(projectsUrl);
}
setAvailableSteps(availableSteps: StepId[]) {
this.availableStepsSubject$.next(availableSteps);
}
}

View file

@ -16,7 +16,7 @@ import { links as timelinesLinks } from './timelines/links';
import { links as casesLinks } from './cases/links';
import { links as managementLinks, getManagementFilteredLinks } from './management/links';
import { exploreLinks } from './explore/links';
import { gettingStartedLinks } from './overview/links';
import { onboardingLinks } from './onboarding/links';
import { findingsLinks } from './cloud_security_posture/links';
import type { StartPlugins } from './types';
import { dashboardsLinks } from './dashboards/links';
@ -34,7 +34,7 @@ export const appLinks: AppLinkItems = Object.freeze([
indicatorsLinks,
exploreLinks,
rulesLinks,
gettingStartedLinks,
onboardingLinks,
managementLinks,
notesLink,
]);
@ -55,7 +55,7 @@ export const getFilteredLinks = async (
indicatorsLinks,
exploreLinks,
rulesLinks,
gettingStartedLinks,
onboardingLinks,
managementFilteredLinks,
notesLink,
]);

View file

@ -0,0 +1,30 @@
/*
* 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, { useMemo } from 'react';
import { EuiLoadingSpinner, useEuiTheme, type EuiLoadingSpinnerProps } from '@elastic/eui';
interface CenteredLoadingSpinnerProps extends EuiLoadingSpinnerProps {
topOffset?: string;
}
export const CenteredLoadingSpinner = React.memo<CenteredLoadingSpinnerProps>(
({ topOffset, ...euiLoadingSpinnerProps }) => {
const { euiTheme } = useEuiTheme();
const style = useMemo(
() => ({
display: 'flex',
margin: `${euiTheme.size.xl} auto`,
...(topOffset && { marginTop: topOffset }),
}),
[topOffset, euiTheme]
);
return <EuiLoadingSpinner {...euiLoadingSpinnerProps} style={style} />;
}
);
CenteredLoadingSpinner.displayName = 'CenteredLoadingSpinner';

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export const useProjectFeaturesUrl = jest.fn(() => 'mocked_user_name');
export { CenteredLoadingSpinner } from './centered_loading_spinner';

View file

@ -10,7 +10,7 @@ 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';
import { OnboardingCardId } from '../../../onboarding/constants';
const mockNavigateTo = jest.fn();
const mockUseNavigateTo = useNavigateTo as jest.Mock;
@ -37,7 +37,7 @@ describe('EmptyPromptComponent component', () => {
fireEvent.click(link);
expect(mockNavigateTo).toBeCalledWith({
deepLinkId: SecurityPageName.landing,
path: `#${AddIntegrationsSteps.connectToDataSources}`,
path: `#${OnboardingCardId.integrations}`,
});
});
});

View file

@ -16,6 +16,7 @@ import {
type EuiThemeComputed,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { OnboardingCardId } from '../../../onboarding/constants';
import { SecurityPageName } from '../../../../common';
import * as i18n from './translations';
@ -23,8 +24,7 @@ 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';
import { ONBOARDING_VIDEO_SOURCE } from '../../constants';
const imgUrls = {
cloud: cloudSvg,
@ -75,7 +75,7 @@ export const EmptyPromptComponent = memo(() => {
const navigateToAddIntegrations = useCallback(() => {
navigateTo({
deepLinkId: SecurityPageName.landing,
path: `#${AddIntegrationsSteps.connectToDataSources}`,
path: `#${OnboardingCardId.integrations}`,
});
}, [navigateTo]);
@ -115,7 +115,7 @@ export const EmptyPromptComponent = memo(() => {
referrerPolicy="no-referrer"
sandbox="allow-scripts allow-same-origin"
scrolling="no"
src={VIDEO_SOURCE}
src={ONBOARDING_VIDEO_SOURCE}
title={i18n.SIEM_HEADER}
width="100%"
/>

View file

@ -1,26 +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 React from 'react';
import { render } from '@testing-library/react';
import { LandingPageComponent } from '.';
jest.mock('../../../sourcerer/containers', () => ({
useSourcererDataView: jest.fn().mockReturnValue({ indicesExist: false }),
}));
jest.mock('./onboarding');
describe('LandingPageComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the onboarding component', () => {
const { queryByTestId } = render(<LandingPageComponent />);
expect(queryByTestId('onboarding-with-settings')).toBeInTheDocument();
});
});

View file

@ -1,17 +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 React, { memo } from 'react';
import { useSourcererDataView } from '../../../sourcerer/containers';
import { Onboarding } from './onboarding';
export const LandingPageComponent = memo(() => {
const { indicesExist } = useSourcererDataView();
return <Onboarding indicesExist={indicesExist} />;
});
LandingPageComponent.displayName = 'LandingPageComponent';

View file

@ -1,15 +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 type { PropsWithChildren } from 'react';
import React from 'react';
export const ProductSwitch = jest
.fn()
.mockImplementation(({ children }: PropsWithChildren<unknown>) => (
<div data-test-subj="product-switch">{children}</div>
));

View file

@ -1,17 +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.
*/
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(() => ({}));

View file

@ -1,15 +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 type { PropsWithChildren } from 'react';
import React from 'react';
export const TogglePanel = jest
.fn()
.mockImplementation(({ children }: PropsWithChildren<unknown>) => (
<div data-test-subj="toggle-panel">{children}</div>
));

View file

@ -1,37 +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 { ProductLine } from './configs';
import { PRODUCT_BADGE_ANALYTICS, PRODUCT_BADGE_CLOUD, PRODUCT_BADGE_EDR } from './translations';
import type { Badge } from './types';
import { BadgeId } from './types';
export const analyticsBadge: Badge = {
id: BadgeId.analytics,
name: PRODUCT_BADGE_ANALYTICS,
};
export const cloudBadge: Badge = {
id: BadgeId.cloud,
name: PRODUCT_BADGE_CLOUD,
};
export const edrBadge: Badge = {
id: BadgeId.edr,
name: PRODUCT_BADGE_EDR,
};
const productBadges: Record<ProductLine, Badge> = {
[ProductLine.security]: analyticsBadge,
[ProductLine.cloud]: cloudBadge,
[ProductLine.endpoint]: edrBadge,
};
export const getProductBadges = (productLineRequired?: ProductLine[] | undefined): Badge[] =>
(productLineRequired ?? [ProductLine.security, ProductLine.cloud, ProductLine.endpoint]).map(
(product) => productBadges[product]
);

View file

@ -1,19 +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 { analyticsBadge, cloudBadge, edrBadge, getProductBadges } from './badge';
import { ProductLine } from './configs';
describe('getProductBadges', () => {
test('should return all badges if no productLineRequired is passed', () => {
expect(getProductBadges()).toEqual([analyticsBadge, cloudBadge, edrBadge]);
});
test('should return only the badges for the productLineRequired passed', () => {
expect(getProductBadges([ProductLine.cloud])).toEqual([cloudBadge]);
});
});

View file

@ -1,59 +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 React from 'react';
import { render } from '@testing-library/react';
import { CardItem } from './card_item';
import type { ExpandedCardSteps, StepId } 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 = {
[AddAndValidateYourDataCardsId.viewDashboards]: {
isExpanded: false,
expandedSteps: [] as StepId[],
},
} as ExpandedCardSteps;
it('should render card', () => {
const { getByTestId } = render(
<CardItem
activeStepIds={[ViewDashboardSteps.analyzeData]}
cardId={AddAndValidateYourDataCardsId.viewDashboards}
expandedCardSteps={expandedCardSteps}
finishedSteps={finishedSteps}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
onStepClicked={onStepClicked}
sectionId={SectionId.addAndValidateYourData}
/>
);
const cardTitle = getByTestId(AddAndValidateYourDataCardsId.viewDashboards);
expect(cardTitle).toBeInTheDocument();
});
it('should not render card when no active steps', () => {
const { queryByText } = render(
<CardItem
activeStepIds={[]}
cardId={AddAndValidateYourDataCardsId.viewDashboards}
expandedCardSteps={expandedCardSteps}
finishedSteps={new Set([])}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
onStepClicked={onStepClicked}
sectionId={SectionId.addAndValidateYourData}
/>
);
const cardTitle = queryByText('Introduction');
expect(cardTitle).not.toBeInTheDocument();
});
});

View file

@ -1,113 +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, EuiPanel } from '@elastic/eui';
import React, { useMemo, useCallback } from 'react';
import classnames from 'classnames';
import type {
CardId,
ExpandedCardSteps,
ToggleTaskCompleteStatus,
OnStepClicked,
SectionId,
StepId,
} from './types';
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;
expandedCardSteps: ExpandedCardSteps;
finishedSteps: Set<StepId>;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
onStepClicked: OnStepClicked;
sectionId: SectionId;
}> = ({
activeStepIds,
cardId,
expandedCardSteps,
finishedSteps,
toggleTaskCompleteStatus,
onStepClicked,
sectionId,
}) => {
const isExpandedCard = expandedCardSteps[cardId].isExpanded;
const cardItem = useMemo(() => getCard({ cardId, sectionId }), [cardId, sectionId]);
const expandedSteps = useMemo(
() => new Set(expandedCardSteps[cardId]?.expandedSteps ?? []),
[cardId, expandedCardSteps]
);
const cardItemPanelStyle = useCardItemStyles();
const cardClassNames = classnames(
'card-item',
{
'card-expanded': isExpandedCard,
},
cardItemPanelStyle
);
const getCardStep = useCallback(
(stepId: StepId) => cardItem?.steps?.find((step) => step.id === stepId),
[cardItem?.steps]
);
const steps = useMemo(
() =>
activeStepIds?.reduce<React.ReactElement[]>((acc, stepId) => {
const step = getCardStep(stepId);
if (step && cardItem) {
acc.push(
<CardStep
cardId={cardItem.id}
expandedSteps={expandedSteps}
finishedSteps={finishedSteps}
key={stepId}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
onStepClicked={onStepClicked}
sectionId={sectionId}
step={step}
/>
);
}
return acc;
}, []),
[
activeStepIds,
cardItem,
expandedSteps,
finishedSteps,
getCardStep,
onStepClicked,
sectionId,
toggleTaskCompleteStatus,
]
);
return cardItem && activeStepIds ? (
<EuiPanel
className={cardClassNames}
hasBorder
paddingSize="none"
borderRadius="none"
data-test-subj={cardId}
>
<EuiFlexGroup gutterSize="s" direction="column">
<EuiFlexItem>{steps}</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
) : null;
};
CardItemComponent.displayName = 'CardItemComponent';
export const CardItem = React.memo(CardItemComponent);

View file

@ -1,11 +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 React from 'react';
export const CardStep = jest
.fn()
.mockReturnValue(({ title }: { title: string }) => <div>{title}</div>);

View file

@ -1,26 +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 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

@ -1,22 +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 classnames from 'classnames';
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 });
const rightPanelContentClassNames = classnames('right-panel-content', rightContentStyles);
return <div className={rightPanelContentClassNames}>{children}</div>;
};
export const ContentWrapper = React.memo(ContentWrapperComponent);

View file

@ -1,19 +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 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

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

View file

@ -1,19 +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 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

@ -1,18 +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 React from 'react';
import { WATCH_VIDEO_DESCRIPTION1, WATCH_VIDEO_DESCRIPTION2 } from '../../translations';
const OverviewVideoDescriptionComponent = () => (
<>
<span className="eui-displayInlineBlock">{WATCH_VIDEO_DESCRIPTION1}</span>
<span className="eui-displayInlineBlock step-paragraph">{WATCH_VIDEO_DESCRIPTION2}</span>
</>
);
export const OverviewVideoDescription = React.memo(OverviewVideoDescriptionComponent);

View file

@ -1,84 +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 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', () => ({
...jest.requireActual('@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' } } }),
EuiCodeBlock: () => <span data-test-subj="mock-code-block" />,
}));
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,
trigger: 'click',
});
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

@ -1,89 +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 { 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';
import { ContentWrapper } from './content_wrapper';
const VIDEO_CONTENT_HEIGHT = 320;
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,
trigger: 'click',
});
setIsVideoPlaying(true);
}, [toggleTaskCompleteStatus]);
return (
<ContentWrapper>
<div
css={css`
height: ${VIDEO_CONTENT_HEIGHT}px;
`}
>
{!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={`${VIDEO_SOURCE}${isVideoPlaying ? '?autoplay=1' : ''}`}
title={WATCH_VIDEO_BUTTON_TITLE}
/>
)}
</div>
</ContentWrapper>
);
};
export const Video = React.memo(VideoComponent);

View file

@ -1,19 +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 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

@ -1,24 +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 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

@ -1,57 +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 { 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: 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: 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: mockAbortController,
kibanaServicesHttp: mockHttp,
onError: mockOnError,
});
expect(mockOnError).not.toHaveBeenCalled();
expect(result).toBe(false);
});
});

View file

@ -1,48 +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 type { HttpSetup } from '@kbn/core/public';
import { fetchRuleManagementFilters } from '../apis';
import { ENABLED_FIELD } from '../../../../../../common/detection_engine/rule_management/rule_fields';
export const autoCheckPrebuildRuleStepCompleted = async ({
abortSignal,
kibanaServicesHttp,
onError,
}: {
abortSignal: 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.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.signal.aborted) {
onError?.(e);
}
return false;
}
};
export const autoCheckAddIntegrationsStepCompleted = async ({
indicesExist,
}: {
indicesExist: boolean;
}) => Promise.resolve(indicesExist);

View file

@ -1,130 +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 React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { CardStep } from '.';
import type { StepId } from '../types';
import {
EnablePrebuiltRulesSteps,
GetStartedWithAlertsCardsId,
QuickStartSectionCardsId,
SectionId,
OverviewSteps,
CreateProjectSteps,
} from '../types';
import { ALL_DONE_TEXT } from '../translations';
import { fetchRuleManagementFilters } from '../apis';
import { createProjectSteps, enablePrebuildRuleSteps, overviewVideoSteps } from '../sections';
jest.mock('./step_content', () => ({
StepContent: () => <div data-test-subj="mock-step-content" />,
}));
jest.mock('../context/step_context');
jest.mock('../apis');
jest.mock('../../../../lib/kibana');
jest.mock('@kbn/security-solution-navigation', () => ({
useNavigateTo: jest.fn().mockReturnValue({ navigateTo: jest.fn() }),
SecurityPageName: {
landing: 'landing',
},
}));
describe('CardStepComponent', () => {
const onStepClicked = jest.fn();
const toggleTaskCompleteStatus = jest.fn();
const expandedSteps = new Set([]);
const props = {
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
expandedSteps,
finishedSteps: new Set<StepId>(),
isExpandedCard: true,
toggleTaskCompleteStatus,
onStepClicked,
sectionId: SectionId.quickStart,
step: overviewVideoSteps[0],
};
const testStepTitle = 'Watch the overview video';
beforeEach(() => {
jest.clearAllMocks();
});
it('should toggle step expansion on click', () => {
const { getByText } = render(<CardStep {...props} />);
const stepTitle = getByText(testStepTitle);
fireEvent.click(stepTitle);
expect(onStepClicked).toHaveBeenCalledTimes(1);
expect(onStepClicked).toHaveBeenCalledWith({
sectionId: SectionId.quickStart,
stepId: OverviewSteps.getToKnowElasticSecurity,
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
isExpanded: true,
trigger: 'click',
});
});
it('should render step content when expanded', () => {
const mockProps = {
...props,
expandedSteps: new Set([
QuickStartSectionCardsId.watchTheOverviewVideo,
]) as unknown as Set<StepId>,
};
const { getByTestId } = render(<CardStep {...mockProps} />);
const content = getByTestId('mock-step-content');
expect(content).toBeInTheDocument();
});
it('should not toggle step expansion on click when there is no content', () => {
const mockProps = {
...props,
stepId: CreateProjectSteps.createFirstProject,
cardId: QuickStartSectionCardsId.createFirstProject,
finishedSteps: new Set<StepId>([CreateProjectSteps.createFirstProject]),
step: { ...createProjectSteps[0], description: undefined, splitPanel: undefined },
};
const { getByText } = render(<CardStep {...mockProps} />);
const stepTitle = getByText('Create your first project');
fireEvent.click(stepTitle);
expect(onStepClicked).toHaveBeenCalledTimes(0);
});
it('should not show the step as completed when it is not', () => {
const { queryByText } = render(<CardStep {...props} />);
const text = queryByText(ALL_DONE_TEXT);
expect(text).not.toBeInTheDocument();
});
it('should show the step as completed when it is done', async () => {
(fetchRuleManagementFilters as jest.Mock).mockResolvedValue({
total: 1,
});
const mockProps = {
...props,
cardId: GetStartedWithAlertsCardsId.enablePrebuiltRules,
finishedSteps: new Set<StepId>([EnablePrebuiltRulesSteps.enablePrebuiltRules]),
sectionId: SectionId.getStartedWithAlerts,
step: enablePrebuildRuleSteps[0],
};
const { queryByText } = render(<CardStep {...mockProps} />);
const text = queryByText(ALL_DONE_TEXT);
expect(text).toBeInTheDocument();
});
});

View file

@ -1,175 +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 {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiBadge,
EuiButtonIcon,
} from '@elastic/eui';
import React, { useCallback } from 'react';
import classnames from 'classnames';
import { useNavigateTo, SecurityPageName } from '@kbn/security-solution-navigation';
import type {
CardId,
OnStepClicked,
ToggleTaskCompleteStatus,
SectionId,
StepId,
Step,
} from '../types';
import { ALL_DONE_TEXT, EXPAND_STEP_BUTTON_LABEL } from '../translations';
import { StepContent } from './step_content';
import { useCheckStepCompleted } from '../hooks/use_check_step_completed';
import { useStepContext } from '../context/step_context';
import { useCardStepStyles } from '../styles/card_step.styles';
const CardStepComponent: React.FC<{
cardId: CardId;
expandedSteps: Set<StepId>;
finishedSteps: Set<StepId>;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
onStepClicked: OnStepClicked;
sectionId: SectionId;
step: Step;
}> = ({
cardId,
expandedSteps,
finishedSteps = new Set(),
toggleTaskCompleteStatus,
onStepClicked,
sectionId,
step,
}) => {
const { navigateTo } = useNavigateTo();
const isExpandedStep = expandedSteps.has(step.id);
const { id: stepId, title, description, splitPanel, icon, autoCheckIfStepCompleted } = step;
const hasStepContent = description != null || splitPanel != null;
const { indicesExist } = useStepContext();
useCheckStepCompleted({
autoCheckIfStepCompleted,
cardId,
indicesExist,
sectionId,
stepId,
stepTitle: title,
toggleTaskCompleteStatus,
});
const isDone = finishedSteps.has(stepId);
const toggleStep = useCallback(
(e: React.SyntheticEvent) => {
e.preventDefault();
const newStatus = !isExpandedStep;
if (hasStepContent) {
// Toggle step and sync the expanded card step to storage & reducer
onStepClicked({ stepId, cardId, sectionId, isExpanded: newStatus, trigger: 'click' });
navigateTo({
deepLinkId: SecurityPageName.landing,
path: newStatus ? `#${stepId}` : undefined,
});
}
},
[isExpandedStep, hasStepContent, onStepClicked, stepId, cardId, sectionId, navigateTo]
);
const {
stepPanelStyles,
stepIconStyles,
stepTitleStyles,
allDoneTextStyles,
toggleButtonStyles,
getStepGroundStyles,
stepItemStyles,
} = useCardStepStyles();
const stepGroundStyles = getStepGroundStyles({ hasStepContent });
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"
grow={false}
hasShadow={false}
borderRadius="none"
paddingSize="none"
className={panelClassNames}
id={stepId}
>
<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} className={stepItemStyles}>
<span className={stepTitleClassNames}>{title}</span>
</EuiFlexItem>
<EuiFlexItem grow={false} className={stepItemStyles}>
<div>
{isDone && (
<EuiBadge className={allDoneTextNames} color="success">
{ALL_DONE_TEXT}
</EuiBadge>
)}
<EuiButtonIcon
className="eui-displayInlineBlock toggle-button"
color="primary"
onClick={toggleStep}
iconType={isExpandedStep ? 'arrowUp' : 'arrowDown'}
aria-label={EXPAND_STEP_BUTTON_LABEL(title ?? '')}
aria-expanded={isExpandedStep}
size="xs"
css={toggleButtonStyles}
isDisabled={!hasStepContent}
/>
</div>
</EuiFlexItem>
</EuiFlexGroup>
{hasStepContent && (
<div className="stepContentWrapper">
<div className="stepContent">
<StepContent
autoCheckIfStepCompleted={isExpandedStep ? autoCheckIfStepCompleted : undefined}
cardId={cardId}
indicesExist={indicesExist}
sectionId={sectionId}
step={step}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
/>
</div>
</div>
)}
</EuiPanel>
);
};
export const CardStep = React.memo(CardStepComponent);

View file

@ -1,55 +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 React from 'react';
import { StepContent } from './step_content';
import { AddAndValidateYourDataCardsId, SectionId } from '../types';
import { viewDashboardSteps } from '../sections';
import { mountWithIntl } from '@kbn/test-jest-helpers';
jest.mock('../context/step_context');
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: AddAndValidateYourDataCardsId.viewDashboards,
indicesExist: false,
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 wrapper = mountWithIntl(<StepContent {...mockProps} />);
const splitPanelElement = wrapper.find('[data-test-subj="split-panel"]');
expect(splitPanelElement.exists()).toBe(true);
expect(
wrapper
.text()
.includes(
'Use dashboards to visualize data and stay up-to-date with key information. Create your own, or use Elastics default dashboards — including alerts, user authentication events, known vulnerabilities, and more.'
)
).toBe(true);
});
});

View file

@ -1,99 +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, 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 {
CardId,
CheckIfStepCompleted,
SectionId,
Step,
ToggleTaskCompleteStatus,
} from '../types';
const StepContentComponent = ({
autoCheckIfStepCompleted,
cardId,
indicesExist,
sectionId,
step,
toggleTaskCompleteStatus,
}: {
autoCheckIfStepCompleted?: CheckIfStepCompleted;
cardId: CardId;
indicesExist: boolean;
sectionId: SectionId;
step: Step;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
}) => {
const { id: stepId, splitPanel } = step;
const {
stepContentGroupStyles,
leftContentStyles,
descriptionStyles,
rightPanelStyles,
rightPanelContentStyles,
} = useStepContentStyles();
useCheckStepCompleted({
autoCheckIfStepCompleted,
cardId,
indicesExist,
sectionId,
stepId,
stepTitle: step.title,
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={stepContentGroupClassName}
data-test-subj={`${stepId}-content`}
direction="row"
gutterSize="none"
>
{step.description && (
<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={descriptionClassNames}
>
{desc}
</div>
))}
</EuiText>
</EuiFlexItem>
)}
{splitPanel && (
<EuiFlexItem grow={false} data-test-subj="split-panel" className={rightPanelClassNames}>
{splitPanel && <div className={rightPanelContentClassNames}>{splitPanel}</div>}
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
export const StepContent = React.memo(StepContentComponent);

View file

@ -1,39 +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.
*/
/**
* We do not want serverless code in this plugin. Please Do Not Reuse.
* This is temporary, will be deleted when issue-174766 is resolved.
*/
export enum ProductLine {
security = 'security',
endpoint = 'endpoint',
cloud = 'cloud',
}
/**
* We do not want serverless code in this plugin. Please Do Not Reuse.
* This is temporary, will be deleted when issue-174766 is resolved.
*/
export enum ProductTier {
essentials = 'essentials',
complete = 'complete',
}
/**
* We do not want serverless code in this plugin. Please Do Not Reuse.
* This is temporary, will be deleted when issue-174766 is resolved.
*/
export type SecurityProductTypes = Array<{
product_line: ProductLine;
product_tier: ProductTier;
}>;
/**
* We do not want serverless code in this plugin. Please Do Not Reuse.
* This is temporary, will be deleted when issue-174766 is resolved.
*/
export const ALL_PRODUCT_LINES = Object.values(ProductLine);

View file

@ -1,25 +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 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 = jest.fn(() => ({
expandedCardSteps: defaultExpandedCards,
finishedSteps: {
[QuickStartSectionCardsId.createFirstProject]: new Set([CreateProjectSteps.createFirstProject]),
},
onStepClicked: mockOnStepClicked,
toggleTaskCompleteStatus: mockToggleTaskCompleteStatus,
}));

View file

@ -1,43 +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 type { PropsWithChildren } from 'react';
import React from 'react';
import type { OnboardingHubStepLinkClickedParams } from '../../../../lib/telemetry/events/onboarding/types';
import type {
ToggleTaskCompleteStatus,
CardId,
StepId,
ExpandedCardSteps,
OnStepClicked,
} from '../types';
export interface StepContextType {
expandedCardSteps: ExpandedCardSteps;
finishedSteps: Record<CardId, Set<StepId>>;
indicesExist: boolean;
onStepClicked: OnStepClicked;
onStepLinkClicked: (params: OnboardingHubStepLinkClickedParams) => void;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
}
const StepContext = React.createContext<StepContextType | null>(null);
export const StepContextProvider: React.FC<PropsWithChildren<StepContextType>> = ({
children,
...others
}) => {
return <StepContext.Provider value={others}>{children}</StepContext.Provider>;
};
export const useStepContext = () => {
const context = React.useContext(StepContext);
if (!context) {
throw new Error('useStepContext must be used within a StepContextProvider');
}
return context;
};

View file

@ -1,60 +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 type { ReactNode } from 'react';
import React from 'react';
import type { EuiCardProps } from '@elastic/eui';
import { EuiCard, EuiTitle } from '@elastic/eui';
import classNames from 'classnames';
import { useCardStyles } from './card.styles';
interface CardProps {
icon: string;
title: string;
description: string;
children: ReactNode;
onClick?: () => void;
href?: EuiCardProps['href'];
target?: EuiCardProps['target'];
}
export const Card: React.FC<CardProps> = React.memo((props) => {
const { icon, title, description, children, onClick, href, target } = props;
const cardStyles = useCardStyles();
const cardClassName = classNames(cardStyles, 'headerCard');
return (
<EuiCard
className={cardClassName}
onClick={onClick}
href={href}
target={target}
data-test-subj="data-ingestion-header-card"
layout="horizontal"
titleSize="xs"
icon={
<img
data-test-subj="data-ingestion-header-card-icon"
className="headerCardImage"
src={icon}
alt={title}
/>
}
title={
<EuiTitle className="headerCardTitle">
<h3>{title}</h3>
</EuiTitle>
}
description={<p className="headerCardDescription">{description}</p>}
>
<div className="headerCardContent">{children}</div>
</EuiCard>
);
});
Card.displayName = 'Card';

View file

@ -1,64 +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 type { ReactNode } from 'react';
import React, { useMemo } from 'react';
import { COLOR_MODES_STANDARD, useEuiTheme } from '@elastic/eui';
import { useSpaceId } from '../../../../../hooks/use_space_id';
import video from '../../images/data_ingestion_hub_video.png';
import darkVideo from '../../images/dark_data_ingestion_hub_video.png';
import teammates from '../../images/data_ingestion_hub_teammates.png';
import darkTeammates from '../../images/dark_data_ingestion_hub_teammates.png';
import demo from '../../images/data_ingestion_hub_demo.png';
import darkDemo from '../../images/dark_data_ingestion_hub_demo.png';
import * as i18n from '../translations';
import { useUsersUrl } from '../../hooks/use_users_url';
import { VideoCard } from './video_card/video_card';
import { LinkCard } from './link_card/link_card';
const demoUrl = 'https://www.elastic.co/demo-gallery/security-overview';
export const useHeaderCards: () => ReactNode[] = () => {
const { colorMode } = useEuiTheme();
const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;
const usersUrl = useUsersUrl();
const spaceId = useSpaceId();
const cards = useMemo(() => {
const headerCards = [
<LinkCard
icon={isDarkMode ? darkTeammates : teammates}
title={i18n.DATA_INGESTION_HUB_HEADER_TEAMMATES_TITLE}
description={i18n.DATA_INGESTION_HUB_HEADER_TEAMMATES_DESCRIPTION}
href={usersUrl}
linkTitle={i18n.DATA_INGESTION_HUB_HEADER_TEAMMATES_LINK_TITLE}
/>,
<LinkCard
icon={isDarkMode ? darkDemo : demo}
title={i18n.DATA_INGESTION_HUB_HEADER_DEMO_TITLE}
description={i18n.DATA_INGESTION_HUB_HEADER_DEMO_DESCRIPTION}
href={demoUrl}
linkTitle={i18n.DATA_INGESTION_HUB_HEADER_DEMO_LINK_TITLE}
/>,
];
if (spaceId) {
return [
<VideoCard
spaceId={spaceId}
icon={isDarkMode ? darkVideo : video}
title={i18n.DATA_INGESTION_HUB_HEADER_VIDEO_TITLE}
description={i18n.DATA_INGESTION_HUB_HEADER_VIDEO_DESCRIPTION}
/>,
...headerCards,
];
}
return headerCards;
}, [isDarkMode, spaceId, usersUrl]);
return cards;
};

View file

@ -1,43 +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 React from 'react';
import { EuiLink, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { Card } from '../card';
interface VideoCardProps {
icon: string;
title: string;
description: string;
href?: string;
linkTitle: string;
}
export const LinkCard: React.FC<VideoCardProps> = React.memo((props) => {
const { icon, title, description, href, linkTitle } = props;
const { euiTheme } = useEuiTheme();
return (
<Card href={href} target="_blank" icon={icon} title={title} description={description}>
<EuiLink
href={href}
external={true}
target="_blank"
className={css({
fontSize: euiTheme.size.m,
fontWeight: euiTheme.font.weight.medium,
lineHeight: euiTheme.size.base,
})}
>
{linkTitle}
</EuiLink>
</Card>
);
});
LinkCard.displayName = 'LinkCard';

View file

@ -1,75 +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 React, { useState } from 'react';
import { useLocalStorage } from 'react-use';
import { EuiLink, EuiText } from '@elastic/eui';
import { Card } from '../card';
import { DataIngestionHubVideoModal } from './video_modal';
import * as i18n from '../../translations';
interface VideoCardProps {
icon: string;
title: string;
description: string;
spaceId: string;
}
export const getStorageKeyBySpace = (storageKey: string, spaceId: string | null | undefined) => {
if (spaceId == null) {
return storageKey;
}
return `${storageKey}.${spaceId}`;
};
const IS_ONBOARDING_HUB_VISITED_LOCAL_STORAGE_KEY = 'secutirySolution.isOnboardingHubVisited';
export const VideoCard: React.FC<VideoCardProps> = React.memo((props) => {
const { icon, title, description, spaceId } = props;
const [isModalVisible, setIsModalVisible] = useState(false);
const isOnboardingHubVisitedStorageKey = getStorageKeyBySpace(
IS_ONBOARDING_HUB_VISITED_LOCAL_STORAGE_KEY,
spaceId
);
const [isOnboardingHubVisited, setIsOnboardingHubVisited] = useLocalStorage<boolean | null>(
isOnboardingHubVisitedStorageKey,
null
);
const closeVideoModal = () => {
if (isOnboardingHubVisited === null) {
setIsOnboardingHubVisited(true);
}
setIsModalVisible(false);
};
const showVideoModal = () => {
setIsModalVisible(true);
};
return (
<>
<Card onClick={showVideoModal} icon={icon} title={title} description={description}>
<EuiText size="xs">
<EuiLink onClick={showVideoModal}>
{i18n.DATA_INGESTION_HUB_HEADER_VIDEO_LINK_TITLE}
</EuiLink>
</EuiText>
</Card>
{isModalVisible && (
<DataIngestionHubVideoModal
onCloseModal={closeVideoModal}
isOnboardingHubVisited={isOnboardingHubVisited}
/>
)}
</>
);
});
VideoCard.displayName = 'VideoCard';

View file

@ -1,29 +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 { css } from '@emotion/css';
import { useMemo } from 'react';
export const useDataIngestionHubHeaderVideoModalStyles = () => {
const dataIngestionHubHeaderStyles = useMemo(() => {
return {
modalFooterStyles: css({
justifyContent: 'center',
}),
modalBodyStyles: css({
'.euiModalBody__overflow': { padding: '0px', maskImage: 'none' },
overflow: 'hidden',
padding: '0px',
height: '312px',
}),
modalTitleStyles: css({ textAlign: 'center', fontSize: '1.375rem', fontWeight: 700 }),
modalDescriptionStyles: css({ textAlign: 'center' }),
modalStyles: css({ width: 550 }),
};
}, []);
return dataIngestionHubHeaderStyles;
};

View file

@ -1,43 +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 React from 'react';
import { render } from '@testing-library/react';
import { DataIngestionHubVideoModal } from './video_modal';
const mockOnCloseModal = jest.fn();
describe('VideoCardComponent', () => {
it('should render the title, description', () => {
const { getByText } = render(
<DataIngestionHubVideoModal onCloseModal={mockOnCloseModal} isOnboardingHubVisited={true} />
);
expect(getByText('Welcome to Elastic Security!')).toBeInTheDocument();
expect(
getByText(
'Were excited to support you in protecting your organizations data. Heres a preview of the steps youll take to set up.'
)
).toBeInTheDocument();
});
it('should render the button label "Close" on isOnboardingHubVisited true', () => {
const { getByTestId } = render(
<DataIngestionHubVideoModal onCloseModal={mockOnCloseModal} isOnboardingHubVisited={true} />
);
expect(getByTestId('video-modal-button')).toHaveTextContent('Close');
});
it('should render the button label "Head to steps" on isOnboardingHubVisited false', () => {
const { getByTestId } = render(
<DataIngestionHubVideoModal onCloseModal={mockOnCloseModal} isOnboardingHubVisited={false} />
);
expect(getByTestId('video-modal-button')).toHaveTextContent('Head to steps');
});
});

View file

@ -1,80 +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 React from 'react';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeaderTitle,
EuiSpacer,
EuiText,
useGeneratedHtmlId,
} from '@elastic/eui';
import { DataIngestionHubVideo } from '../../../card_step/content/data_ingestion_hub_video';
import {
DATA_INGESTION_HUB_VIDEO_MODAL_BUTTON,
DATA_INGESTION_HUB_VIDEO_MODAL_BUTTON_CLOSE,
DATA_INGESTION_HUB_VIDEO_MODAL_DESCRIPTION,
DATA_INGESTION_HUB_VIDEO_MODAL_TITLE,
} from '../../translations';
import { useDataIngestionHubHeaderVideoModalStyles } from './video_modal.styles';
interface DataIngestionHubVideoModalComponentProps {
onCloseModal: () => void;
isOnboardingHubVisited?: boolean | null;
}
export const DataIngestionHubVideoModal: React.FC<DataIngestionHubVideoModalComponentProps> =
React.memo(({ onCloseModal, isOnboardingHubVisited }) => {
const modalTitle = useGeneratedHtmlId();
const {
modalStyles,
modalFooterStyles,
modalBodyStyles,
modalTitleStyles,
modalDescriptionStyles,
} = useDataIngestionHubHeaderVideoModalStyles();
return (
<EuiModal
data-test-subj="data-ingestion-hub-video-modal"
aria-labelledby={modalTitle}
className={modalStyles}
onClose={onCloseModal}
>
<EuiModalBody className={modalBodyStyles}>
<DataIngestionHubVideo />
</EuiModalBody>
<EuiModalFooter className={modalFooterStyles}>
<EuiFlexGroup wrap={true} justifyContent="center">
<EuiFlexItem grow={false} justifyContent="center">
<EuiModalHeaderTitle className={modalTitleStyles} id={modalTitle}>
{DATA_INGESTION_HUB_VIDEO_MODAL_TITLE}
</EuiModalHeaderTitle>
<EuiSpacer size="m" />
<EuiText size="s" className={modalDescriptionStyles}>
{DATA_INGESTION_HUB_VIDEO_MODAL_DESCRIPTION}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton data-test-subj="video-modal-button" color="primary" onClick={onCloseModal}>
{isOnboardingHubVisited
? DATA_INGESTION_HUB_VIDEO_MODAL_BUTTON_CLOSE
: DATA_INGESTION_HUB_VIDEO_MODAL_BUTTON}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);
});
DataIngestionHubVideoModal.displayName = 'DataIngestionHubVideoModal';

View file

@ -1,68 +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 { useEuiTheme, COLOR_MODES_STANDARD } from '@elastic/eui';
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { CONTENT_WIDTH, IMG_HEADER_WIDTH } from '../helpers';
import rocket from '../images/rocket.png';
import darkRocket from '../images/dark_rocket.png';
export const useDataIngestionHubHeaderStyles = () => {
const { euiTheme, colorMode } = useEuiTheme();
const headerBackgroundImage = useMemo(
() => (colorMode === COLOR_MODES_STANDARD.dark ? darkRocket : rocket),
[colorMode]
);
const dataIngestionHubHeaderStyles = useMemo(() => {
return {
headerImageStyles: css({
backgroundImage: `url(${headerBackgroundImage})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPositionX: 'center',
backgroundPositionY: 'center',
width: `${IMG_HEADER_WIDTH}px`,
height: `${IMG_HEADER_WIDTH}px`,
}),
headerTitleStyles: css({
fontSize: `${euiTheme.base}px`,
color: euiTheme.colors.darkShade,
fontWeight: euiTheme.font.weight.bold,
lineHeight: euiTheme.size.l,
}),
headerSubtitleStyles: css({
fontSize: `${euiTheme.base * 2.125}px`,
color: euiTheme.colors.title,
fontWeight: euiTheme.font.weight.bold,
lineHeight: euiTheme.size.xxl,
}),
headerDescriptionStyles: css({
fontSize: `${euiTheme.base}px`,
color: euiTheme.colors.subduedText,
lineHeight: euiTheme.size.l,
fontWeight: euiTheme.font.weight.regular,
}),
headerContentStyles: css({
width: `${CONTENT_WIDTH / 2}px`,
}),
};
}, [
euiTheme.base,
euiTheme.colors.darkShade,
euiTheme.colors.subduedText,
euiTheme.colors.title,
euiTheme.font.weight.bold,
euiTheme.font.weight.regular,
euiTheme.size.l,
euiTheme.size.xxl,
headerBackgroundImage,
]);
return dataIngestionHubHeaderStyles;
};

View file

@ -1,122 +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 React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { DataIngestionHubHeader } from '.';
import darkRocket from '../images/dark_rocket.png';
import { useCurrentUser } from '../../../../lib/kibana';
import { useHeaderCards } from './cards/header_cards';
import { VideoCard } from './cards/video_card/video_card';
const mockSpace = {
id: 'space',
name: 'space',
disabledFeatures: [],
};
jest.mock('../../../../lib/kibana', () => {
const original = jest.requireActual('../../../../lib/kibana');
return {
useCurrentUser: jest.fn(),
useEuiTheme: jest.fn(() => ({ colorMode: 'DARK' })),
useKibana: () => ({
...original.useKibana(),
services: {
...original.useKibana().services,
spaces: {
getActiveSpace: jest.fn().mockResolvedValue(mockSpace),
},
},
}),
};
});
jest.mock('./cards/header_cards', () => ({
useHeaderCards: jest.fn(),
}));
const mockUseCurrentUser = useCurrentUser as jest.Mock;
const mockUseHeaderCards = useHeaderCards as jest.Mock;
describe('WelcomeHeaderComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseHeaderCards.mockReturnValue([]);
});
it('should render fullName when fullName is provided', () => {
const fullName = 'John Doe';
mockUseCurrentUser.mockReturnValue({ fullName });
const { getByText } = render(<DataIngestionHubHeader />);
const titleElement = getByText(`Hi ${fullName}!`);
expect(titleElement).toBeInTheDocument();
});
it('should render username when fullName is an empty string', () => {
const fullName = '';
const username = 'jd';
mockUseCurrentUser.mockReturnValue({ fullName, username });
const { getByText } = render(<DataIngestionHubHeader />);
const titleElement = getByText(`Hi ${username}!`);
expect(titleElement).toBeInTheDocument();
});
it('should render username when fullName is not provided', () => {
const username = 'jd';
mockUseCurrentUser.mockReturnValue({ username });
const { getByText } = render(<DataIngestionHubHeader />);
const titleElement = getByText(`Hi ${username}!`);
expect(titleElement).toBeInTheDocument();
});
it('should not render the greeting message if both fullName and username are not available', () => {
mockUseCurrentUser.mockReturnValue({});
const { queryByTestId } = render(<DataIngestionHubHeader />);
const greetings = queryByTestId('data-ingestion-hub-header-greetings');
expect(greetings).not.toBeInTheDocument();
});
it('should render subtitle', () => {
const { getByText } = render(<DataIngestionHubHeader />);
const subtitleElement = getByText('Welcome to Elastic Security');
expect(subtitleElement).toBeInTheDocument();
});
it('should render description', () => {
const { getByText } = render(<DataIngestionHubHeader />);
const descriptionElement = getByText('Follow these steps to set up your workspace.');
expect(descriptionElement).toBeInTheDocument();
});
it('should render the rocket dark image when the theme is DARK', () => {
const { queryByTestId } = render(<DataIngestionHubHeader />);
const image = queryByTestId('data-ingestion-hub-header-image');
expect(image).toHaveStyle({ backgroundImage: `url(${darkRocket})` });
});
it('should display the modal when the "video" card is clicked', () => {
mockUseHeaderCards.mockReturnValue([
<VideoCard
spaceId="mockSpaceId"
icon={'mockIcon.png'}
title={'Video'}
description={'Video description'}
/>,
]);
const { getByText, queryByTestId } = render(<DataIngestionHubHeader />);
const cardElement = getByText('Watch video');
fireEvent.click(cardElement);
const modalElement = queryByTestId('data-ingestion-hub-video-modal');
expect(modalElement).toBeInTheDocument();
});
});

View file

@ -1,83 +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 React from 'react';
import classnames from 'classnames';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import {
GET_STARTED_PAGE_TITLE,
GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION,
GET_STARTED_DATA_INGESTION_HUB_SUBTITLE,
} from '../translations';
import { useCurrentUser } from '../../../../lib/kibana';
import { useDataIngestionHubHeaderStyles } from './index.styles';
import { useHeaderCards } from './cards/header_cards';
export const DataIngestionHubHeader: React.FC = React.memo(() => {
const userName = useCurrentUser();
const headerCards = useHeaderCards();
const {
headerContentStyles,
headerImageStyles,
headerTitleStyles,
headerSubtitleStyles,
headerDescriptionStyles,
} = useDataIngestionHubHeaderStyles();
// Full name could be null, user name should always exist
const name = userName?.fullName || userName?.username;
const headerSubtitleClassNames = classnames('eui-displayBlock', headerSubtitleStyles);
const headerDescriptionClassNames = classnames('eui-displayBlock', headerDescriptionStyles);
return (
<>
<EuiFlexGroup
data-test-subj="data-ingestion-hub-header"
justifyContent="center"
alignItems="center"
>
<EuiFlexItem
data-test-subj="data-ingestion-hub-header-image"
grow={false}
className={headerImageStyles}
/>
<EuiFlexItem grow={false} className={headerContentStyles}>
{name && (
<EuiTitle
size="l"
className={headerTitleStyles}
data-test-subj="data-ingestion-hub-header-greetings"
>
<span>{GET_STARTED_PAGE_TITLE(name)}</span>
</EuiTitle>
)}
<EuiSpacer size="s" />
<h1 className={headerSubtitleClassNames}>{GET_STARTED_DATA_INGESTION_HUB_SUBTITLE}</h1>
<EuiSpacer size="s" />
<span className={headerDescriptionClassNames}>
{GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION}
</span>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xxl" />
<EuiFlexGroup
data-test-subj="data-ingestion-hub-header-cards"
justifyContent="center"
alignItems="center"
wrap
>
{headerCards.map((card, i) => (
<EuiFlexItem key={i}>{card}</EuiFlexItem>
))}
</EuiFlexGroup>
</>
);
});
DataIngestionHubHeader.displayName = 'DataIngestionHubHeader';

View file

@ -1,100 +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 { i18n } from '@kbn/i18n';
export const DATA_INGESTION_HUB_HEADER_VIDEO_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.video.title',
{
defaultMessage: 'Watch 2 minute overview video',
}
);
export const DATA_INGESTION_HUB_HEADER_VIDEO_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.video.description',
{
defaultMessage: 'Get acquainted with Elastic Security',
}
);
export const DATA_INGESTION_HUB_HEADER_VIDEO_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.video.link.title',
{
defaultMessage: 'Watch video',
}
);
export const DATA_INGESTION_HUB_HEADER_DEMO_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.demo.title',
{
defaultMessage: 'See Elastic Security in action',
}
);
export const DATA_INGESTION_HUB_HEADER_DEMO_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.demo.description',
{
defaultMessage: 'Explore the demo, no setup required!',
}
);
export const DATA_INGESTION_HUB_HEADER_DEMO_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.demo.link.title',
{
defaultMessage: 'Explore Demo',
}
);
export const DATA_INGESTION_HUB_HEADER_TEAMMATES_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.teammates.title',
{
defaultMessage: 'Add teammates',
}
);
export const DATA_INGESTION_HUB_HEADER_TEAMMATES_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.teammates.description',
{
defaultMessage: 'Increase collaboration across your org',
}
);
export const DATA_INGESTION_HUB_HEADER_TEAMMATES_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.teammates.link.title',
{
defaultMessage: 'Add users',
}
);
export const DATA_INGESTION_HUB_VIDEO_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.videoModal.title',
{
defaultMessage: 'Welcome to Elastic Security!',
}
);
export const DATA_INGESTION_HUB_VIDEO_MODAL_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.videoModal.description',
{
defaultMessage:
'Were excited to support you in protecting your organizations data. Heres a preview of the steps youll take to set up.',
}
);
export const DATA_INGESTION_HUB_VIDEO_MODAL_BUTTON = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.videoModal.button',
{
defaultMessage: 'Head to steps',
}
);
export const DATA_INGESTION_HUB_VIDEO_MODAL_BUTTON_CLOSE = i18n.translate(
'xpack.securitySolution.onboarding.dataIngestionHubHeader.videoModal.buttonClose',
{
defaultMessage: 'Close',
}
);

View file

@ -1,56 +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 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

@ -1,42 +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, EuiLink, EuiSpacer, EuiTitle } 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"
className={wrapperStyle}
>
{footer.map((item) => (
<EuiFlexItem key={`footer-${item.key}`}>
<img src={item.icon} alt={item.title} height="64" width="64" />
<EuiSpacer size="m" />
<EuiTitle>
<h3 className={titleStyle}>{item.title}</h3>
</EuiTitle>
<p className={descriptionStyle}>{item.description}</p>
<EuiSpacer size="m" />
<EuiLink href={item.link.href} external={true} target="_blank" className={linkStyle}>
{item.link.title}
</EuiLink>
</EuiFlexItem>
))}
</EuiFlexGroup>
);
};
export const Footer = React.memo(FooterComponent);

View file

@ -1,92 +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 { i18n } from '@kbn/i18n';
export const FOOTER_DOCUMENTATION_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.documentation.title',
{
defaultMessage: 'Browse documentation',
}
);
export const FOOTER_DOCUMENTATION_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.footer.documentation.description',
{
defaultMessage: 'In-depth guides on all Elastic features',
}
);
export const FOOTER_DOCUMENTATION_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.documentation.link.title',
{
defaultMessage: 'Start reading',
}
);
export const FOOTER_FORUM_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.forum.title',
{
defaultMessage: 'Explore forum',
}
);
export const FOOTER_FORUM_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.footer.forum.description',
{
defaultMessage: 'Exchange thoughts about Elastic',
}
);
export const FOOTER_FORUM_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.forum.link.title',
{
defaultMessage: 'Discuss Forum',
}
);
export const FOOTER_DEMO_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.demo.title',
{
defaultMessage: 'View demo project',
}
);
export const FOOTER_DEMO_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.footer.demo.description',
{
defaultMessage: 'Discover Elastic using sample data',
}
);
export const FOOTER_DEMO_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.demo.link.title',
{
defaultMessage: 'Explore demo',
}
);
export const FOOTER_LABS_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.labs.title',
{
defaultMessage: 'Elastic Security Labs',
}
);
export const FOOTER_LABS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.footer.labs.description',
{
defaultMessage: 'Insights from security researchers',
}
);
export const FOOTER_LABS_LINK_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.footer.labs.link.title',
{
defaultMessage: 'Learn more',
}
);

View file

@ -1,344 +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 {
getCardTimeInMinutes,
getCardStepsLeft,
setupActiveSections,
updateActiveSections,
isStepActive,
} from './helpers';
import type { ActiveSections, Card, CardId, Section, Step, StepId } from './types';
import {
AddAndValidateYourDataCardsId,
AddIntegrationsSteps,
CreateProjectSteps,
EnablePrebuiltRulesSteps,
OverviewSteps,
QuickStartSectionCardsId,
SectionId,
GetStartedWithAlertsCardsId,
ViewAlertsSteps,
ViewDashboardSteps,
} from './types';
import * as sectionsConfigs from './sections';
import { ProductLine } from './configs';
const mockSections = jest.spyOn(sectionsConfigs, 'getSections');
const onboardingSteps = [
CreateProjectSteps.createFirstProject,
OverviewSteps.getToKnowElasticSecurity,
AddIntegrationsSteps.connectToDataSources,
ViewDashboardSteps.analyzeData,
EnablePrebuiltRulesSteps.enablePrebuiltRules,
ViewAlertsSteps.viewAlerts,
];
describe('getCardTimeInMinutes', () => {
it('should calculate the total time in minutes for a card correctly', () => {
const card = {
steps: [
{ id: 'step1', timeInMinutes: 30 },
{ id: 'step2', timeInMinutes: 45 },
{ id: 'step3', timeInMinutes: 15 },
],
} as unknown as Card;
const activeProducts = new Set([ProductLine.security, ProductLine.cloud]);
const activeSteps = card.steps?.filter((step) => isStepActive(step, activeProducts));
const stepsDone = new Set(['step1', 'step3']) as unknown as Set<StepId>;
const timeInMinutes = getCardTimeInMinutes(activeSteps, stepsDone);
expect(timeInMinutes).toEqual(45);
});
it('should return 0 if the card is null or has no steps', () => {
const card = {} as Card;
const activeProducts = new Set([ProductLine.security, ProductLine.cloud]);
const activeSteps = card.steps?.filter((step) => isStepActive(step, activeProducts));
const stepsDone = new Set(['step1']) as unknown as Set<StepId>;
const timeInMinutes = getCardTimeInMinutes(activeSteps, stepsDone);
expect(timeInMinutes).toEqual(0);
});
});
describe('getCardStepsLeft', () => {
it('should calculate the number of steps left for a card correctly', () => {
const card = { steps: ['step1', 'step2', 'step3'] } as unknown as Card;
const activeProducts = new Set([ProductLine.security, ProductLine.cloud]);
const activeSteps = card.steps?.filter((step) => isStepActive(step, activeProducts));
const stepsDone = new Set(['step1', 'step3']) as unknown as Set<StepId>;
const stepsLeft = getCardStepsLeft(activeSteps, stepsDone);
expect(stepsLeft).toEqual(1);
});
it('should return the total number of steps if the card is null or has no steps', () => {
const card = {} as Card;
const activeProducts = new Set([ProductLine.security, ProductLine.cloud]);
const activeSteps = card.steps?.filter((step) => isStepActive(step, activeProducts));
const stepsDone = new Set() as unknown as Set<StepId>;
const stepsLeft = getCardStepsLeft(activeSteps, stepsDone);
expect(stepsLeft).toEqual(0);
});
});
describe('isStepActive', () => {
it('should return true if the step is active based on the active products', () => {
const step = {
productLineRequired: [ProductLine.cloud, ProductLine.endpoint],
id: OverviewSteps.getToKnowElasticSecurity,
} as Step;
const activeProducts = new Set([ProductLine.cloud]);
const isActive = isStepActive(step, activeProducts);
expect(isActive).toBe(true);
});
it('should return true if the card has no product type requirement', () => {
const step = {
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
} as Step;
const activeProducts = new Set([ProductLine.security]);
const isActive = isStepActive(step, activeProducts);
expect(isActive).toBe(true);
});
it('should return false if the card is not active based on the active products', () => {
const step = {
productLineRequired: [ProductLine.cloud, ProductLine.endpoint],
id: OverviewSteps.getToKnowElasticSecurity,
} as Step;
const activeProducts = new Set([ProductLine.security]);
const isActive = isStepActive(step, activeProducts);
expect(isActive).toBe(false);
});
});
describe('setupActiveSections', () => {
const getCard = (cardId: CardId, sectionId: SectionId, activeSections: ActiveSections | null) => {
const section = activeSections ? activeSections[sectionId] : {};
return section ? section[cardId] ?? { activeStepIds: null } : {};
};
it('should set up active steps based on active products', () => {
const finishedSteps = {} as unknown as Record<CardId, Set<StepId>>;
const activeProducts = new Set([ProductLine.cloud]);
const { activeSections } = setupActiveSections(finishedSteps, activeProducts, onboardingSteps);
expect(
getCard(QuickStartSectionCardsId.createFirstProject, SectionId.quickStart, activeSections)
.activeStepIds
).toEqual([CreateProjectSteps.createFirstProject]);
expect(
getCard(
AddAndValidateYourDataCardsId.addIntegrations,
SectionId.addAndValidateYourData,
activeSections
).activeStepIds
).toEqual([AddIntegrationsSteps.connectToDataSources]);
expect(
getCard(
AddAndValidateYourDataCardsId.viewDashboards,
SectionId.addAndValidateYourData,
activeSections
).activeStepIds
).toEqual([ViewDashboardSteps.analyzeData]);
expect(
getCard(
GetStartedWithAlertsCardsId.enablePrebuiltRules,
SectionId.getStartedWithAlerts,
activeSections
).activeStepIds
).toEqual([EnablePrebuiltRulesSteps.enablePrebuiltRules]);
expect(
getCard(
GetStartedWithAlertsCardsId.viewAlerts,
SectionId.getStartedWithAlerts,
activeSections
).activeStepIds
).toEqual([ViewAlertsSteps.viewAlerts]);
});
it('should set up active cards based on finished steps', () => {
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
} as unknown as Record<CardId, Set<StepId>>;
const activeProducts = new Set([ProductLine.security]);
const { activeSections } = setupActiveSections(finishedSteps, activeProducts, onboardingSteps);
expect(
getCard(QuickStartSectionCardsId.createFirstProject, SectionId.quickStart, activeSections)
).toEqual({
activeStepIds: [CreateProjectSteps.createFirstProject],
id: QuickStartSectionCardsId.createFirstProject,
stepsLeft: 0,
timeInMins: 0,
});
});
it('should return null if there are no active products', () => {
const finishedSteps = {} as unknown as Record<CardId, Set<StepId>>;
const activeProducts: Set<ProductLine> = new Set();
const activeSections = setupActiveSections(finishedSteps, activeProducts, onboardingSteps);
expect(activeSections).toEqual({
activeSections: null,
totalActiveSteps: null,
totalStepsLeft: null,
});
});
it('should handle null or empty cards in sections', () => {
mockSections.mockImplementation(() => [
{
id: SectionId.quickStart,
} as unknown as Section,
]);
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
} as unknown as Record<CardId, Set<StepId>>;
const activeProducts: Set<ProductLine> = new Set([ProductLine.security]);
const activeSections = setupActiveSections(finishedSteps, activeProducts, onboardingSteps);
expect(activeSections).toEqual({
activeSections: {},
totalActiveSteps: 0,
totalStepsLeft: 0,
});
mockSections.mockRestore();
});
});
describe('updateActiveSections', () => {
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set([CreateProjectSteps.createFirstProject]),
} as unknown as Record<CardId, Set<StepId>>;
const activeSections = {
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
stepsLeft: 0,
},
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
id: QuickStartSectionCardsId.watchTheOverviewVideo,
stepsLeft: 1,
},
},
} as ActiveSections;
it('should update the active card based on finished steps and active products', () => {
const activeProducts = new Set([ProductLine.cloud]);
const sectionId = SectionId.quickStart;
const cardId = QuickStartSectionCardsId.createFirstProject;
const testActiveSections = {
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
stepsLeft: 0,
timeInMins: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
};
const updatedSections = updateActiveSections({
activeProducts,
activeSections: testActiveSections,
cardId,
finishedSteps,
onboardingSteps,
sectionId,
});
expect(updatedSections).toEqual({
activeSections: {
...testActiveSections,
[SectionId.quickStart]: {
...testActiveSections[SectionId.quickStart],
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 0,
stepsLeft: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
},
totalActiveSteps: 1,
totalStepsLeft: 0,
});
});
it('should return null if the card is inactive based on active products', () => {
const activeProducts = new Set([ProductLine.cloud]);
const sectionId = SectionId.quickStart;
const cardId = QuickStartSectionCardsId.createFirstProject;
const updatedSections = updateActiveSections({
activeProducts,
finishedSteps,
onboardingSteps,
activeSections: null,
sectionId,
cardId,
});
expect(updatedSections).toEqual({
activeSections: null,
totalStepsLeft: null,
totalActiveSteps: null,
});
});
it('should return null if the card or activeSections is not found', () => {
const activeProducts = new Set([ProductLine.cloud]);
const sectionId = SectionId.quickStart;
const cardId = 'test' as CardId;
const updatedSections = updateActiveSections({
activeProducts,
finishedSteps,
onboardingSteps,
activeSections,
sectionId,
cardId,
});
expect(updatedSections).toEqual({
activeSections,
totalStepsLeft: null,
totalActiveSteps: null,
});
});
});

View file

@ -1,258 +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 type { ProductLine } from './configs';
import { getSections } from './sections';
import type {
ActiveCard,
ActiveSections,
Card,
CardId,
Section,
SectionId,
Step,
StepId,
} from './types';
import { CreateProjectSteps, QuickStartSectionCardsId } from './types';
export const CONTENT_WIDTH = 1150;
export const IMG_HEADER_WIDTH = 128;
export const DEFAULT_FINISHED_STEPS: Partial<Record<CardId, StepId[]>> = {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
};
export const isDefaultFinishedCardStep = (
cardId: CardId,
stepId: StepId,
onboardingSteps: StepId[]
) => !!DEFAULT_FINISHED_STEPS[cardId]?.includes(stepId) && onboardingSteps?.includes(stepId);
export const getCardTimeInMinutes = (activeSteps: Step[] | undefined, stepsDone: Set<StepId>) =>
activeSteps?.reduce(
(totalMin, { timeInMinutes, id: stepId }) =>
totalMin + (stepsDone.has(stepId) ? 0 : timeInMinutes ?? 0),
0
) ?? 0;
export const getCardStepsLeft = (activeSteps: Step[] | undefined, stepsDone: Set<StepId>) =>
Math.max((activeSteps?.length ?? 0) - (stepsDone.size ?? 0), 0);
export const isStepActive = (step: Step, activeProducts: Set<ProductLine>) =>
!step.productLineRequired ||
step.productLineRequired?.some((condition) => activeProducts.has(condition));
export const getActiveSteps = (
steps: Step[] | undefined,
activeProducts: Set<ProductLine>,
onboardingSteps: StepId[]
) =>
steps?.filter((step) => onboardingSteps.includes(step.id) && isStepActive(step, activeProducts));
const getfinishedActiveSteps = (
finishedStepIds: StepId[] | undefined,
activeStepIds: StepId[] | undefined
) => {
const finishedActiveSteps = finishedStepIds?.reduce((acc, finishedStepId) => {
const activeFinishedStepId = activeStepIds?.find(
(activeStepId) => finishedStepId === activeStepId
);
if (activeFinishedStepId) {
acc.push(activeFinishedStepId);
}
return acc;
}, [] as StepId[]);
return new Set(finishedActiveSteps);
};
export const findCardSectionByStepId = (
stepId: string
): { matchedCard: Card | null; matchedStep: Step | null; matchedSection: Section | null } => {
const cards = getSections().flatMap((s) => s.cards);
let matchedStep: Step | null = null;
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 }) => {
const sections = getSections();
const section = sections.find(({ id }) => id === sectionId);
const cards = section?.cards;
const card = cards?.find(({ id }) => id === cardId);
return card;
};
export const getStepsByActiveProduct = ({
activeProducts,
cardId,
sectionId,
onboardingSteps,
}: {
activeProducts: Set<ProductLine>;
cardId: CardId;
sectionId: SectionId;
onboardingSteps: StepId[];
}) => {
const card = getCard({ cardId, sectionId });
const steps = getActiveSteps(card?.steps, activeProducts, onboardingSteps);
return steps;
};
export const setupActiveSections = (
finishedSteps: Record<CardId, Set<StepId>>,
activeProducts: Set<ProductLine>,
onboardingSteps: StepId[]
) =>
activeProducts.size > 0
? getSections().reduce(
(acc, section) => {
const activeCards =
section.cards?.reduce((accCards, card) => {
const activeSteps = getActiveSteps(card.steps, activeProducts, onboardingSteps);
const activeStepIds = activeSteps?.map(({ id }) => id);
const stepsDone: Set<StepId> = getfinishedActiveSteps(
finishedSteps[card.id] ? [...finishedSteps[card.id]] : undefined,
activeStepIds
);
const timeInMins = getCardTimeInMinutes(activeSteps, stepsDone);
const stepsLeft = getCardStepsLeft(activeSteps, stepsDone);
acc.totalStepsLeft += stepsLeft;
acc.totalActiveSteps += activeStepIds?.length ?? 0;
if (activeSteps && activeSteps.length > 0) {
accCards[card.id] = {
id: card.id,
timeInMins,
stepsLeft,
activeStepIds,
};
}
return accCards;
}, {} as Record<CardId, ActiveCard>) ?? {};
if (Object.keys(activeCards).length > 0) {
acc.activeSections[section.id] = activeCards;
}
return acc;
},
{ activeSections: {} as ActiveSections, totalStepsLeft: 0, totalActiveSteps: 0 }
)
: { activeSections: null, totalStepsLeft: null, totalActiveSteps: null };
export const updateActiveSections = ({
activeProducts,
activeSections,
cardId,
finishedSteps,
onboardingSteps,
sectionId,
}: {
activeProducts: Set<ProductLine>;
activeSections: ActiveSections | null;
cardId: CardId;
finishedSteps: Record<CardId, Set<StepId>>;
onboardingSteps: StepId[];
sectionId: SectionId;
}): {
activeSections: ActiveSections | null;
totalStepsLeft: number | null;
totalActiveSteps: number | null;
} => {
const activeSection = activeSections ? activeSections[sectionId] : undefined;
const activeCard = activeSection ? activeSection[cardId] : undefined;
if (!activeCard || !activeSections) {
return { activeSections, totalActiveSteps: null, totalStepsLeft: null };
}
const steps = getStepsByActiveProduct({ activeProducts, cardId, sectionId, onboardingSteps });
const activeStepIds = activeCard.activeStepIds;
const stepsDone: Set<StepId> = getfinishedActiveSteps(
finishedSteps[cardId] ? [...finishedSteps[cardId]] : undefined,
activeStepIds
);
const timeInMins = getCardTimeInMinutes(steps, stepsDone);
const stepsLeft = getCardStepsLeft(steps, stepsDone);
const newActiveSections = {
...activeSections,
[sectionId]: {
...activeSections[sectionId],
...(activeStepIds && activeStepIds?.length > 0
? {
[cardId]: {
id: cardId,
timeInMins,
stepsLeft,
activeStepIds,
},
}
: {}),
},
};
const { totalStepsLeft, totalActiveSteps } = Object.values(newActiveSections).reduce(
(acc, newActiveSection) => {
Object.values(newActiveSection).forEach(
(newActiveCard) => {
acc.totalStepsLeft += newActiveCard.stepsLeft;
acc.totalActiveSteps += newActiveCard?.activeStepIds?.length ?? 0;
},
{ totalStepsLeft: 0, totalActiveSteps: 0 }
);
return acc;
},
{ totalStepsLeft: 0, totalActiveSteps: 0 }
);
return {
activeSections: newActiveSections,
totalStepsLeft,
totalActiveSteps,
};
};
export const getTotalStepsLeftAndActiveSteps = (activeSections: ActiveSections | null) =>
Object.values(activeSections ?? {}).reduce(
(acc, activeSection) => {
Object.values(activeSection).forEach(
(newActiveCard) => {
acc.totalStepsLeft += newActiveCard.stepsLeft;
acc.totalActiveSteps += newActiveCard?.activeStepIds?.length ?? 0;
},
{ totalStepsLeft: 0, totalActiveSteps: 0 }
);
return acc;
},
{ totalStepsLeft: 0, totalActiveSteps: 0 }
);

View file

@ -1,24 +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 {
AddIntegrationsSteps,
CreateProjectSteps,
EnablePrebuiltRulesSteps,
OverviewSteps,
ViewAlertsSteps,
ViewDashboardSteps,
} from '../../types';
export const useAvailableSteps = jest.fn(() => [
CreateProjectSteps.createFirstProject,
OverviewSteps.getToKnowElasticSecurity,
AddIntegrationsSteps.connectToDataSources,
ViewDashboardSteps.analyzeData,
EnablePrebuiltRulesSteps.enablePrebuiltRules,
ViewAlertsSteps.viewAlerts,
]);

View file

@ -1,11 +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 { ProductLine, ProductTier } from '../../configs';
export const useProductTypes = jest.fn(() => [
{ product_line: ProductLine.security, product_tier: ProductTier.complete },
]);

View file

@ -1,13 +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 { useObservable } from 'react-use';
import { useKibana } from '../../../../lib/kibana';
export const useAvailableSteps = () => {
const { availableSteps$ } = useKibana().services.onboarding;
return useObservable(availableSteps$);
};

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 { renderHook } from '@testing-library/react-hooks';
import { useCheckStepCompleted } from './use_check_step_completed';
import {
EnablePrebuiltRulesSteps,
GetStartedWithAlertsCardsId,
OverviewSteps,
QuickStartSectionCardsId,
SectionId,
} from '../types';
jest.mock('../../../../lib/kibana');
describe('useCheckStepCompleted', () => {
beforeEach(() => {
jest.clearAllMocks();
});
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,
trigger: 'auto_check',
});
});
});
it('does not toggleTaskCompleteStatus if authCheckIfStepCompleted was aborted', async () => {
const mockAutoCheck = jest.fn(({ abortSignal }) => {
abortSignal.abort();
return Promise.resolve(false);
});
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).not.toHaveBeenCalled();
});
});
});

View file

@ -1,84 +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 { useEffect, useRef } from 'react';
import { useKibana } from '../../../../lib/kibana';
import type {
StepId,
CardId,
SectionId,
CheckIfStepCompleted,
ToggleTaskCompleteStatus,
} from '../types';
interface Props {
autoCheckIfStepCompleted?: CheckIfStepCompleted;
cardId: CardId;
indicesExist: boolean;
sectionId: SectionId;
stepId: StepId;
stepTitle?: string;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
}
export const useCheckStepCompleted = ({
autoCheckIfStepCompleted,
cardId,
indicesExist,
sectionId,
stepId,
stepTitle,
toggleTaskCompleteStatus,
}: Props) => {
const {
http: kibanaServicesHttp,
notifications: { toasts },
} = useKibana().services;
const addError = useRef(toasts.addError.bind(toasts)).current;
useEffect(() => {
if (!autoCheckIfStepCompleted) {
return;
}
const abortSignal = new AbortController();
const autoCheckStepCompleted = async () => {
const isDone = await autoCheckIfStepCompleted({
indicesExist,
abortSignal,
kibanaServicesHttp,
onError: (error: Error) => {
addError(error, { title: `Failed to check ${stepTitle ?? stepId} completion.` });
},
});
if (!abortSignal.signal.aborted) {
toggleTaskCompleteStatus({
stepId,
cardId,
sectionId,
undo: !isDone,
trigger: 'auto_check',
});
}
};
autoCheckStepCompleted();
return () => {
abortSignal.abort();
};
}, [
autoCheckIfStepCompleted,
stepId,
cardId,
sectionId,
toggleTaskCompleteStatus,
kibanaServicesHttp,
indicesExist,
addError,
stepTitle,
]);
};

View file

@ -1,14 +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 { useObservable } from 'react-use';
import { useKibana } from '../../../../lib/kibana';
export const useProductTypes = () => {
const { productTypes$ } = useKibana().services.onboarding;
return useObservable(productTypes$, undefined);
};

View file

@ -1,14 +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 { useObservable } from 'react-use';
import { useKibana } from '../../../../lib/kibana';
export const useProjectFeaturesUrl = () => {
const { projectFeaturesUrl$ } = useKibana().services.onboarding;
return useObservable(projectFeaturesUrl$, undefined);
};

View file

@ -1,14 +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 { useObservable } from 'react-use';
import { useKibana } from '../../../../lib/kibana';
export const useProjectsUrl = () => {
const { projectsUrl$ } = useKibana().services.onboarding;
return useObservable(projectsUrl$, undefined);
};

View file

@ -1,42 +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 { 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

@ -1,70 +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 { renderHook } from '@testing-library/react-hooks';
import type { EuiThemeComputed } from '@elastic/eui';
import { useSetUpSections } from './use_setup_sections';
import type { ActiveSections, CardId, ExpandedCardSteps, StepId } from '../types';
import { CreateProjectSteps, QuickStartSectionCardsId, SectionId } from '../types';
const mockEuiTheme: EuiThemeComputed = {
size: {
l: '16px',
base: '20px',
},
colors: {},
font: { weight: { bold: 700 } },
} as EuiThemeComputed;
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set<StepId>([
CreateProjectSteps.createFirstProject,
]),
} as Record<CardId, Set<StepId>>;
describe('useSetUpSections', () => {
const onStepClicked = jest.fn();
const toggleTaskCompleteStatus = jest.fn();
it('should return the sections', () => {
const { result } = renderHook(() => useSetUpSections({ euiTheme: mockEuiTheme }));
const activeSections = {
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 3,
stepsLeft: 1,
},
},
} as ActiveSections;
const sections = result.current.setUpSections({
activeSections,
expandedCardSteps: {} as ExpandedCardSteps,
onStepClicked,
toggleTaskCompleteStatus,
finishedSteps,
});
expect(sections).toHaveLength(1);
});
it('should return no section if no active cards', () => {
const { result } = renderHook(() => useSetUpSections({ euiTheme: mockEuiTheme }));
const activeSections = null;
const sections = result.current.setUpSections({
activeSections,
expandedCardSteps: {} as ExpandedCardSteps,
onStepClicked,
toggleTaskCompleteStatus,
finishedSteps,
});
expect(sections.length).toEqual(0);
});
});

View file

@ -1,138 +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 type { EuiThemeComputed } from '@elastic/eui';
import { EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import React, { useCallback } from 'react';
import { css } from '@emotion/css';
import type {
ActiveSections,
CardId,
ExpandedCardSteps,
ToggleTaskCompleteStatus,
OnStepClicked,
SectionId,
StepId,
} from '../types';
import { CardItem } from '../card_item';
import { getSections } from '../sections';
export const useSetUpSections = ({ euiTheme }: { euiTheme: EuiThemeComputed }) => {
const setUpCards = useCallback(
({
activeSections,
expandedCardSteps,
finishedSteps,
toggleTaskCompleteStatus,
onStepClicked,
sectionId,
}: {
activeSections: ActiveSections | null;
expandedCardSteps: ExpandedCardSteps;
finishedSteps: Record<CardId, Set<StepId>>;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
onStepClicked: OnStepClicked;
sectionId: SectionId;
}) => {
const section = activeSections?.[sectionId];
return section
? Object.values(section)?.map<React.ReactNode>((cardItem) => (
<EuiFlexItem key={cardItem.id}>
<CardItem
activeStepIds={cardItem.activeStepIds}
cardId={cardItem.id}
data-test-subj={cardItem.id}
expandedCardSteps={expandedCardSteps}
finishedSteps={finishedSteps[cardItem.id]}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
onStepClicked={onStepClicked}
sectionId={sectionId}
/>
</EuiFlexItem>
))
: null;
},
[]
);
const setUpSections = useCallback(
({
activeSections,
expandedCardSteps,
finishedSteps,
toggleTaskCompleteStatus,
onStepClicked,
}: {
activeSections: ActiveSections | null;
expandedCardSteps: ExpandedCardSteps;
finishedSteps: Record<CardId, Set<StepId>>;
toggleTaskCompleteStatus: ToggleTaskCompleteStatus;
onStepClicked: OnStepClicked;
}) =>
getSections().reduce<React.ReactNode[]>((acc, currentSection) => {
const cardNodes = setUpCards({
activeSections,
expandedCardSteps,
finishedSteps,
toggleTaskCompleteStatus,
onStepClicked,
sectionId: currentSection.id,
});
if (cardNodes && cardNodes.length > 0) {
acc.push(
<EuiPanel
color="plain"
element="div"
grow={false}
paddingSize="none"
hasShadow={false}
borderRadius="none"
css={css`
margin: ${euiTheme.size.l} 0;
padding-top: 4px;
background-color: ${euiTheme.colors.lightestShade};
`}
key={currentSection.id}
id={currentSection.id}
data-test-subj={`section-${currentSection.id}`}
>
<h2
css={css`
font-size: ${euiTheme.base * 1.375}px;
font-weight: ${euiTheme.font.weight.bold};
`}
>
{currentSection.title}
</h2>
<EuiSpacer size="l" />
<EuiFlexGroup
gutterSize="none"
direction="column"
css={css`
gap: ${euiTheme.size.base};
`}
>
{cardNodes}
</EuiFlexGroup>
</EuiPanel>
);
}
return acc;
}, []),
[
euiTheme.base,
euiTheme.colors.lightestShade,
euiTheme.font.weight.bold,
euiTheme.size.base,
euiTheme.size.l,
setUpCards,
]
);
return { setUpSections };
};

View file

@ -1,389 +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 { renderHook, act } from '@testing-library/react-hooks';
import { useTogglePanel } from './use_toggle_panel';
import type { StepId } from '../types';
import {
QuickStartSectionCardsId,
SectionId,
CreateProjectSteps,
OverviewSteps,
AddAndValidateYourDataCardsId,
AddIntegrationsSteps,
ViewDashboardSteps,
GetStartedWithAlertsCardsId,
ViewAlertsSteps,
EnablePrebuiltRulesSteps,
} from '../types';
import type { SecurityProductTypes } from '../configs';
import { ProductLine } from '../configs';
import { OnboardingStorage } from '../storage';
import * as mockStorage from '../__mocks__/storage';
jest.mock('../storage', () => ({
OnboardingStorage: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
useLocation: jest.fn().mockReturnValue({ hash: '' }),
}));
jest.mock('@kbn/security-solution-navigation', () => ({
useNavigateTo: jest.fn().mockReturnValue({ navigateTo: jest.fn() }),
SecurityPageName: {
landing: 'landing',
},
}));
jest.mock('../../../../lib/kibana', () => ({
useKibana: jest.fn().mockReturnValue({
services: {
telemetry: {
reportOnboardingHubStepOpen: jest.fn(),
reportOnboardingHubStepFinished: jest.fn(),
},
},
}),
}));
describe('useTogglePanel', () => {
const productTypes = [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'complete' },
] as SecurityProductTypes;
const onboardingSteps: StepId[] = [
CreateProjectSteps.createFirstProject,
AddIntegrationsSteps.connectToDataSources,
ViewDashboardSteps.analyzeData,
EnablePrebuiltRulesSteps.enablePrebuiltRules,
ViewAlertsSteps.viewAlerts,
];
const spaceId = 'testSpaceId';
beforeEach(() => {
jest.clearAllMocks();
(OnboardingStorage as jest.Mock).mockImplementation(() => ({
getAllFinishedStepsFromStorage: mockStorage.mockGetAllFinishedStepsFromStorage,
getFinishedStepsFromStorageByCardId: mockStorage.mockGetFinishedStepsFromStorageByCardId,
getActiveProductsFromStorage: mockStorage.mockGetActiveProductsFromStorage,
toggleActiveProductsInStorage: mockStorage.mockToggleActiveProductsInStorage,
resetAllExpandedCardStepsToStorage: mockStorage.mockResetAllExpandedCardStepsToStorage,
addFinishedStepToStorage: mockStorage.mockAddFinishedStepToStorage,
removeFinishedStepFromStorage: mockStorage.mockRemoveFinishedStepFromStorage,
addExpandedCardStepToStorage: mockStorage.mockAddExpandedCardStepToStorage,
removeExpandedCardStepFromStorage: mockStorage.mockRemoveExpandedCardStepFromStorage,
getAllExpandedCardStepsFromStorage: mockStorage.mockGetAllExpandedCardStepsFromStorage,
}));
(mockStorage.mockGetAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
});
(mockStorage.mockGetActiveProductsFromStorage as jest.Mock).mockReturnValue([
ProductLine.security,
ProductLine.cloud,
ProductLine.endpoint,
]);
});
test('should initialize state with correct initial values - when no active products from local storage', () => {
(mockStorage.mockGetActiveProductsFromStorage as jest.Mock).mockReturnValue([]);
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { state } = result.current;
expect(state.activeProducts).toEqual(new Set([ProductLine.security, ProductLine.endpoint]));
expect(state.finishedSteps).toEqual({
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
});
expect(state.activeSections).toEqual(
expect.objectContaining({
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 0,
stepsLeft: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
[SectionId.addAndValidateYourData]: {
[AddAndValidateYourDataCardsId.addIntegrations]: {
id: AddAndValidateYourDataCardsId.addIntegrations,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [AddIntegrationsSteps.connectToDataSources],
},
[AddAndValidateYourDataCardsId.viewDashboards]: {
id: AddAndValidateYourDataCardsId.viewDashboards,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewDashboardSteps.analyzeData],
},
},
[SectionId.getStartedWithAlerts]: {
[GetStartedWithAlertsCardsId.enablePrebuiltRules]: {
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [EnablePrebuiltRulesSteps.enablePrebuiltRules],
},
[GetStartedWithAlertsCardsId.viewAlerts]: {
id: GetStartedWithAlertsCardsId.viewAlerts,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewAlertsSteps.viewAlerts],
},
},
})
);
});
test('should initialize state with correct initial values - when all products active', () => {
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { state } = result.current;
expect(state.activeProducts).toEqual(
new Set([ProductLine.security, ProductLine.cloud, ProductLine.endpoint])
);
expect(state.finishedSteps).toEqual({
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
});
expect(state.activeSections).toEqual(
expect.objectContaining({
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 0,
stepsLeft: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
[SectionId.addAndValidateYourData]: {
[AddAndValidateYourDataCardsId.addIntegrations]: {
id: AddAndValidateYourDataCardsId.addIntegrations,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [AddIntegrationsSteps.connectToDataSources],
},
[AddAndValidateYourDataCardsId.viewDashboards]: {
id: AddAndValidateYourDataCardsId.viewDashboards,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewDashboardSteps.analyzeData],
},
},
[SectionId.getStartedWithAlerts]: {
[GetStartedWithAlertsCardsId.enablePrebuiltRules]: {
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [EnablePrebuiltRulesSteps.enablePrebuiltRules],
},
[GetStartedWithAlertsCardsId.viewAlerts]: {
id: GetStartedWithAlertsCardsId.viewAlerts,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewAlertsSteps.viewAlerts],
},
},
})
);
});
test('should initialize state with correct initial values - when security product active', () => {
(mockStorage.mockGetActiveProductsFromStorage as jest.Mock).mockReturnValue([
ProductLine.security,
]);
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { state } = result.current;
expect(state.activeProducts).toEqual(new Set([ProductLine.security]));
expect(state.finishedSteps).toEqual({
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
});
expect(state.activeSections).toEqual(
expect.objectContaining({
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 0,
stepsLeft: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
[SectionId.addAndValidateYourData]: {
[AddAndValidateYourDataCardsId.addIntegrations]: {
id: AddAndValidateYourDataCardsId.addIntegrations,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [AddIntegrationsSteps.connectToDataSources],
},
[AddAndValidateYourDataCardsId.viewDashboards]: {
id: AddAndValidateYourDataCardsId.viewDashboards,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewDashboardSteps.analyzeData],
},
},
[SectionId.getStartedWithAlerts]: {
[GetStartedWithAlertsCardsId.enablePrebuiltRules]: {
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [EnablePrebuiltRulesSteps.enablePrebuiltRules],
},
[GetStartedWithAlertsCardsId.viewAlerts]: {
id: GetStartedWithAlertsCardsId.viewAlerts,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewAlertsSteps.viewAlerts],
},
},
})
);
});
test('should reset all the card steps in storage when a step is expanded. (As it allows only one step open at a time)', () => {
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { onStepClicked } = result.current;
act(() => {
onStepClicked({
stepId: OverviewSteps.getToKnowElasticSecurity,
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
sectionId: SectionId.quickStart,
isExpanded: true,
trigger: 'click',
});
});
expect(mockStorage.mockResetAllExpandedCardStepsToStorage).toHaveBeenCalledTimes(1);
});
test('should add the current step to storage when it is expanded', () => {
const { result } = renderHook(() =>
useTogglePanel({ productTypes, onboardingSteps, spaceId: 'testSpaceId' })
);
const { onStepClicked } = result.current;
act(() => {
onStepClicked({
stepId: OverviewSteps.getToKnowElasticSecurity,
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
sectionId: SectionId.quickStart,
isExpanded: true,
trigger: 'click',
});
});
expect(mockStorage.mockAddExpandedCardStepToStorage).toHaveBeenCalledTimes(1);
expect(mockStorage.mockAddExpandedCardStepToStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
});
test('should remove the current step from storage when it is collapsed', () => {
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { onStepClicked } = result.current;
act(() => {
onStepClicked({
stepId: OverviewSteps.getToKnowElasticSecurity,
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
sectionId: SectionId.quickStart,
isExpanded: false,
trigger: 'click',
});
});
expect(mockStorage.mockRemoveExpandedCardStepFromStorage).toHaveBeenCalledTimes(1);
expect(mockStorage.mockRemoveExpandedCardStepFromStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
});
test('should call addFinishedStepToStorage when toggleTaskCompleteStatus is executed', () => {
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { toggleTaskCompleteStatus } = result.current;
act(() => {
toggleTaskCompleteStatus({
stepId: OverviewSteps.getToKnowElasticSecurity,
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
sectionId: SectionId.quickStart,
trigger: 'click',
});
});
expect(mockStorage.mockAddFinishedStepToStorage).toHaveBeenCalledTimes(1);
expect(mockStorage.mockAddFinishedStepToStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
});
test('should call removeFinishedStepToStorage when toggleTaskCompleteStatus is executed with undo equals to true', () => {
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { toggleTaskCompleteStatus } = result.current;
act(() => {
toggleTaskCompleteStatus({
stepId: OverviewSteps.getToKnowElasticSecurity,
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
sectionId: SectionId.quickStart,
undo: true,
trigger: 'click',
});
});
expect(mockStorage.mockRemoveFinishedStepFromStorage).toHaveBeenCalledTimes(1);
expect(mockStorage.mockRemoveFinishedStepFromStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity,
onboardingSteps
);
});
test('should call toggleActiveProductsInStorage when onProductSwitchChanged is executed', () => {
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps, spaceId }));
const { onProductSwitchChanged } = result.current;
act(() => {
onProductSwitchChanged({ id: ProductLine.security, label: 'Analytics' });
});
expect(mockStorage.mockToggleActiveProductsInStorage).toHaveBeenCalledTimes(1);
expect(mockStorage.mockToggleActiveProductsInStorage).toHaveBeenCalledWith(
ProductLine.security
);
});
});

View file

@ -1,269 +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 { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useLocation } from 'react-router-dom';
import { useNavigateTo } from '@kbn/security-solution-navigation';
import { SecurityPageName } from '../../../../../../common';
import { OnboardingStorage } from '../storage';
import {
getActiveSectionsInitialStates,
getActiveProductsInitialStates,
getFinishedStepsInitialStates,
reducer,
} from '../reducer';
import type {
Card,
ExpandedCardSteps,
ToggleTaskCompleteStatus,
OnStepClicked,
Step,
Switch,
StepId,
} from '../types';
import { OnboardingActions } from '../types';
import { findCardSectionByStepId } from '../helpers';
import type { SecurityProductTypes } from '../configs';
import { ALL_PRODUCT_LINES, ProductLine } from '../configs';
import { useKibana } from '../../../../lib/kibana';
const syncExpandedCardStepsToStorageFromURL = (
onboardingStorage: OnboardingStorage,
maybeStepId: string
) => {
const { matchedCard, matchedStep } = findCardSectionByStepId(maybeStepId);
const hasStepContent = matchedStep && matchedStep.description;
if (matchedCard && matchedStep && hasStepContent) {
onboardingStorage.resetAllExpandedCardStepsToStorage();
onboardingStorage.addExpandedCardStepToStorage(matchedCard.id, matchedStep.id);
}
};
const syncExpandedCardStepsFromStorageToURL = (
expandedCardSteps: ExpandedCardSteps,
callback: ({
matchedCard,
matchedStep,
}: {
matchedCard: Card | null;
matchedStep: Step | null;
}) => void
) => {
const expandedCardStep = Object.values(expandedCardSteps).find(
(expandedCard) => expandedCard.expandedSteps.length > 0
);
if (expandedCardStep?.expandedSteps[0]) {
const { matchedCard, matchedStep } = findCardSectionByStepId(
expandedCardStep?.expandedSteps[0]
);
callback?.({ matchedCard, matchedStep });
}
};
export const useTogglePanel = ({
productTypes,
onboardingSteps,
spaceId,
}: {
productTypes?: SecurityProductTypes;
onboardingSteps: StepId[];
spaceId: string | undefined;
}) => {
const { telemetry } = useKibana().services;
const { navigateTo } = useNavigateTo();
const { hash: detailName } = useLocation();
const stepIdFromHash = detailName.split('#')[1];
const onboardingStorage = useMemo(() => new OnboardingStorage(spaceId), [spaceId]);
const {
getAllFinishedStepsFromStorage,
getActiveProductsFromStorage,
toggleActiveProductsInStorage,
addExpandedCardStepToStorage,
addFinishedStepToStorage,
removeFinishedStepFromStorage,
removeExpandedCardStepFromStorage,
resetAllExpandedCardStepsToStorage,
getAllExpandedCardStepsFromStorage,
} = onboardingStorage;
const finishedStepsInitialStates = useMemo(
() =>
getFinishedStepsInitialStates({
finishedSteps: getAllFinishedStepsFromStorage(),
}),
[getAllFinishedStepsFromStorage]
);
const activeProductsInitialStates = useMemo(() => {
const activeProductsFromStorage = getActiveProductsInitialStates({
activeProducts: getActiveProductsFromStorage(),
});
return activeProductsFromStorage.size > 0
? activeProductsFromStorage
: productTypes && productTypes.length > 0
? new Set(productTypes.map(({ product_line: productLine }) => ProductLine[productLine]))
: new Set(ALL_PRODUCT_LINES);
}, [getActiveProductsFromStorage, productTypes]);
const {
activeSections: activeSectionsInitialStates,
totalActiveSteps: totalActiveStepsInitialStates,
totalStepsLeft: totalStepsLeftInitialStates,
} = useMemo(
() =>
getActiveSectionsInitialStates({
activeProducts: activeProductsInitialStates,
finishedSteps: finishedStepsInitialStates,
onboardingSteps,
}),
[activeProductsInitialStates, finishedStepsInitialStates, onboardingSteps]
);
const expandedCardsInitialStates: ExpandedCardSteps = useMemo(() => {
if (stepIdFromHash) {
syncExpandedCardStepsToStorageFromURL(onboardingStorage, stepIdFromHash);
}
return getAllExpandedCardStepsFromStorage();
}, [onboardingStorage, getAllExpandedCardStepsFromStorage, stepIdFromHash]);
const [state, dispatch] = useReducer(reducer, {
activeProducts: activeProductsInitialStates,
activeSections: activeSectionsInitialStates,
expandedCardSteps: expandedCardsInitialStates,
finishedSteps: finishedStepsInitialStates,
totalActiveSteps: totalActiveStepsInitialStates,
totalStepsLeft: totalStepsLeftInitialStates,
onboardingSteps,
});
const onStepClicked: OnStepClicked = useCallback(
({ stepId, cardId, isExpanded, trigger }) => {
dispatch({
type: OnboardingActions.ToggleExpandedStep,
payload: { stepId, cardId, isStepExpanded: isExpanded },
});
if (isExpanded) {
// It allows Only One step open at a time
resetAllExpandedCardStepsToStorage();
addExpandedCardStepToStorage(cardId, stepId);
telemetry.reportOnboardingHubStepOpen({
stepId,
trigger,
});
} else {
removeExpandedCardStepFromStorage(cardId, stepId);
}
},
[
addExpandedCardStepToStorage,
removeExpandedCardStepFromStorage,
resetAllExpandedCardStepsToStorage,
telemetry,
]
);
const toggleTaskCompleteStatus: ToggleTaskCompleteStatus = useCallback(
({ stepId, stepLinkId, cardId, sectionId, undo, trigger }) => {
dispatch({
type: undo ? OnboardingActions.RemoveFinishedStep : OnboardingActions.AddFinishedStep,
payload: { stepId, cardId, sectionId },
});
if (undo) {
removeFinishedStepFromStorage(cardId, stepId, state.onboardingSteps);
} else {
addFinishedStepToStorage(cardId, stepId);
telemetry.reportOnboardingHubStepFinished({ stepId, stepLinkId, trigger });
}
},
[addFinishedStepToStorage, removeFinishedStepFromStorage, state.onboardingSteps, telemetry]
);
const onProductSwitchChanged = useCallback(
(section: Switch) => {
dispatch({ type: OnboardingActions.ToggleProduct, payload: { section: section.id } });
toggleActiveProductsInStorage(section.id);
},
[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;
}
// The step is opened by navigation instead of clicking directly on the step. e.g.: clicking a stepLink
// Toggle step and sync the expanded card step to storage & reducer
onStepClicked({
stepId: matchedStep.id,
cardId: matchedCard.id,
sectionId: matchedSection.id,
isExpanded: true,
trigger: 'navigation',
});
navigateTo({
deepLinkId: SecurityPageName.landing,
path: `#${matchedStep.id}`,
});
}
}
}, [navigateTo, onStepClicked, state.expandedCardSteps, stepIdFromHash]);
return {
state,
onStepClicked,
toggleTaskCompleteStatus,
onProductSwitchChanged,
};
};

View file

@ -1,14 +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 { useObservable } from 'react-use';
import { useKibana } from '../../../../lib/kibana';
export const useUsersUrl = () => {
const { usersUrl$ } = useKibana().services.onboarding;
return useObservable(usersUrl$, undefined);
};

View file

@ -1,10 +0,0 @@
<svg width="32" height="28" viewBox="0 0 32 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 14V12H26V14H20Z" fill="#00BFB3"/>
<path d="M12 14V12H18V14H12Z" fill="#00BFB3"/>
<path d="M1 7V5H31V7H1Z" fill="#535766"/>
<path d="M10.7275 15.3139L9.27247 16.6861L5.79651 13L9.27247 9.31393L10.7275 10.6861L8.54551 13L10.7275 15.3139Z" fill="#535766"/>
<path d="M12 19V21H6V19H12Z" fill="#00BFB3"/>
<path d="M20 19V21H14V19H20Z" fill="#00BFB3"/>
<path d="M21.2929 17.7071L22.7071 16.2929L26.4142 20L22.7071 23.7071L21.2929 22.2929L23.5858 20L21.2929 17.7071Z" fill="#535766"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29 0H3C1.34315 0 0 1.34315 0 3V25C0 26.6569 1.34315 28 3 28H29C30.6569 28 32 26.6569 32 25V3C32 1.34315 30.6569 0 29 0ZM2 3C2 2.44772 2.44772 2 3 2H29C29.5523 2 30 2.44772 30 3V25C30 25.5523 29.5523 26 29 26H3C2.44772 26 2 25.5523 2 25V3Z" fill="#535766"/>
</svg>

Before

Width:  |  Height:  |  Size: 901 B

View file

@ -1,18 +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 React, { lazy, Suspense } from 'react';
import { EuiLoadingLogo } from '@elastic/eui';
const OnboardingLazy = lazy(() => import('./onboarding_with_settings'));
const centerLogoStyle = { display: 'flex', margin: 'auto' };
export const Onboarding = ({ indicesExist }: { indicesExist?: boolean }) => (
<Suspense fallback={<EuiLoadingLogo logo="logoSecurity" size="xl" style={centerLogoStyle} />}>
<OnboardingLazy indicesExist={indicesExist} />
</Suspense>
);

View file

@ -1,140 +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 React from 'react';
import { OnboardingComponent } from './onboarding';
import {
AddIntegrationsSteps,
EnablePrebuiltRulesSteps,
OverviewSteps,
ViewAlertsSteps,
ViewDashboardSteps,
} from './types';
import { ProductLine, ProductTier } from './configs';
import type { AppContextTestRender } from '../../../mock/endpoint';
import { createAppRootMockRenderer } from '../../../mock/endpoint';
import { useKibana as mockUseKibana } from '../../../lib/kibana/__mocks__';
const mockedUseKibana = mockUseKibana();
const mockedStorageGet = jest.fn();
const mockedStorageSet = jest.fn();
jest.mock('../../../lib/kibana', () => {
const original = jest.requireActual('../../../lib/kibana');
return {
...original,
useCurrentUser: jest.fn().mockReturnValue({ fullName: 'UserFullName' }),
useKibana: () => ({
mockedUseKibana,
services: {
...mockedUseKibana.services,
storage: {
...mockedUseKibana.services.storage,
get: mockedStorageGet,
set: mockedStorageSet,
},
},
}),
};
});
jest.mock('./toggle_panel');
describe('OnboardingComponent', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let mockedContext: AppContextTestRender;
const props = {
indicesExist: true,
productTypes: [{ product_line: ProductLine.security, product_tier: ProductTier.complete }],
onboardingSteps: [
OverviewSteps.getToKnowElasticSecurity,
AddIntegrationsSteps.connectToDataSources,
ViewDashboardSteps.analyzeData,
EnablePrebuiltRulesSteps.enablePrebuiltRules,
ViewAlertsSteps.viewAlerts,
],
spaceId: 'spaceId',
};
beforeEach(() => {
mockedContext = createAppRootMockRenderer();
render = () => (renderResult = mockedContext.render(<OnboardingComponent {...props} />));
});
afterEach(() => {
jest.clearAllMocks();
});
it('should render page title, subtitle, and description', () => {
render();
const pageTitle = renderResult.getByText('Hi UserFullName!');
const subtitle = renderResult.getByText(`Welcome to Elastic Security`);
const description = renderResult.getByText(`Follow these steps to set up your workspace.`);
expect(pageTitle).toBeInTheDocument();
expect(subtitle).toBeInTheDocument();
expect(description).toBeInTheDocument();
});
it('should render dataIngestionHubHeader and TogglePanel', () => {
render();
const dataIngestionHubHeader = renderResult.getByTestId('data-ingestion-hub-header');
const togglePanel = renderResult.getByTestId('toggle-panel');
expect(dataIngestionHubHeader).toBeInTheDocument();
expect(togglePanel).toBeInTheDocument();
});
describe('AVC 2024 Results banner', () => {
beforeEach(() => {
mockedStorageGet.mockReturnValue(true);
});
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});
it('should render on the page', () => {
render();
expect(renderResult.getByTestId('avcResultsBanner')).toBeTruthy();
});
it('should link to the blog post', () => {
render();
expect(renderResult.getByTestId('avcReadTheBlog')).toHaveAttribute(
'href',
'https://www.elastic.co/blog/elastic-av-comparatives-business-security-test'
);
});
it('on closing the callout should store dismissal state in local storage', () => {
render();
renderResult.getByTestId('euiDismissCalloutButton').click();
expect(renderResult.queryByTestId('avcResultsBanner')).toBeNull();
expect(mockedStorageSet).toHaveBeenCalledWith('securitySolution.showAvcBanner', false);
});
it('should stay dismissed if it has been closed once', () => {
mockedStorageGet.mockReturnValueOnce(false);
render();
expect(renderResult.queryByTestId('avcResultsBanner')).toBeNull();
});
it('should not be shown if the current date is January 1, 2025', () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T05:00:00.000Z'));
render();
expect(renderResult.queryByTestId('avcResultsBanner')).toBeNull();
jest.useRealTimers();
});
it('should be shown if the current date is before January 1, 2025', () => {
jest.useFakeTimers().setSystemTime(new Date('2024-12-31T05:00:00.000Z'));
render();
expect(renderResult.queryByTestId('avcResultsBanner')).toBeTruthy();
});
});
});

View file

@ -1,129 +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 React, { useCallback, useMemo, useState } from 'react';
import { AVCResultsBanner2024, useIsStillYear2024 } from '@kbn/avc-banner';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { TogglePanel } from './toggle_panel';
import { useTogglePanel } from './hooks/use_toggle_panel';
import { Progress } from './progress_bar';
import { StepContextProvider } from './context/step_context';
import { CONTENT_WIDTH } from './helpers';
import { DataIngestionHubHeader } from './data_ingestion_hub_header';
import { Footer } from './footer';
import { useScrollToHash } from './hooks/use_scroll';
import type { SecurityProductTypes } from './configs';
import { ProductLine } from './configs';
import type { StepId } from './types';
import { useOnboardingStyles } from './styles/onboarding.styles';
import { useKibana } from '../../../lib/kibana';
import type { OnboardingHubStepLinkClickedParams } from '../../../lib/telemetry/events/onboarding/types';
interface OnboardingProps {
indicesExist?: boolean;
productTypes: SecurityProductTypes | undefined;
onboardingSteps: StepId[];
spaceId: string;
}
export const OnboardingComponent: React.FC<OnboardingProps> = ({
indicesExist,
productTypes,
onboardingSteps,
spaceId,
}) => {
const {
onStepClicked,
toggleTaskCompleteStatus,
state: {
activeProducts,
activeSections,
finishedSteps,
totalActiveSteps,
totalStepsLeft,
expandedCardSteps,
},
} = useTogglePanel({ productTypes, onboardingSteps, spaceId });
const productTier = useMemo(
() =>
productTypes?.find((product) => product.product_line === ProductLine.security)?.product_tier,
[productTypes]
);
const { wrapperStyles, headerSectionStyles, progressSectionStyles, stepsSectionStyles } =
useOnboardingStyles();
const { telemetry, storage } = useKibana().services;
const onStepLinkClicked = useCallback(
(params: OnboardingHubStepLinkClickedParams) => {
telemetry.reportOnboardingHubStepLinkClicked(params);
},
[telemetry]
);
const [showAVCBanner, setShowAVCBanner] = useState(
storage.get('securitySolution.showAvcBanner') ?? true
);
const onBannerDismiss = useCallback(() => {
setShowAVCBanner(false);
storage.set('securitySolution.showAvcBanner', false);
}, [storage]);
useScrollToHash();
return (
<div className={wrapperStyles}>
{useIsStillYear2024() && showAVCBanner && (
<KibanaPageTemplate.Section paddingSize="none">
<AVCResultsBanner2024 onDismiss={onBannerDismiss} />
</KibanaPageTemplate.Section>
)}
<KibanaPageTemplate.Section
className={headerSectionStyles}
restrictWidth={CONTENT_WIDTH}
paddingSize="xl"
>
<DataIngestionHubHeader />
</KibanaPageTemplate.Section>
<KibanaPageTemplate.Section
restrictWidth={CONTENT_WIDTH}
paddingSize="none"
className={progressSectionStyles}
>
<Progress
totalActiveSteps={totalActiveSteps}
totalStepsLeft={totalStepsLeft}
productTier={productTier}
/>
</KibanaPageTemplate.Section>
<KibanaPageTemplate.Section
bottomBorder="extended"
grow={true}
restrictWidth={CONTENT_WIDTH}
paddingSize="none"
className={stepsSectionStyles}
>
<StepContextProvider
expandedCardSteps={expandedCardSteps}
finishedSteps={finishedSteps}
indicesExist={!!indicesExist}
onStepClicked={onStepClicked}
onStepLinkClicked={onStepLinkClicked}
toggleTaskCompleteStatus={toggleTaskCompleteStatus}
>
<TogglePanel activeProducts={activeProducts} activeSections={activeSections} />
</StepContextProvider>
</KibanaPageTemplate.Section>
<KibanaPageTemplate.Section grow={true} restrictWidth={CONTENT_WIDTH} paddingSize="none">
<Footer />
</KibanaPageTemplate.Section>
</div>
);
};
export const Onboarding = React.memo(OnboardingComponent);

View file

@ -1,43 +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 React from 'react';
import { render } from '@testing-library/react';
import { OnboardingWithSettings } from './onboarding_with_settings';
import { useAvailableSteps } from './hooks/use_available_steps';
import { useSpaceId } from '../../../hooks/use_space_id';
const useAvailableStepsMock = useAvailableSteps as jest.Mock;
const useSpaceIdMock = useSpaceId as jest.Mock;
jest.mock('./onboarding');
jest.mock('../../../hooks/use_space_id', () => ({
useSpaceId: jest.fn().mockReturnValue('mockSpaceId'),
}));
jest.mock('./hooks/use_available_steps', () => ({
useAvailableSteps: jest.fn().mockReturnValue([]),
}));
jest.mock('./hooks/use_product_types');
describe('OnboardingWithSettings', () => {
it('should render Onboarding component', () => {
const { getByTestId } = render(<OnboardingWithSettings />);
expect(getByTestId('onboarding')).toBeInTheDocument();
});
it('should not render Onboarding component when onboardingSteps is null', () => {
useAvailableStepsMock.mockReturnValue(null);
const { queryByTestId } = render(<OnboardingWithSettings />);
expect(queryByTestId('onboarding')).toBeNull();
});
it('should not render Onboarding component when spaceId is null', () => {
useSpaceIdMock.mockReturnValue(undefined);
const { queryByTestId } = render(<OnboardingWithSettings />);
expect(queryByTestId('onboarding')).toBeNull();
});
});

View file

@ -1,42 +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 React from 'react';
import { useSpaceId } from '../../../hooks/use_space_id';
import { useAvailableSteps } from './hooks/use_available_steps';
import { useProductTypes } from './hooks/use_product_types';
import { Onboarding } from './onboarding';
const OnboardingWithSettingsComponent: React.FC<{ indicesExist?: boolean }> = ({
indicesExist,
}) => {
const productTypes = useProductTypes();
const onboardingSteps = useAvailableSteps();
const spaceId = useSpaceId();
/* spaceId returns undefined if the space is loading.
** We render the onboarding component only when spaceId is ready
** to make sure it reads the local storage data with the correct spaceId.
*/
if (!onboardingSteps || !spaceId) {
return null;
}
return (
<Onboarding
indicesExist={indicesExist}
productTypes={productTypes}
onboardingSteps={onboardingSteps}
spaceId={spaceId}
/>
);
};
export const OnboardingWithSettings = React.memo(OnboardingWithSettingsComponent);
// eslint-disable-next-line import/no-default-export
export default OnboardingWithSettings;

View file

@ -1,74 +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 React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { ProductSwitch } from './product_switch';
import type { EuiThemeComputed } from '@elastic/eui';
import { ProductLine } from './configs';
describe('ProductSwitch', () => {
const onProductSwitchChangedMock = jest.fn();
const mockEuiTheme = { base: 16, size: { xs: '4px' } } as EuiThemeComputed;
afterEach(() => {
jest.clearAllMocks();
});
it('should render product switches with correct labels', () => {
const { getByText } = render(
<ProductSwitch
onProductSwitchChanged={onProductSwitchChangedMock}
activeProducts={new Set()}
euiTheme={mockEuiTheme}
/>
);
const analyticsSwitch = getByText('Analytics');
const cloudSwitch = getByText('Cloud Security');
const endpointSwitch = getByText('Endpoint Security');
expect(analyticsSwitch).toBeInTheDocument();
expect(cloudSwitch).toBeInTheDocument();
expect(endpointSwitch).toBeInTheDocument();
});
it('should call onProductSwitchChanged when a switch is toggled', () => {
const { getByText } = render(
<ProductSwitch
onProductSwitchChanged={onProductSwitchChangedMock}
activeProducts={new Set()}
euiTheme={mockEuiTheme}
/>
);
const analyticsSwitch = getByText('Analytics');
fireEvent.click(analyticsSwitch);
expect(onProductSwitchChangedMock).toHaveBeenCalledWith(
expect.objectContaining({ id: 'security' })
);
});
it('should have checked switches for activeProducts', () => {
const activeProducts = new Set([ProductLine.security, ProductLine.endpoint]);
const { getByTestId } = render(
<ProductSwitch
onProductSwitchChanged={onProductSwitchChangedMock}
activeProducts={activeProducts}
euiTheme={mockEuiTheme}
/>
);
const analyticsSwitch = getByTestId('security');
const cloudSwitch = getByTestId('cloud');
const endpointSwitch = getByTestId('endpoint');
expect(analyticsSwitch).toHaveAttribute('aria-checked', 'true');
expect(cloudSwitch).toHaveAttribute('aria-checked', 'false');
expect(endpointSwitch).toHaveAttribute('aria-checked', 'true');
});
});

View file

@ -1,80 +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 { EuiPanel, EuiSwitch, EuiText, EuiTitle, type EuiThemeComputed } from '@elastic/eui';
import { css } from '@emotion/css';
import React, { useMemo } from 'react';
import { ProductLine } from './configs';
import * as i18n from './translations';
import type { Switch } from './types';
const switches: Switch[] = [
{
id: ProductLine.security,
label: i18n.ANALYTICS_SWITCH_LABEL,
},
{
id: ProductLine.cloud,
label: i18n.CLOUD_SWITCH_LABEL,
},
{
id: ProductLine.endpoint,
label: i18n.ENDPOINT_SWITCH_LABEL,
},
];
const ProductSwitchComponent: React.FC<{
onProductSwitchChanged: (item: Switch) => void;
activeProducts: Set<ProductLine>;
euiTheme: EuiThemeComputed;
}> = ({ onProductSwitchChanged, activeProducts, euiTheme }) => {
const switchNodes = useMemo(
() =>
switches.map((item) => (
<EuiSwitch
key={item.id}
data-test-subj={item.id}
label={item.label}
onChange={() => onProductSwitchChanged(item)}
css={css`
padding-left: ${euiTheme.base * 0.625}px;
`}
checked={activeProducts.has(item.id)}
/>
)),
[activeProducts, euiTheme.base, onProductSwitchChanged]
);
return (
<EuiPanel
data-test-subj="product-switch"
color="plain"
element="div"
grow={false}
paddingSize="none"
hasShadow={false}
css={css`
padding: ${euiTheme.base * 1.25}px 0;
`}
borderRadius="none"
>
<EuiTitle
size="xxs"
css={css`
padding-right: ${euiTheme.size.xs};
`}
>
<strong>{i18n.TOGGLE_PANEL_TITLE}</strong>
</EuiTitle>
<EuiText size="s" className="eui-displayInline">
{switchNodes}
</EuiText>
</EuiPanel>
);
};
ProductSwitchComponent.displayName = 'ProductSwitchComponent';
export const ProductSwitch = ProductSwitchComponent;

View file

@ -1,46 +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, EuiProgress, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { PROGRESS_TRACKER_LABEL } from './translations';
import { useProgressBarStyles } from './styles/progress_bar.style';
import type { ProductTier } from './configs';
const ProgressComponent: React.FC<{
productTier: ProductTier | undefined;
totalActiveSteps: number | null;
totalStepsLeft: number | null;
}> = ({ productTier, totalActiveSteps, totalStepsLeft }) => {
const stepsDone =
totalActiveSteps != null && totalStepsLeft != null ? totalActiveSteps - totalStepsLeft : null;
const { textStyle } = useProgressBarStyles();
return (
<EuiFlexGroup alignItems="center" gutterSize="s">
{totalActiveSteps != null && totalStepsLeft != null && stepsDone != null && (
<EuiFlexItem grow={true}>
<EuiProgress
value={stepsDone}
max={totalActiveSteps}
size="m"
label={
<span>
<span className={textStyle}>{PROGRESS_TRACKER_LABEL}</span>
<EuiSpacer size="s" />
</span>
}
valueText={<span className={textStyle}>{`${stepsDone}/${totalActiveSteps}`}</span>}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
export const Progress = React.memo(ProgressComponent);

View file

@ -1,242 +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 { ProductLine } from './configs';
import { setupActiveSections } from './helpers';
import {
reducer,
getFinishedStepsInitialStates,
getActiveProductsInitialStates,
getActiveSectionsInitialStates,
} from './reducer';
import type {
AddFinishedStepAction,
CardId,
ExpandedCardSteps,
StepId,
ToggleProductAction,
} from './types';
import {
AddAndValidateYourDataCardsId,
AddIntegrationsSteps,
CreateProjectSteps,
EnablePrebuiltRulesSteps,
OnboardingActions,
GetStartedWithAlertsCardsId,
OverviewSteps,
QuickStartSectionCardsId,
SectionId,
ViewAlertsSteps,
ViewDashboardSteps,
} from './types';
const onboardingSteps = [
CreateProjectSteps.createFirstProject,
AddIntegrationsSteps.connectToDataSources,
ViewDashboardSteps.analyzeData,
EnablePrebuiltRulesSteps.enablePrebuiltRules,
ViewAlertsSteps.viewAlerts,
];
describe('reducer', () => {
it('should toggle section correctly', () => {
const activeProducts = new Set([ProductLine.security]);
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
} as Record<CardId, Set<StepId>>;
const { activeSections, totalStepsLeft, totalActiveSteps } = setupActiveSections(
finishedSteps,
activeProducts,
[OverviewSteps.getToKnowElasticSecurity]
);
const initialState = {
activeProducts: new Set([ProductLine.security]),
finishedSteps,
activeSections,
totalStepsLeft,
totalActiveSteps,
expandedCardSteps: {} as ExpandedCardSteps,
onboardingSteps,
};
const action: ToggleProductAction = {
type: OnboardingActions.ToggleProduct,
payload: { section: ProductLine.security },
};
const nextState = reducer(initialState, action);
expect(nextState.activeProducts.has(ProductLine.security)).toBe(false);
expect(nextState.activeSections).toBeNull();
});
it('should add a finished step correctly', () => {
const activeProducts = new Set([ProductLine.security]);
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
} as Record<CardId, Set<StepId>>;
const { activeSections, totalStepsLeft, totalActiveSteps } = setupActiveSections(
finishedSteps,
activeProducts,
onboardingSteps
);
const initialState = {
activeProducts: new Set([ProductLine.security]),
finishedSteps,
activeSections,
totalStepsLeft,
totalActiveSteps,
expandedCardSteps: {} as ExpandedCardSteps,
onboardingSteps,
};
const action: AddFinishedStepAction = {
type: OnboardingActions.AddFinishedStep,
payload: {
cardId: QuickStartSectionCardsId.watchTheOverviewVideo,
stepId: OverviewSteps.getToKnowElasticSecurity,
sectionId: SectionId.quickStart,
},
};
const nextState = reducer(initialState, action);
expect(nextState.finishedSteps[QuickStartSectionCardsId.watchTheOverviewVideo]).toEqual(
new Set([OverviewSteps.getToKnowElasticSecurity])
);
expect(nextState.activeSections).toEqual({
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 0,
stepsLeft: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
[SectionId.addAndValidateYourData]: {
[AddAndValidateYourDataCardsId.addIntegrations]: {
id: AddAndValidateYourDataCardsId.addIntegrations,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [AddIntegrationsSteps.connectToDataSources],
},
[AddAndValidateYourDataCardsId.viewDashboards]: {
id: AddAndValidateYourDataCardsId.viewDashboards,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewDashboardSteps.analyzeData],
},
},
[SectionId.getStartedWithAlerts]: {
[GetStartedWithAlertsCardsId.enablePrebuiltRules]: {
id: GetStartedWithAlertsCardsId.enablePrebuiltRules,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [EnablePrebuiltRulesSteps.enablePrebuiltRules],
},
[GetStartedWithAlertsCardsId.viewAlerts]: {
id: GetStartedWithAlertsCardsId.viewAlerts,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewAlertsSteps.viewAlerts],
},
},
});
});
});
describe('getFinishedStepsInitialStates', () => {
it('should return the initial states of finished steps correctly', () => {
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
[QuickStartSectionCardsId.watchTheOverviewVideo]: [],
} as unknown as Record<CardId, StepId[]>;
const initialStates = getFinishedStepsInitialStates({ finishedSteps });
expect(initialStates[QuickStartSectionCardsId.createFirstProject]).toEqual(
new Set([CreateProjectSteps.createFirstProject])
);
expect(initialStates[QuickStartSectionCardsId.watchTheOverviewVideo]).toEqual(new Set([]));
});
});
describe('getActiveProductsInitialStates', () => {
it('should return the initial states of active sections correctly', () => {
const activeProducts = [ProductLine.security];
const initialStates = getActiveProductsInitialStates({ activeProducts });
expect(initialStates.has(ProductLine.security)).toBe(true);
});
});
describe('getActiveSectionsInitialStates', () => {
it('should return the initial states of active cards correctly', () => {
const activeProducts = new Set([ProductLine.security]);
const finishedSteps = {
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
} as unknown as Record<CardId, Set<StepId>>;
const {
activeSections: initialStates,
totalActiveSteps,
totalStepsLeft,
} = getActiveSectionsInitialStates({
activeProducts,
finishedSteps,
onboardingSteps,
});
expect(initialStates).toEqual({
[SectionId.quickStart]: {
[QuickStartSectionCardsId.createFirstProject]: {
id: QuickStartSectionCardsId.createFirstProject,
timeInMins: 0,
stepsLeft: 0,
activeStepIds: [CreateProjectSteps.createFirstProject],
},
},
[SectionId.addAndValidateYourData]: {
[AddAndValidateYourDataCardsId.addIntegrations]: {
id: AddAndValidateYourDataCardsId.addIntegrations,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [AddIntegrationsSteps.connectToDataSources],
},
[AddAndValidateYourDataCardsId.viewDashboards]: {
id: AddAndValidateYourDataCardsId.viewDashboards,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewDashboardSteps.analyzeData],
},
},
[SectionId.getStartedWithAlerts]: {
[GetStartedWithAlertsCardsId.enablePrebuiltRules]: {
id: GetStartedWithAlertsCardsId.enablePrebuiltRules,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [EnablePrebuiltRulesSteps.enablePrebuiltRules],
},
[GetStartedWithAlertsCardsId.viewAlerts]: {
id: GetStartedWithAlertsCardsId.viewAlerts,
timeInMins: 0,
stepsLeft: 1,
activeStepIds: [ViewAlertsSteps.viewAlerts],
},
},
});
expect(totalActiveSteps).toEqual(5);
expect(totalStepsLeft).toEqual(4);
});
});

View file

@ -1,170 +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 type { ProductLine } from './configs';
import { setupActiveSections, updateActiveSections } from './helpers';
import type { ExpandedCardSteps, ReducerActions } from './types';
import { type CardId, type StepId, type TogglePanelReducer, OnboardingActions } from './types';
export const reducer = (state: TogglePanelReducer, action: ReducerActions): TogglePanelReducer => {
if (action.type === OnboardingActions.ToggleProduct) {
const activeProducts = new Set([...state.activeProducts]);
if (activeProducts.has(action.payload.section)) {
activeProducts.delete(action.payload.section);
} else {
activeProducts.add(action.payload.section);
}
const { activeSections, totalStepsLeft, totalActiveSteps } = setupActiveSections(
state.finishedSteps,
activeProducts,
state.onboardingSteps
);
return {
...state,
activeProducts,
activeSections,
totalStepsLeft,
totalActiveSteps,
};
}
if (action.type === OnboardingActions.AddFinishedStep) {
const finishedSteps = {
...state.finishedSteps,
[action.payload.cardId]: state.finishedSteps[action.payload.cardId]
? new Set([...state.finishedSteps[action.payload.cardId]])
: new Set(),
};
finishedSteps[action.payload.cardId].add(action.payload.stepId);
const { activeSections, totalStepsLeft, totalActiveSteps } = updateActiveSections({
activeProducts: state.activeProducts,
activeSections: state.activeSections,
cardId: action.payload.cardId,
finishedSteps,
sectionId: action.payload.sectionId,
onboardingSteps: state.onboardingSteps,
});
return {
...state,
finishedSteps,
activeSections,
totalStepsLeft,
totalActiveSteps,
};
}
if (action.type === OnboardingActions.RemoveFinishedStep) {
const finishedSteps = {
...state.finishedSteps,
[action.payload.cardId]: state.finishedSteps[action.payload.cardId]
? new Set([...state.finishedSteps[action.payload.cardId]])
: new Set(),
};
finishedSteps[action.payload.cardId].delete(action.payload.stepId);
const { activeSections, totalStepsLeft, totalActiveSteps } = updateActiveSections({
activeProducts: state.activeProducts,
activeSections: state.activeSections,
cardId: action.payload.cardId,
finishedSteps,
sectionId: action.payload.sectionId,
onboardingSteps: state.onboardingSteps,
});
return {
...state,
finishedSteps,
activeSections,
totalStepsLeft,
totalActiveSteps,
};
}
if (
action.type === OnboardingActions.ToggleExpandedStep &&
action.payload.isStepExpanded != null
) {
// It allows Only One step open at a time
const expandedSteps = new Set<StepId>();
if (action.payload.isStepExpanded === true && action.payload.stepId != null) {
return {
...state,
expandedCardSteps: Object.entries(state.expandedCardSteps).reduce((acc, [cardId, card]) => {
if (cardId === action.payload.cardId) {
expandedSteps.add(action.payload.stepId);
acc[action.payload.cardId] = {
expandedSteps: [...expandedSteps],
isExpanded: true,
};
} else {
// Remove all other expanded steps in other cards
acc[cardId as CardId] = {
expandedSteps: [],
isExpanded: false,
};
}
return acc;
}, {} as ExpandedCardSteps),
};
}
if (action.payload.isStepExpanded === false) {
expandedSteps.delete(action.payload.stepId);
return {
...state,
expandedCardSteps: {
...state.expandedCardSteps,
[action.payload.cardId]: {
expandedSteps: [],
isExpanded: false,
},
},
};
}
}
return state;
};
export const getFinishedStepsInitialStates = ({
finishedSteps,
}: {
finishedSteps: Record<CardId, StepId[]>;
}): Record<CardId, Set<StepId>> => {
const initialStates = Object.entries(finishedSteps).reduce((acc, [cardId, stepIdsByCard]) => {
if (stepIdsByCard) {
acc[cardId] = new Set(stepIdsByCard);
}
return acc;
}, {} as Record<string, Set<StepId>>);
return initialStates;
};
export const getActiveProductsInitialStates = ({
activeProducts,
}: {
activeProducts: ProductLine[];
}) => new Set(activeProducts);
export const getActiveSectionsInitialStates = ({
activeProducts,
finishedSteps,
onboardingSteps,
}: {
activeProducts: Set<ProductLine>;
finishedSteps: Record<CardId, Set<StepId>>;
onboardingSteps: StepId[];
}) => setupActiveSections(finishedSteps, activeProducts, onboardingSteps);

View file

@ -1,151 +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 React from 'react';
import type { Step, StepId } from './types';
import {
SectionId,
QuickStartSectionCardsId,
OverviewSteps,
type Section,
AddIntegrationsSteps,
ViewDashboardSteps,
AddAndValidateYourDataCardsId,
GetStartedWithAlertsCardsId,
CreateProjectSteps,
EnablePrebuiltRulesSteps,
ViewAlertsSteps,
} from './types';
import * as i18n from './translations';
import { AddIntegrationButtons } from './step_links/add_integration_buttons';
import { AlertsButton } from './step_links/alerts_link';
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/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 = [
{
id: CreateProjectSteps.createFirstProject,
title: i18n.CREATE_PROJECT_TITLE,
icon: { type: 'addDataApp', size: 'xl' as const },
description: [i18n.CREATE_PROJECT_DESCRIPTION, <ManageProjectsButton />],
splitPanel: <CreateProjectImage />,
},
];
export const overviewVideoSteps = [
{
icon: { type: overviewVideo, size: 'xl' as const },
title: i18n.WATCH_VIDEO_TITLE,
id: OverviewSteps.getToKnowElasticSecurity,
description: [<OverviewVideoDescription />],
splitPanel: <Video />,
},
];
export const addIntegrationsSteps: Array<Step<AddIntegrationsSteps.connectToDataSources>> = [
{
icon: { type: 'fleetApp', size: 'xl' as const },
id: AddIntegrationsSteps.connectToDataSources,
title: i18n.ADD_INTEGRATIONS_TITLE,
description: [i18n.ADD_INTEGRATIONS_DESCRIPTION, <AddIntegrationButtons />],
splitPanel: <AddIntegrationsImage />,
autoCheckIfStepCompleted: autoCheckAddIntegrationsStepCompleted,
},
];
export const viewDashboardSteps = [
{
id: ViewDashboardSteps.analyzeData,
icon: { type: 'dashboardApp', size: 'xl' as const },
title: i18n.VIEW_DASHBOARDS,
description: [i18n.VIEW_DASHBOARDS_DESCRIPTION, <DashboardButton />],
splitPanel: <ViewDashboardImage />,
},
];
export const enablePrebuildRuleSteps: Array<Step<EnablePrebuiltRulesSteps.enablePrebuiltRules>> = [
{
title: i18n.ENABLE_RULES,
icon: { type: 'advancedSettingsApp', size: 'xl' as const },
id: EnablePrebuiltRulesSteps.enablePrebuiltRules,
description: [i18n.ENABLE_RULES_DESCRIPTION, <AddElasticRulesButton />],
splitPanel: <EnableRuleImage />,
autoCheckIfStepCompleted: autoCheckPrebuildRuleStepCompleted,
},
];
export const viewAlertSteps = [
{
icon: { type: 'watchesApp', size: 'xl' as const },
title: i18n.VIEW_ALERTS_TITLE,
id: ViewAlertsSteps.viewAlerts,
description: [i18n.VIEW_ALERTS_DESCRIPTION, <AlertsButton />],
splitPanel: <ViewAlertsImage />,
},
];
export const sections: Section[] = [
{
id: SectionId.quickStart,
title: i18n.SECTION_1_TITLE,
cards: [
{
id: QuickStartSectionCardsId.createFirstProject,
steps: createProjectSteps,
hideSteps: true,
},
],
},
{
id: SectionId.addAndValidateYourData,
title: i18n.SECTION_2_TITLE,
cards: [
{
id: AddAndValidateYourDataCardsId.addIntegrations,
steps: addIntegrationsSteps,
},
{
id: AddAndValidateYourDataCardsId.viewDashboards,
steps: viewDashboardSteps,
},
],
},
{
id: SectionId.getStartedWithAlerts,
title: i18n.SECTION_3_TITLE,
cards: [
{
id: GetStartedWithAlertsCardsId.enablePrebuiltRules,
steps: enablePrebuildRuleSteps,
},
{
id: GetStartedWithAlertsCardsId.viewAlerts,
steps: viewAlertSteps,
},
],
},
];
export const getSections = () => sections;
export const getCardById = (stepId: StepId) => {
const cards = sections.flatMap((s) => s.cards);
return cards.find((c) => c.steps?.find((step) => stepId === step.id));
};

View file

@ -1,11 +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 React from 'react';
export const AddIntegrationCallout = jest
.fn()
.mockReturnValue(<div data-test-subj="add-integration-callout" />);

View file

@ -1,53 +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 React, { useCallback } from 'react';
import { LinkButton } from '@kbn/security-solution-navigation/links';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import {
AddAndValidateYourDataCardsId,
AddIntegrationsSteps,
EnablePrebuiltRulesSteps,
} from '../types';
import { useStepContext } from '../context/step_context';
import { AddIntegrationCallout } from './add_integration_callout';
import { ADD_ELASTIC_RULES, ADD_ELASTIC_RULES_CALLOUT_TITLE } from './translations';
const AddElasticRulesButtonComponent = () => {
const { finishedSteps, onStepLinkClicked } = useStepContext();
const isIntegrationsStepComplete = finishedSteps[
AddAndValidateYourDataCardsId.addIntegrations
]?.has(AddIntegrationsSteps.connectToDataSources);
const onClick = useCallback(() => {
onStepLinkClicked({
originStepId: EnablePrebuiltRulesSteps.enablePrebuiltRules,
stepLinkId: SecurityPageName.rules,
});
}, [onStepLinkClicked]);
return (
<>
{!isIntegrationsStepComplete && (
<AddIntegrationCallout
stepName={ADD_ELASTIC_RULES_CALLOUT_TITLE}
stepId={EnablePrebuiltRulesSteps.enablePrebuiltRules}
/>
)}
<LinkButton
id={SecurityPageName.rules}
fill
className="step-paragraph"
disabled={!isIntegrationsStepComplete}
onClick={onClick}
>
{ADD_ELASTIC_RULES}
</LinkButton>
</>
);
};
export const AddElasticRulesButton = React.memo(AddElasticRulesButtonComponent);

View file

@ -1,194 +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 React, { useCallback, type SVGProps } from 'react';
import { i18n } from '@kbn/i18n';
import { LinkButton } from '@kbn/security-solution-navigation/links';
import type { IconType } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiText, EuiTitle } from '@elastic/eui';
import { useKibana } from '../../../../lib/kibana/kibana_react';
import { AddIntegrationsSteps } from '../types';
import { useStepContext } from '../context/step_context';
import { IntegrationsPageName } from './types';
const SEE_INTEGRATIONS = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.seeIntegrationsButton',
{ defaultMessage: 'See integrations' }
);
const ADD_CLOUD_INTEGRATIONS_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.title',
{ defaultMessage: 'Add Cloud Security data' }
);
const ADD_CLOUD_INTEGRATIONS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.description',
{ defaultMessage: 'Cloud-specific security integrations' }
);
const ADD_EDR_XDR_INTEGRATIONS_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.title',
{ defaultMessage: 'Add EDR/XDR data' }
);
const ADD_EDR_XDR_INTEGRATIONS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.description',
{ defaultMessage: 'EDR/XDR-specific security integrations' }
);
const ADD_ALL_INTEGRATIONS_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.title',
{ defaultMessage: 'All security integrations' }
);
const ADD_ALL_INTEGRATIONS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.description',
{ defaultMessage: 'The full set of security integrations' }
);
const CloudIntegrationsIcon: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5.08008 18.7995C2.26902 18.7676 0 16.4581 0 13.6142C0 10.7504 2.30088 8.42882 5.13916 8.42882C5.15374 8.42882 5.16831 8.42888 5.18287 8.429C5.8371 4.10905 9.53542 0.799805 13.9998 0.799805C18.4641 0.799805 22.1624 4.10905 22.8167 8.429C22.8312 8.42888 22.8458 8.42882 22.8604 8.42882C25.6987 8.42882 27.9995 10.7504 27.9995 13.6142C27.9995 16.4581 25.7305 18.7676 22.9195 18.7995H5.08008Z"
fill="#343741"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.00012 16.3996C12.1797 16.3996 17.9999 10.5794 17.9999 3.39986C17.9999 2.82316 17.9624 2.25523 17.8896 1.69839C16.714 1.12268 15.3942 0.799805 13.9998 0.799805C9.53542 0.799805 5.8371 4.10905 5.18287 8.429L5.13916 8.42882C2.30088 8.42882 0 10.7504 0 13.6142C0 14.2989 0.131541 14.9526 0.370513 15.5511C1.80873 16.0994 3.36936 16.3996 5.00012 16.3996Z"
fill="#0077CC"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22.8227 8.42887L22.8608 8.42871C25.6991 8.42871 28 10.7503 28 13.6141C28 16.4579 25.731 18.7675 22.9199 18.7994H16.8138C17.012 14.4375 19.3597 10.6359 22.8227 8.42887Z"
fill="#00BFB3"
/>
<path
d="M23.6655 22.1415L23.9999 22.2602L24.3344 22.1415C24.7834 21.9821 25.2825 21.6651 25.7452 21.3081C26.2247 20.9382 26.7305 20.4743 27.194 19.9624C27.6559 19.4523 28.0945 18.874 28.4227 18.2721C28.7447 17.6815 28.9999 16.9964 28.9999 16.293V11.1992V10.1992H27.9999H20H19V11.1992V16.293C19 16.9964 19.2552 17.6815 19.5772 18.2721C19.9054 18.874 20.344 19.4523 20.8058 19.9624C21.2694 20.4743 21.7751 20.9382 22.2546 21.3081C22.7173 21.6651 23.2165 21.9821 23.6655 22.1415Z"
fill="#FA744E"
stroke="#F7F8FC"
strokeWidth="2"
/>
</svg>
);
const EdrXdrIntegrationsIcon: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_3269_21193)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.5 11.9997L4.404 1.68421C3.38775 -0.0370402 0.75 0.68446 0.75 2.68321V21.3162C0.75 23.315 3.38775 24.0365 4.404 22.3152L10.5 11.9997Z"
fill="#F04E98"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.1133 12L12.4376 13.1445L6.34156 23.46C6.22681 23.6535 6.09706 23.8305 5.95831 24H12.8403C14.6508 24 16.3331 23.0677 17.2923 21.5325L23.2503 12H13.1133Z"
fill="#FA744E"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.2924 2.4675C16.3332 0.93225 14.6509 0 12.8397 0H5.95844C6.09644 0.1695 6.22694 0.3465 6.34094 0.54L12.4369 10.8555L13.1134 12H23.2497L17.2924 2.4675Z"
fill="#343741"
/>
</g>
<defs>
<clipPath id="clip0_3269_21193">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
const AddIntegrationPanel: React.FC<{
icon: IconType;
title: string;
description: string;
buttonId: IntegrationsPageName;
}> = React.memo(({ title, description, buttonId, icon }) => {
const { onStepLinkClicked } = useStepContext();
const onClick = useCallback(() => {
onStepLinkClicked({
originStepId: AddIntegrationsSteps.connectToDataSources,
stepLinkId: buttonId,
});
}, [onStepLinkClicked, buttonId]);
return (
<EuiPanel paddingSize="m" hasShadow={false} hasBorder>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type={icon} size="xl" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xs">
<h3>{title}</h3>
</EuiTitle>
<EuiText color="subdued" size="xs">
{description}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LinkButton id={buttonId} onClick={onClick}>
{SEE_INTEGRATIONS}
</LinkButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
});
AddIntegrationPanel.displayName = 'AddIntegrationPanel';
export const AddIntegrationButtons: React.FC = React.memo(() => {
const { integrationAssistant } = useKibana().services;
const { CreateIntegrationCardButton } = integrationAssistant?.components ?? {};
return (
<EuiFlexGroup direction="column" className="step-paragraph" gutterSize="m">
<EuiFlexItem grow={false}>
<AddIntegrationPanel
title={ADD_CLOUD_INTEGRATIONS_TITLE}
description={ADD_CLOUD_INTEGRATIONS_DESCRIPTION}
icon={CloudIntegrationsIcon}
buttonId={IntegrationsPageName.integrationsSecurityCloud}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddIntegrationPanel
title={ADD_EDR_XDR_INTEGRATIONS_TITLE}
description={ADD_EDR_XDR_INTEGRATIONS_DESCRIPTION}
icon={EdrXdrIntegrationsIcon}
buttonId={IntegrationsPageName.integrationsSecurityEdrXrd}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddIntegrationPanel
title={ADD_ALL_INTEGRATIONS_TITLE}
description={ADD_ALL_INTEGRATIONS_DESCRIPTION}
icon="logoSecurity"
buttonId={IntegrationsPageName.integrationsSecurity}
/>
</EuiFlexItem>
{CreateIntegrationCardButton && (
<EuiFlexItem grow={false}>
<CreateIntegrationCardButton compressed />
</EuiFlexItem>
)}
</EuiFlexGroup>
);
});
AddIntegrationButtons.displayName = 'AddIntegrationButtons';

View file

@ -1,85 +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 React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiIcon, EuiLink, useEuiTheme } from '@elastic/eui';
import { SecurityPageName, useNavigateTo } from '@kbn/security-solution-navigation';
import classnames from 'classnames';
import { useAddIntegrationsCalloutStyles } from '../styles/add_integrations_callout.styles';
import { ADD_INTEGRATIONS_STEP } from './translations';
import type { StepId } from '../types';
import { AddIntegrationsSteps } from '../types';
import { useStepContext } from '../context/step_context';
import { AddIntegrationCalloutStepLinkId } from './types';
const AddIntegrationsCalloutComponent = ({
stepName,
stepId,
}: {
stepName?: string;
stepId: StepId;
}) => {
const { calloutWrapperStyles, calloutTitleStyles, calloutAnchorStyles } =
useAddIntegrationsCalloutStyles();
const { euiTheme } = useEuiTheme();
const { navigateTo } = useNavigateTo();
const { onStepLinkClicked } = useStepContext();
const onClick = useCallback(() => {
navigateTo({
deepLinkId: SecurityPageName.landing,
path: `#${AddIntegrationsSteps.connectToDataSources}`,
});
onStepLinkClicked({
originStepId: stepId,
stepLinkId: AddIntegrationCalloutStepLinkId,
});
}, [navigateTo, onStepLinkClicked, stepId]);
const classNames = classnames('add-integrations-callout', calloutWrapperStyles);
return (
<EuiCallOut
className={classNames}
title={
<>
<EuiIcon
size="m"
type="iInCircle"
color={euiTheme.colors.title}
className="eui-alignMiddle"
/>
<span className={calloutTitleStyles}>
<FormattedMessage
id="xpack.securitySolution.onboarding.addIntegrationCallout.description"
defaultMessage="To {stepName} add integrations first {addIntegration}"
values={{
addIntegration: (
<EuiLink onClick={onClick} className={calloutAnchorStyles}>
{ADD_INTEGRATIONS_STEP}
<EuiIcon type="arrowRight" size="s" className={calloutAnchorStyles} />
</EuiLink>
),
stepName: stepName ?? (
<FormattedMessage
id="xpack.securitySolution.onboarding.addIntegrationCallout.link.action"
defaultMessage="enable this step"
/>
),
}}
/>
</span>
</>
}
size="s"
/>
);
};
export const AddIntegrationCallout = React.memo(AddIntegrationsCalloutComponent);

View file

@ -1,65 +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 { LinkButton } from '@kbn/security-solution-navigation/links';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import React, { useCallback } from 'react';
import { useStepContext } from '../context/step_context';
import {
AddAndValidateYourDataCardsId,
AddIntegrationsSteps,
GetStartedWithAlertsCardsId,
SectionId,
ViewAlertsSteps,
} from '../types';
import { AddIntegrationCallout } from './add_integration_callout';
import { VIEW_ALERTS, VIEW_ALERTS_CALLOUT_TITLE } from './translations';
const AlertsButtonComponent = () => {
const { toggleTaskCompleteStatus, onStepLinkClicked, finishedSteps } = useStepContext();
const isIntegrationsStepComplete = finishedSteps[
AddAndValidateYourDataCardsId.addIntegrations
]?.has(AddIntegrationsSteps.connectToDataSources);
const onClick = useCallback(() => {
onStepLinkClicked({
originStepId: ViewAlertsSteps.viewAlerts,
stepLinkId: SecurityPageName.alerts,
});
toggleTaskCompleteStatus({
stepId: ViewAlertsSteps.viewAlerts,
stepLinkId: SecurityPageName.alerts,
cardId: GetStartedWithAlertsCardsId.viewAlerts,
sectionId: SectionId.getStartedWithAlerts,
undo: false,
trigger: 'click',
});
}, [onStepLinkClicked, toggleTaskCompleteStatus]);
return (
<>
{!isIntegrationsStepComplete && (
<AddIntegrationCallout
stepName={VIEW_ALERTS_CALLOUT_TITLE}
stepId={ViewAlertsSteps.viewAlerts}
/>
)}
<LinkButton
className="step-paragraph"
disabled={!isIntegrationsStepComplete}
fill
id={SecurityPageName.alerts}
onClick={onClick}
>
{VIEW_ALERTS}
</LinkButton>
</>
);
};
export const AlertsButton = React.memo(AlertsButtonComponent);

View file

@ -1,63 +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 React, { useCallback } from 'react';
import { LinkButton } from '@kbn/security-solution-navigation/links';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import {
AddAndValidateYourDataCardsId,
AddIntegrationsSteps,
SectionId,
ViewDashboardSteps,
} from '../types';
import { useStepContext } from '../context/step_context';
import { AddIntegrationCallout } from './add_integration_callout';
import { GO_TO_DASHBOARDS, VIEW_DASHBOARDS_CALLOUT_TITLE } from './translations';
const DashboardButtonComponent = () => {
const { toggleTaskCompleteStatus, finishedSteps, onStepLinkClicked } = useStepContext();
const isIntegrationsStepComplete = finishedSteps[
AddAndValidateYourDataCardsId.addIntegrations
]?.has(AddIntegrationsSteps.connectToDataSources);
const onClick = useCallback(() => {
onStepLinkClicked({
originStepId: ViewDashboardSteps.analyzeData,
stepLinkId: SecurityPageName.dashboards,
});
toggleTaskCompleteStatus({
stepId: ViewDashboardSteps.analyzeData,
stepLinkId: SecurityPageName.dashboards,
cardId: AddAndValidateYourDataCardsId.viewDashboards,
sectionId: SectionId.addAndValidateYourData,
undo: false,
trigger: 'click',
});
}, [onStepLinkClicked, toggleTaskCompleteStatus]);
return (
<>
{!isIntegrationsStepComplete && (
<AddIntegrationCallout
stepName={VIEW_DASHBOARDS_CALLOUT_TITLE}
stepId={ViewDashboardSteps.analyzeData}
/>
)}
<LinkButton
className="step-paragraph"
disabled={!isIntegrationsStepComplete}
fill
id={SecurityPageName.dashboards}
onClick={onClick}
>
{GO_TO_DASHBOARDS}
</LinkButton>
</>
);
};
export const DashboardButton = React.memo(DashboardButtonComponent);

View file

@ -1,42 +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 React, { useCallback } from 'react';
import { EuiIcon } from '@elastic/eui';
import { MANAGE_PROJECTS } from './translations';
import { useProjectsUrl } from '../hooks/use_projects_url';
import { LinkButton } from '../../../links';
import { useStepContext } from '../context/step_context';
import { CreateProjectSteps } from '../types';
import { ManageProjectsStepLinkId } from './types';
const ManageProjectsButtonComponent = () => {
const projectsUrl = useProjectsUrl();
const { onStepLinkClicked } = useStepContext();
const onClick = useCallback(() => {
onStepLinkClicked({
originStepId: CreateProjectSteps.createFirstProject,
stepLinkId: ManageProjectsStepLinkId,
});
}, [onStepLinkClicked]);
return projectsUrl ? (
<LinkButton
aria-label={MANAGE_PROJECTS}
className="step-paragraph"
fill
href={projectsUrl}
target="_blank"
onClick={onClick}
>
{MANAGE_PROJECTS}
<EuiIcon type="popout" />
</LinkButton>
) : null;
};
export const ManageProjectsButton = React.memo(ManageProjectsButtonComponent);

View file

@ -1,61 +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 { i18n } from '@kbn/i18n';
export const MANAGE_PROJECTS = i18n.translate(
'xpack.securitySolution.onboarding.task.manageProjects',
{
defaultMessage: 'Manage projects',
}
);
export const ADD_ELASTIC_RULES = i18n.translate(
'xpack.securitySolution.onboarding.task.addElasticRules',
{
defaultMessage: 'Add Elastic rules',
}
);
export const ADD_ELASTIC_RULES_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.task.addElasticRules.callout.title',
{
defaultMessage: 'add Elastic rules',
}
);
export const ADD_INTEGRATIONS_STEP = i18n.translate(
'xpack.securitySolution.onboarding.task.addIntegrationsStep.title',
{
defaultMessage: 'Add integrations step',
}
);
export const VIEW_ALERTS = i18n.translate('xpack.securitySolution.onboarding.task.viewAlerts', {
defaultMessage: 'View alerts',
});
export const VIEW_ALERTS_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.task.viewAlerts.callout.title',
{
defaultMessage: 'view alerts',
}
);
export const GO_TO_DASHBOARDS = i18n.translate(
'xpack.securitySolution.onboarding.task.goToDashboards',
{
defaultMessage: 'Go to dashboards',
}
);
export const VIEW_DASHBOARDS_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.task.viewDashboards.callout.title',
{
defaultMessage: 'view dashboards',
}
);

View file

@ -1,43 +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 type { NavigateToUrlOptions } from '@kbn/core/public';
import type { SecurityPageName } from '../../../../../../common';
export type NavigateToUrl = (
url: string,
options?: NavigateToUrlOptions | undefined
) => Promise<void>;
export type GetUrlForApp = (
appId: string,
options?:
| {
path?: string | undefined;
absolute?: boolean | undefined;
deepLinkId?: string | undefined;
}
| undefined
) => string;
export enum IntegrationsPageName {
integrationsSecurity = 'integrations:/browse/security',
integrationsSecurityCloud = 'integrations:/browse/security/cloudsecurity_cdr',
integrationsSecurityEdrXrd = 'integrations:/browse/security/edr_xdr',
}
export const AddIntegrationCalloutStepLinkId = 'addIntegrationCallout';
export const ManageProjectsStepLinkId = 'manageProjects';
export type StepLinkId =
| SecurityPageName.rules
| 'addIntegrationCallout'
| IntegrationsPageName.integrationsSecurityCloud
| IntegrationsPageName.integrationsSecurityEdrXrd
| IntegrationsPageName.integrationsSecurity
| 'manageProjects'
| SecurityPageName.alerts
| SecurityPageName.dashboards;

View file

@ -1,344 +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 {
ACTIVE_PRODUCTS_STORAGE_KEY,
defaultExpandedCards,
EXPANDED_CARDS_STORAGE_KEY,
FINISHED_STEPS_STORAGE_KEY,
getStorageKeyBySpace,
OnboardingStorage,
} from './storage';
import {
AddIntegrationsSteps,
CreateProjectSteps,
EnablePrebuiltRulesSteps,
OverviewSteps,
QuickStartSectionCardsId,
ViewAlertsSteps,
ViewDashboardSteps,
type StepId,
} from './types';
import { DEFAULT_FINISHED_STEPS } from './helpers';
import type { MockStorage } from '../../../lib/local_storage/__mocks__';
import { storage } from '../../../lib/local_storage';
import { ProductLine } from './configs';
jest.mock('../../../lib/local_storage');
describe.each([['test'], [undefined]])('useStorage - spaceId: %s', (spaceId) => {
const mockStorage = storage as unknown as MockStorage;
const onboardingStorage = new OnboardingStorage(spaceId);
const onboardingSteps = [
CreateProjectSteps.createFirstProject,
OverviewSteps.getToKnowElasticSecurity,
AddIntegrationsSteps.connectToDataSources,
ViewDashboardSteps.analyzeData,
EnablePrebuiltRulesSteps.enablePrebuiltRules,
ViewAlertsSteps.viewAlerts,
];
beforeEach(() => {
// Clear the mocked storage object before each test
mockStorage.clearMockStorageData();
jest.clearAllMocks();
});
it('should return the active products from storage', () => {
expect(onboardingStorage.getActiveProductsFromStorage()).toEqual([]);
const activeProductsStorageKey = getStorageKeyBySpace(ACTIVE_PRODUCTS_STORAGE_KEY, spaceId);
mockStorage.set(activeProductsStorageKey, [ProductLine.security, ProductLine.endpoint]);
expect(onboardingStorage.getActiveProductsFromStorage()).toEqual([
ProductLine.security,
ProductLine.endpoint,
]);
});
it('should toggle active products in storage', () => {
const activeProductsStorageKey = getStorageKeyBySpace(ACTIVE_PRODUCTS_STORAGE_KEY, spaceId);
expect(onboardingStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([
ProductLine.security,
]);
expect(mockStorage.set).toHaveBeenCalledWith(activeProductsStorageKey, [ProductLine.security]);
mockStorage.set(activeProductsStorageKey, [ProductLine.security]);
expect(onboardingStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([]);
expect(mockStorage.set).toHaveBeenCalledWith(activeProductsStorageKey, []);
});
it('should return the finished steps from storage by card ID', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
expect(
onboardingStorage.getFinishedStepsFromStorageByCardId(
QuickStartSectionCardsId.createFirstProject
)
).toEqual([CreateProjectSteps.createFirstProject]);
mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
});
expect(
onboardingStorage.getFinishedStepsFromStorageByCardId(
QuickStartSectionCardsId.createFirstProject
)
).toEqual([CreateProjectSteps.createFirstProject, 'step2']);
});
it('should return all finished steps from storage', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
expect(onboardingStorage.getAllFinishedStepsFromStorage()).toEqual(DEFAULT_FINISHED_STEPS);
mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
[QuickStartSectionCardsId.watchTheOverviewVideo]: ['step3'],
});
expect(onboardingStorage.getAllFinishedStepsFromStorage()).toEqual({
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
[QuickStartSectionCardsId.watchTheOverviewVideo]: ['step3'],
});
});
it('should add a finished step to storage', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
onboardingStorage.addFinishedStepToStorage(
QuickStartSectionCardsId.createFirstProject,
CreateProjectSteps.createFirstProject
);
expect(mockStorage.set).toHaveBeenCalledWith(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
});
mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
});
onboardingStorage.addFinishedStepToStorage(
QuickStartSectionCardsId.createFirstProject,
'step2' as StepId
);
expect(mockStorage.set).toHaveBeenCalledWith(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
});
});
it('should get finished steps from storage by card ID', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
[QuickStartSectionCardsId.watchTheOverviewVideo]: ['step3'],
});
expect(
onboardingStorage.getFinishedStepsFromStorageByCardId(
QuickStartSectionCardsId.createFirstProject
)
).toEqual([CreateProjectSteps.createFirstProject, 'step2']);
expect(
onboardingStorage.getFinishedStepsFromStorageByCardId(
QuickStartSectionCardsId.watchTheOverviewVideo
)
).toEqual(['step3']);
});
it('should get all finished steps from storage', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
[QuickStartSectionCardsId.watchTheOverviewVideo]: ['step3'],
card3: ['step4'],
});
expect(onboardingStorage.getAllFinishedStepsFromStorage()).toEqual({
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
[QuickStartSectionCardsId.watchTheOverviewVideo]: ['step3'],
card3: ['step4'],
});
mockStorage.set(finishedStepsStorageKey, {});
expect(onboardingStorage.getAllFinishedStepsFromStorage()).toEqual(DEFAULT_FINISHED_STEPS);
});
it('should remove a finished step from storage', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
onboardingStorage.removeFinishedStepFromStorage(
QuickStartSectionCardsId.createFirstProject,
'step2' as StepId,
onboardingSteps
);
expect(mockStorage.set).toHaveBeenCalledWith(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
});
});
it('should not remove a default finished step from storage', () => {
const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
});
onboardingStorage.removeFinishedStepFromStorage(
QuickStartSectionCardsId.createFirstProject,
CreateProjectSteps.createFirstProject,
onboardingSteps
);
expect(mockStorage.get(finishedStepsStorageKey)).toEqual({
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
],
});
});
it('should get all expanded card steps from storage', () => {
const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
(mockStorage.get as jest.Mock).mockReturnValueOnce({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: true,
expandedSteps: [CreateProjectSteps.createFirstProject],
},
});
const result = onboardingStorage.getAllExpandedCardStepsFromStorage();
expect(mockStorage.get).toHaveBeenCalledWith(expandedCardsStorageKey);
expect(result).toEqual({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: true,
expandedSteps: [CreateProjectSteps.createFirstProject],
},
});
});
it('should get default expanded card steps from storage', () => {
const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
(mockStorage.get as jest.Mock).mockReturnValueOnce(null);
const result = onboardingStorage.getAllExpandedCardStepsFromStorage();
expect(mockStorage.get).toHaveBeenCalledWith(expandedCardsStorageKey);
expect(result).toEqual(defaultExpandedCards);
});
it('should reset card steps in storage', () => {
const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
(mockStorage.get as jest.Mock).mockReturnValueOnce({
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
expandedSteps: [OverviewSteps.getToKnowElasticSecurity],
},
});
onboardingStorage.resetAllExpandedCardStepsToStorage();
expect(mockStorage.get(expandedCardsStorageKey)).toEqual({
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
expandedSteps: [],
},
});
});
it('should add a step to expanded card steps in storage', () => {
const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
mockStorage.set(expandedCardsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: false,
expandedSteps: [],
},
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
expandedSteps: [OverviewSteps.getToKnowElasticSecurity],
},
});
onboardingStorage.addExpandedCardStepToStorage(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
expect(mockStorage.get(expandedCardsStorageKey)).toEqual({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: false,
expandedSteps: [],
},
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: true,
expandedSteps: [OverviewSteps.getToKnowElasticSecurity],
},
});
});
it('should remove a step from expanded card steps in storage', () => {
const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
mockStorage.set(expandedCardsStorageKey, {
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: true,
expandedSteps: [OverviewSteps.getToKnowElasticSecurity],
},
});
onboardingStorage.removeExpandedCardStepFromStorage(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
expect(mockStorage.get(expandedCardsStorageKey)).toEqual({
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
expandedSteps: [],
},
});
});
it('should update a card from expanded card steps in storage', () => {
const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
(mockStorage.get as jest.Mock).mockReturnValueOnce({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: true,
expandedSteps: [CreateProjectSteps.createFirstProject],
},
});
onboardingStorage.removeExpandedCardStepFromStorage(
QuickStartSectionCardsId.createFirstProject
);
expect(mockStorage.set).toHaveBeenCalledWith(expandedCardsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: false,
expandedSteps: [CreateProjectSteps.createFirstProject],
},
});
});
});

View file

@ -1,194 +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 type { CardId, StepId } from './types';
import {
QuickStartSectionCardsId,
AddAndValidateYourDataCardsId,
GetStartedWithAlertsCardsId,
} from './types';
import { DEFAULT_FINISHED_STEPS, isDefaultFinishedCardStep } from './helpers';
import { getSections } from './sections';
import { storage } from '../../../lib/local_storage';
import type { ProductLine } from './configs';
export const ACTIVE_PRODUCTS_STORAGE_KEY = 'securitySolution.getStarted.activeProducts';
export const FINISHED_STEPS_STORAGE_KEY = 'securitySolution.getStarted.finishedSteps';
export const EXPANDED_CARDS_STORAGE_KEY = 'securitySolution.getStarted.expandedCards';
export const getStorageKeyBySpace = (storageKey: string, spaceId: string | null | undefined) => {
if (spaceId == null) {
return storageKey;
}
return `${storageKey}.${spaceId}`;
};
export const defaultExpandedCards = {
[QuickStartSectionCardsId.watchTheOverviewVideo]: { isExpanded: false, expandedSteps: [] },
[QuickStartSectionCardsId.createFirstProject]: { isExpanded: false, expandedSteps: [] },
[AddAndValidateYourDataCardsId.addIntegrations]: { isExpanded: false, expandedSteps: [] },
[AddAndValidateYourDataCardsId.viewDashboards]: { isExpanded: false, expandedSteps: [] },
[GetStartedWithAlertsCardsId.enablePrebuiltRules]: { isExpanded: false, expandedSteps: [] },
[GetStartedWithAlertsCardsId.viewAlerts]: { isExpanded: false, expandedSteps: [] },
};
export class OnboardingStorage {
private finishedStepsStorageKey: string;
private activeProductsStorageKey: string;
private expandedCardsStorageKey: string;
constructor(spaceId: string | undefined) {
this.finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
this.activeProductsStorageKey = getStorageKeyBySpace(ACTIVE_PRODUCTS_STORAGE_KEY, spaceId);
this.expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
}
setDefaultFinishedSteps = (cardId: CardId) => {
const finishedStepsStorageKey = this.finishedStepsStorageKey;
const allFinishedSteps: Record<CardId, StepId[]> = storage.get(finishedStepsStorageKey);
const defaultFinishedStepsByCardId = DEFAULT_FINISHED_STEPS[cardId];
const hasDefaultFinishedSteps = defaultFinishedStepsByCardId != null;
if (!hasDefaultFinishedSteps) {
return;
}
storage.set(finishedStepsStorageKey, {
...allFinishedSteps,
[cardId]: Array.from(
// dedupe card steps
new Set([...(defaultFinishedStepsByCardId ?? []), ...(allFinishedSteps[cardId] ?? [])])
),
});
};
public getActiveProductsFromStorage = () => {
const activeProductsStorageKey = this.activeProductsStorageKey;
const activeProducts: ProductLine[] = storage.get(activeProductsStorageKey);
return activeProducts ?? [];
};
public toggleActiveProductsInStorage = (productId: ProductLine) => {
const activeProductsStorageKey = this.activeProductsStorageKey;
const activeProducts: ProductLine[] = storage.get(activeProductsStorageKey) ?? [];
const index = activeProducts.indexOf(productId);
if (index < 0) {
activeProducts.push(productId);
} else {
activeProducts.splice(index, 1);
}
storage.set(activeProductsStorageKey, activeProducts);
return activeProducts;
};
getFinishedStepsFromStorageByCardId = (cardId: CardId) => {
const finishedSteps = this.getAllFinishedStepsFromStorage();
const steps: StepId[] = finishedSteps[cardId] ?? [];
return steps;
};
public getAllFinishedStepsFromStorage = () => {
const finishedStepsStorageKey = this.finishedStepsStorageKey;
const allFinishedSteps: Record<CardId, StepId[]> = storage.get(finishedStepsStorageKey);
if (allFinishedSteps == null) {
storage.set(finishedStepsStorageKey, DEFAULT_FINISHED_STEPS);
} else {
getSections().forEach((section) => {
section.cards?.forEach((card) => {
this.setDefaultFinishedSteps(card.id);
});
});
}
return storage.get(finishedStepsStorageKey);
};
public addFinishedStepToStorage = (cardId: CardId, stepId: StepId) => {
const finishedStepsStorageKey = this.finishedStepsStorageKey;
const finishedSteps = this.getAllFinishedStepsFromStorage();
const card: StepId[] = finishedSteps[cardId] ?? [];
if (card.indexOf(stepId) < 0) {
card.push(stepId);
storage.set(finishedStepsStorageKey, { ...finishedSteps, [cardId]: card });
}
};
public removeFinishedStepFromStorage = (
cardId: CardId,
stepId: StepId,
onboardingSteps: StepId[]
) => {
if (isDefaultFinishedCardStep(cardId, stepId, onboardingSteps)) {
return;
}
const finishedStepsStorageKey = this.finishedStepsStorageKey;
const finishedSteps = this.getAllFinishedStepsFromStorage();
const steps: StepId[] = finishedSteps[cardId] ?? [];
const index = steps.indexOf(stepId);
if (index >= 0) {
steps.splice(index, 1);
}
storage.set(finishedStepsStorageKey, { ...finishedSteps, [cardId]: steps });
};
public getAllExpandedCardStepsFromStorage = () => {
const expandedCardsStorageKey = this.expandedCardsStorageKey;
const storageData = storage.get(expandedCardsStorageKey);
return !storageData || Object.keys(storageData).length === 0
? defaultExpandedCards
: storageData;
};
public resetAllExpandedCardStepsToStorage = () => {
const activeCards: Record<CardId, { isExpanded: boolean; expandedSteps: StepId[] }> =
this.getAllExpandedCardStepsFromStorage();
const expandedCardsStorageKey = this.expandedCardsStorageKey;
storage.set(
expandedCardsStorageKey,
Object.entries(activeCards).reduce((acc, [cardId, card]) => {
acc[cardId as CardId] = defaultExpandedCards[cardId as CardId] ?? card;
return acc;
}, {} as Record<CardId, { isExpanded: boolean; expandedSteps: StepId[] }>)
);
};
public addExpandedCardStepToStorage = (cardId: CardId, stepId: StepId) => {
const activeCards: Record<CardId, { isExpanded: boolean; expandedSteps: StepId[] }> =
this.getAllExpandedCardStepsFromStorage();
const expandedCardsStorageKey = this.expandedCardsStorageKey;
const card = activeCards[cardId]
? {
expandedSteps: [stepId],
isExpanded: true,
}
: {
isExpanded: false,
expandedSteps: [],
};
storage.set(expandedCardsStorageKey, { ...activeCards, [cardId]: card });
};
public removeExpandedCardStepFromStorage = (cardId: CardId, stepId?: StepId) => {
const expandedCardsStorageKey = this.expandedCardsStorageKey;
const activeCards: Record<
CardId,
{ isExpanded: boolean; expandedSteps: StepId[] } | undefined
> = storage.get(expandedCardsStorageKey) ?? {};
const card = activeCards[cardId];
if (card && !stepId) {
card.isExpanded = false;
}
if (card && stepId) {
const index = card.expandedSteps.indexOf(stepId);
if (index >= 0) {
card.expandedSteps.splice(index, 1);
card.isExpanded = false;
}
}
storage.set(expandedCardsStorageKey, { ...activeCards, [cardId]: card });
};
}

View file

@ -1,49 +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 { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { useMemo } from 'react';
export const useAddIntegrationsCalloutStyles = () => {
const { euiTheme } = useEuiTheme();
const backgroundColor = useEuiBackgroundColor('primary');
const customStyles = useMemo(
() => ({
calloutWrapperStyles: css({
borderRadius: euiTheme.border.radius.medium,
border: `1px solid ${euiTheme.colors.lightShade}`,
padding: `${euiTheme.size.xs} ${euiTheme.size.m}`,
backgroundColor,
marginTop: euiTheme.size.base,
}),
calloutTitleStyles: css({
color: euiTheme.colors.title,
fontSize: euiTheme.size.m,
fontWeight: euiTheme.font.weight.regular,
lineHeight: `${euiTheme.base * 1.25}px`,
marginLeft: euiTheme.size.xs,
}),
calloutAnchorStyles: css({ marginLeft: euiTheme.size.s }),
}),
[
backgroundColor,
euiTheme.base,
euiTheme.border.radius.medium,
euiTheme.colors.lightShade,
euiTheme.colors.title,
euiTheme.font.weight.regular,
euiTheme.size.base,
euiTheme.size.m,
euiTheme.size.s,
euiTheme.size.xs,
]
);
return customStyles;
};

View file

@ -1,32 +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 { useEuiShadow, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
export const SHADOW_ANIMATION_DURATION = 350;
export const useCardItemStyles = () => {
const { euiTheme } = useEuiTheme();
const shadow = useEuiShadow('l');
return css({
'&.card-item': {
padding: euiTheme.size.base,
borderRadius: euiTheme.size.s,
border: `1px solid ${euiTheme.colors.lightShade}`,
boxSizing: `content-box`,
},
'&:hover, &.card-expanded': {
boxShadow: shadow,
transition: `box-shadow ${SHADOW_ANIMATION_DURATION}ms ease-out`,
},
'&.card-expanded': {
border: `2px solid #6092c0`,
},
});
};

View file

@ -1,95 +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 { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { useMemo } from 'react';
export const HEIGHT_ANIMATION_DURATION = 250;
export const useCardStepStyles = () => {
const { euiTheme } = useEuiTheme();
const completeStepBackgroundColor = useEuiBackgroundColor('success');
const customStyles = useMemo(
() => ({
stepPanelStyles: css({
'.stepContentWrapper': {
display: 'grid',
gridTemplateRows: '1fr',
visibility: 'visible',
transition: `
grid-template-rows ${HEIGHT_ANIMATION_DURATION}ms ease-in,
visibility ${euiTheme.animation.normal} ${euiTheme.animation.resistance}
`,
},
'&.step-panel-collapsed .stepContentWrapper': {
visibility: 'collapse',
gridTemplateRows: '0fr',
},
'.stepContent': {
overflow: 'hidden',
},
}),
getStepGroundStyles: ({ hasStepContent }: { hasStepContent: boolean }) =>
css({
cursor: hasStepContent ? 'pointer' : 'default',
gap: euiTheme.size.base,
}),
stepItemStyles: css({ alignSelf: 'center' }),
stepIconStyles: css({
'&.step-icon': {
borderRadius: '50%',
width: euiTheme.size.xxxl,
height: euiTheme.size.xxxl,
padding: euiTheme.size.m,
backgroundColor: euiTheme.colors.body,
},
'&.step-icon-done': {
backgroundColor: completeStepBackgroundColor,
},
}),
stepTitleStyles: css({
'&.step-title': {
paddingRight: euiTheme.size.m,
lineHeight: euiTheme.size.xxxl,
fontSize: `${euiTheme.base * 0.875}px`,
fontWeight: euiTheme.font.weight.semiBold,
verticalAlign: 'middle',
},
}),
allDoneTextStyles: css({
'&.all-done-badge': {
backgroundColor: completeStepBackgroundColor,
color: euiTheme.colors.successText,
},
}),
toggleButtonStyles: css({
'&.toggle-button': {
marginLeft: `${euiTheme.base * 0.375}px`,
},
}),
}),
[
completeStepBackgroundColor,
euiTheme.animation.normal,
euiTheme.animation.resistance,
euiTheme.base,
euiTheme.colors.body,
euiTheme.colors.successText,
euiTheme.font.weight.semiBold,
euiTheme.size.base,
euiTheme.size.m,
euiTheme.size.xxxl,
]
);
return customStyles;
};

View file

@ -1,40 +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 { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { useMemo } from 'react';
export const useCurrentPlanStyles = () => {
const { euiTheme } = useEuiTheme();
const styles = useMemo(() => {
return {
currentPlanWrapperStyles: css({
backgroundColor: euiTheme.colors.lightestShade,
borderRadius: '56px',
padding: `${euiTheme.size.xs} ${euiTheme.size.s} ${euiTheme.size.xs} ${euiTheme.size.m}`,
height: euiTheme.size.xl,
}),
currentPlanTextStyles: css({
fontSize: euiTheme.size.m,
fontWeight: euiTheme.font.weight.bold,
paddingRight: euiTheme.size.xs,
}),
projectFeaturesUrlStyles: css({
marginLeft: euiTheme.size.xs,
}),
};
}, [
euiTheme.colors.lightestShade,
euiTheme.font.weight.bold,
euiTheme.size.m,
euiTheme.size.s,
euiTheme.size.xl,
euiTheme.size.xs,
]);
return styles;
};

View file

@ -1,53 +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 { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
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({
fontSize: `${euiTheme.base * 0.875}px`,
fontWeight: euiTheme.font.weight.semiBold,
lineHeight: euiTheme.size.l,
color: euiTheme.colors.title,
}),
descriptionStyle: css({
fontSize: '12.25px',
fontWeight: euiTheme.font.weight.regular,
lineHeight: `${euiTheme.base * 1.25}px`,
color: euiTheme.colors.darkestShade,
}),
linkStyle: css({
fontSize: euiTheme.size.m,
fontWeight: euiTheme.font.weight.medium,
lineHeight: 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;
};

Some files were not shown because too many files have changed in this diff Show more