mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
df830eec60
commit
a75861e434
272 changed files with 5949 additions and 8476 deletions
|
@ -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;
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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';
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const useProjectFeaturesUrl = jest.fn(() => 'mocked_user_name');
|
||||
export { CenteredLoadingSpinner } from './centered_loading_spinner';
|
|
@ -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}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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%"
|
||||
/>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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>
|
||||
));
|
|
@ -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(() => ({}));
|
|
@ -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>
|
||||
));
|
|
@ -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]
|
||||
);
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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>);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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 Elastic’s default dashboards — including alerts, user authentication events, known vulnerabilities, and more.'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -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,
|
||||
}));
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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(
|
||||
'We’re excited to support you in protecting your organization’s data. Here’s a preview of the steps you’ll 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');
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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:
|
||||
'We’re excited to support you in protecting your organization’s data. Here’s a preview of the steps you’ll 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',
|
||||
}
|
||||
);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 }
|
||||
);
|
|
@ -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,
|
||||
]);
|
|
@ -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 },
|
||||
]);
|
|
@ -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$);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
]);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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]);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 };
|
||||
};
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
Before Width: | Height: | Size: 69 KiB |
|
@ -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 |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
|
@ -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>
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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));
|
||||
};
|
|
@ -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" />);
|
|
@ -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);
|
|
@ -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';
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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;
|
|
@ -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],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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`,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue