mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[SecuritySolution] Get Started Page UI update (#160850)
## Summary
1. Add content max-width: 1150px
2. Change wording `runtime` to `realtime`
3. Enable product toggles based on `product type` if no data from local
storage. (Product type `security` displayed as `Analytics` in toggle but
it's product id changed to `security` to aligned with product types'
configs)
**Follow up:** Wait for UX to confirm if `product tiers (essential /
complete)` affects the cards.
<img width="2559" alt="Screenshot 2023-06-29 at 22 06 39"
src="be5831d2
-2538-43fd-82c4-30e8321837ed">
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a6a8f5b9ab
commit
61b792f50f
25 changed files with 569 additions and 218 deletions
|
@ -7,11 +7,18 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export enum ProductLine {
|
||||
security = 'security',
|
||||
cloud = 'cloud',
|
||||
endpoint = 'endpoint',
|
||||
}
|
||||
|
||||
export const productLine = schema.oneOf([
|
||||
schema.literal('security'),
|
||||
schema.literal('endpoint'),
|
||||
schema.literal('cloud'),
|
||||
schema.literal(ProductLine.security),
|
||||
schema.literal(ProductLine.endpoint),
|
||||
schema.literal(ProductLine.cloud),
|
||||
]);
|
||||
|
||||
export type SecurityProductLine = TypeOf<typeof productLine>;
|
||||
|
||||
export const productTier = schema.oneOf([schema.literal('essentials'), schema.literal('complete')]);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import { getProductAppFeatures } from './pli_features';
|
||||
import * as pliConfig from './pli_config';
|
||||
import { ProductLine } from '../config';
|
||||
|
||||
describe('getProductAppFeatures', () => {
|
||||
it('should return the essentials PLIs features', () => {
|
||||
|
@ -18,7 +19,7 @@ describe('getProductAppFeatures', () => {
|
|||
};
|
||||
|
||||
const appFeatureKeys = getProductAppFeatures([
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: ProductLine.security, product_tier: 'essentials' },
|
||||
]);
|
||||
|
||||
expect(appFeatureKeys).toEqual(['foo']);
|
||||
|
@ -34,7 +35,7 @@ describe('getProductAppFeatures', () => {
|
|||
};
|
||||
|
||||
const appFeatureKeys = getProductAppFeatures([
|
||||
{ product_line: 'security', product_tier: 'complete' },
|
||||
{ product_line: ProductLine.security, product_tier: 'complete' },
|
||||
]);
|
||||
|
||||
expect(appFeatureKeys).toEqual(['foo', 'baz']);
|
||||
|
@ -58,9 +59,9 @@ describe('getProductAppFeatures', () => {
|
|||
};
|
||||
|
||||
const appFeatureKeys = getProductAppFeatures([
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
{ product_line: 'cloud', product_tier: 'essentials' },
|
||||
{ product_line: ProductLine.security, product_tier: 'essentials' },
|
||||
{ product_line: ProductLine.endpoint, product_tier: 'complete' },
|
||||
{ product_line: ProductLine.cloud, product_tier: 'essentials' },
|
||||
]);
|
||||
|
||||
expect(appFeatureKeys).toEqual(['foo', 'bar', 'repeated', 'qux', 'quux', 'corge', 'garply']);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { GetStartedComponent } from './get_started';
|
||||
import { SecurityProductTypes } from '../../../common/config';
|
||||
|
||||
jest.mock('./toggle_panel');
|
||||
jest.mock('./welcome_panel');
|
||||
|
@ -20,9 +21,15 @@ jest.mock('@elastic/eui', () => {
|
|||
};
|
||||
});
|
||||
|
||||
const productTypes = [
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
{ product_line: 'cloud', product_tier: 'complete' },
|
||||
] as SecurityProductTypes;
|
||||
|
||||
describe('GetStartedComponent', () => {
|
||||
it('should render page title, subtitle, and description', () => {
|
||||
const { getByText } = render(<GetStartedComponent />);
|
||||
const { getByText } = render(<GetStartedComponent productTypes={productTypes} />);
|
||||
|
||||
const pageTitle = getByText('Welcome');
|
||||
const subtitle = getByText('Let’s get started');
|
||||
|
@ -35,8 +42,16 @@ describe('GetStartedComponent', () => {
|
|||
expect(description).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render Product Switch', () => {
|
||||
const { getByTestId } = render(<GetStartedComponent productTypes={productTypes} />);
|
||||
|
||||
const productSwitch = getByTestId('product-switch');
|
||||
|
||||
expect(productSwitch).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render WelcomePanel and TogglePanel', () => {
|
||||
const { getByTestId } = render(<GetStartedComponent />);
|
||||
const { getByTestId } = render(<GetStartedComponent productTypes={productTypes} />);
|
||||
|
||||
const welcomePanel = getByTestId('welcome-panel');
|
||||
const togglePanel = getByTestId('toggle-panel');
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import { EuiTitle, useEuiTheme, useEuiShadow } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { css } from '@emotion/react';
|
||||
|
@ -17,10 +17,22 @@ import {
|
|||
GET_STARTED_PAGE_SUBTITLE,
|
||||
GET_STARTED_PAGE_TITLE,
|
||||
} from './translations';
|
||||
import { SecurityProductTypes } from '../../../common/config';
|
||||
import { ProductSwitch } from './product_switch';
|
||||
import { useTogglePanel } from './use_toggle_panel';
|
||||
|
||||
export const GetStartedComponent: React.FC = () => {
|
||||
const CONTENT_WIDTH = 1150;
|
||||
|
||||
export const GetStartedComponent: React.FC<{ productTypes: SecurityProductTypes }> = ({
|
||||
productTypes,
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const shadow = useEuiShadow('s');
|
||||
const {
|
||||
onProductSwitchChanged,
|
||||
onStepClicked,
|
||||
state: { activeProducts, activeCards, finishedSteps },
|
||||
} = useTogglePanel({ productTypes });
|
||||
return (
|
||||
<KibanaPageTemplate
|
||||
restrictWidth={false}
|
||||
|
@ -38,6 +50,7 @@ export const GetStartedComponent: React.FC = () => {
|
|||
`}
|
||||
>
|
||||
<KibanaPageTemplate.Header
|
||||
restrictWidth={CONTENT_WIDTH}
|
||||
css={css`
|
||||
padding: 0 ${euiTheme.base * 2.25}px;
|
||||
`}
|
||||
|
@ -60,13 +73,39 @@ export const GetStartedComponent: React.FC = () => {
|
|||
>
|
||||
<WelcomePanel />
|
||||
</KibanaPageTemplate.Header>
|
||||
<KibanaPageTemplate.Section
|
||||
bottomBorder={false}
|
||||
grow={true}
|
||||
restrictWidth={CONTENT_WIDTH}
|
||||
paddingSize="none"
|
||||
css={css`
|
||||
${shadow};
|
||||
z-index: 1;
|
||||
flex-grow: 0;
|
||||
padding: 0 ${euiTheme.base * 2.25}px;
|
||||
`}
|
||||
>
|
||||
<ProductSwitch
|
||||
onProductSwitchChanged={onProductSwitchChanged}
|
||||
activeProducts={activeProducts}
|
||||
euiTheme={euiTheme}
|
||||
/>
|
||||
</KibanaPageTemplate.Section>
|
||||
<KibanaPageTemplate.Section
|
||||
bottomBorder="extended"
|
||||
grow={true}
|
||||
restrictWidth={false}
|
||||
restrictWidth={CONTENT_WIDTH}
|
||||
paddingSize="none"
|
||||
css={css`
|
||||
padding: 0 ${euiTheme.base * 2.25}px;
|
||||
`}
|
||||
>
|
||||
<TogglePanel />
|
||||
<TogglePanel
|
||||
finishedSteps={finishedSteps}
|
||||
activeCards={activeCards}
|
||||
activeProducts={activeProducts}
|
||||
onStepClicked={onStepClicked}
|
||||
/>
|
||||
</KibanaPageTemplate.Section>
|
||||
</KibanaPageTemplate>
|
||||
);
|
||||
|
|
|
@ -13,18 +13,18 @@ import {
|
|||
updateCard,
|
||||
} from './helpers';
|
||||
import {
|
||||
ActiveCard,
|
||||
ActiveCards,
|
||||
Card,
|
||||
CardId,
|
||||
GetMoreFromElasticSecurityCardId,
|
||||
GetSetUpCardId,
|
||||
IntroductionSteps,
|
||||
ProductId,
|
||||
Section,
|
||||
SectionId,
|
||||
StepId,
|
||||
} from './types';
|
||||
import * as sectionsConfigs from './sections';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
const mockSections = jest.spyOn(sectionsConfigs, 'getSections');
|
||||
describe('getCardTimeInMinutes', () => {
|
||||
it('should calculate the total time in minutes for a card correctly', () => {
|
||||
|
@ -74,8 +74,8 @@ describe('getCardStepsLeft', () => {
|
|||
|
||||
describe('isCardActive', () => {
|
||||
it('should return true if the card is active based on the active products', () => {
|
||||
const card = { productTypeRequired: [ProductId.analytics, ProductId.cloud] } as Card;
|
||||
const activeProducts = new Set([ProductId.analytics]);
|
||||
const card = { productLineRequired: [ProductLine.security, ProductLine.cloud] } as Card;
|
||||
const activeProducts = new Set([ProductLine.security]);
|
||||
|
||||
const isActive = isCardActive(card, activeProducts);
|
||||
|
||||
|
@ -84,7 +84,7 @@ describe('isCardActive', () => {
|
|||
|
||||
it('should return true if the card has no product type requirement', () => {
|
||||
const card = {} as Card;
|
||||
const activeProducts = new Set([ProductId.analytics]);
|
||||
const activeProducts = new Set([ProductLine.security]);
|
||||
|
||||
const isActive = isCardActive(card, activeProducts);
|
||||
|
||||
|
@ -92,8 +92,8 @@ describe('isCardActive', () => {
|
|||
});
|
||||
|
||||
it('should return false if the card is not active based on the active products', () => {
|
||||
const card = { productTypeRequired: [ProductId.analytics, ProductId.cloud] } as Card;
|
||||
const activeProducts = new Set([ProductId.endpoint]);
|
||||
const card = { productLineRequired: [ProductLine.security, ProductLine.cloud] } as Card;
|
||||
const activeProducts = new Set([ProductLine.endpoint]);
|
||||
|
||||
const isActive = isCardActive(card, activeProducts);
|
||||
|
||||
|
@ -140,7 +140,7 @@ describe('setupCards', () => {
|
|||
};
|
||||
it('should set up active cards based on active products', () => {
|
||||
const finishedSteps = {} as unknown as Record<CardId, Set<StepId>>;
|
||||
const activeProducts = new Set([ProductId.cloud]);
|
||||
const activeProducts = new Set([ProductLine.cloud]);
|
||||
|
||||
const activeCards = setupCards(finishedSteps, activeProducts);
|
||||
|
||||
|
@ -148,8 +148,8 @@ describe('setupCards', () => {
|
|||
...analyticProductActiveCards,
|
||||
[SectionId.getSetUp]: {
|
||||
...analyticProductActiveCards[SectionId.getSetUp],
|
||||
[GetSetUpCardId.protectYourEnvironmentInRuntime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRuntime,
|
||||
[GetSetUpCardId.protectYourEnvironmentInRealtime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRealtime,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
|
@ -159,7 +159,7 @@ describe('setupCards', () => {
|
|||
|
||||
it('should skip inactive cards based on finished steps and active products', () => {
|
||||
const finishedSteps = {} as Record<CardId, Set<StepId>>;
|
||||
const activeProducts = new Set([ProductId.analytics]);
|
||||
const activeProducts = new Set([ProductLine.security]);
|
||||
|
||||
const activeCards = setupCards(finishedSteps, activeProducts);
|
||||
|
||||
|
@ -171,7 +171,7 @@ describe('setupCards', () => {
|
|||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
} as unknown as Record<CardId, Set<StepId>>;
|
||||
|
||||
const activeProducts: Set<ProductId> = new Set();
|
||||
const activeProducts: Set<ProductLine> = new Set();
|
||||
|
||||
const activeCards = setupCards(finishedSteps, activeProducts);
|
||||
|
||||
|
@ -188,7 +188,7 @@ describe('setupCards', () => {
|
|||
const finishedSteps = {
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
} as unknown as Record<CardId, Set<StepId>>;
|
||||
const activeProducts = new Set([ProductId.analytics]);
|
||||
const activeProducts = new Set([ProductLine.security]);
|
||||
|
||||
const activeCards = setupCards(finishedSteps, activeProducts);
|
||||
|
||||
|
@ -202,7 +202,7 @@ describe('updateCard', () => {
|
|||
const finishedSteps = {
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
} as unknown as Record<CardId, Set<StepId>>;
|
||||
const activeProducts = new Set([ProductId.analytics, ProductId.cloud]);
|
||||
const activeProducts = new Set([ProductLine.security, ProductLine.cloud]);
|
||||
|
||||
const activeCards = {
|
||||
[SectionId.getSetUp]: {
|
||||
|
@ -221,8 +221,8 @@ describe('updateCard', () => {
|
|||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.protectYourEnvironmentInRuntime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRuntime,
|
||||
[GetSetUpCardId.protectYourEnvironmentInRealtime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRealtime,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
|
@ -244,7 +244,7 @@ describe('updateCard', () => {
|
|||
timeInMins: 0,
|
||||
},
|
||||
},
|
||||
} as Record<SectionId, Record<CardId, ActiveCard>>;
|
||||
} as ActiveCards;
|
||||
|
||||
it('should update the active card based on finished steps and active products', () => {
|
||||
const sectionId = SectionId.getSetUp;
|
||||
|
@ -273,7 +273,7 @@ describe('updateCard', () => {
|
|||
|
||||
it('should return null if the card is inactive based on active products', () => {
|
||||
const sectionId = SectionId.getSetUp;
|
||||
const cardId = GetSetUpCardId.protectYourEnvironmentInRuntime;
|
||||
const cardId = GetSetUpCardId.protectYourEnvironmentInRealtime;
|
||||
|
||||
const updatedCards = updateCard({
|
||||
finishedSteps,
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ProductLine } from '../../../common/config';
|
||||
import { getSections } from './sections';
|
||||
import { ActiveCard, Card, CardId, ProductId, SectionId, StepId } from './types';
|
||||
import { ActiveCard, ActiveCards, Card, CardId, SectionId, StepId } from './types';
|
||||
|
||||
export const getCardTimeInMinutes = (card: Card, stepsDone: Set<StepId>) =>
|
||||
card.steps?.reduce(
|
||||
|
@ -18,13 +19,13 @@ export const getCardTimeInMinutes = (card: Card, stepsDone: Set<StepId>) =>
|
|||
export const getCardStepsLeft = (card: Card, stepsDone: Set<StepId>) =>
|
||||
(card.steps?.length ?? 0) - (stepsDone.size ?? 0);
|
||||
|
||||
export const isCardActive = (card: Card, activeProducts: Set<ProductId>) =>
|
||||
!card.productTypeRequired ||
|
||||
card.productTypeRequired?.some((condition) => activeProducts.has(condition));
|
||||
export const isCardActive = (card: Card, activeProducts: Set<ProductLine>) =>
|
||||
!card.productLineRequired ||
|
||||
card.productLineRequired?.some((condition) => activeProducts.has(condition));
|
||||
|
||||
export const setupCards = (
|
||||
finishedSteps: Record<CardId, Set<StepId>>,
|
||||
activeProducts: Set<ProductId>
|
||||
activeProducts: Set<ProductLine>
|
||||
) =>
|
||||
activeProducts.size > 0
|
||||
? getSections().reduce((acc, section) => {
|
||||
|
@ -46,7 +47,7 @@ export const setupCards = (
|
|||
acc[section.id] = cardsInSections;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<SectionId, Record<CardId, ActiveCard>>)
|
||||
}, {} as ActiveCards)
|
||||
: null;
|
||||
|
||||
export const updateCard = ({
|
||||
|
@ -57,11 +58,11 @@ export const updateCard = ({
|
|||
cardId,
|
||||
}: {
|
||||
finishedSteps: Record<CardId, Set<StepId>>;
|
||||
activeProducts: Set<ProductId>;
|
||||
activeCards: Record<SectionId, Record<CardId, ActiveCard>> | null;
|
||||
activeProducts: Set<ProductLine>;
|
||||
activeCards: ActiveCards | null;
|
||||
sectionId: SectionId;
|
||||
cardId: CardId;
|
||||
}): Record<SectionId, Record<CardId, ActiveCard>> | null => {
|
||||
}): ActiveCards | null => {
|
||||
const sections = getSections();
|
||||
const section = sections.find(({ id }) => id === sectionId);
|
||||
const cards = section?.cards;
|
||||
|
|
|
@ -13,14 +13,16 @@ import type { GetStartedComponent } from './types';
|
|||
import { GetStarted } from './lazy';
|
||||
import { KibanaServicesProvider } from '../../services';
|
||||
import { ServerlessSecurityPluginStartDependencies } from '../../types';
|
||||
import { SecurityProductTypes } from '../../../common/config';
|
||||
|
||||
export const getSecurityGetStartedComponent = (
|
||||
core: CoreStart,
|
||||
pluginsStart: ServerlessSecurityPluginStartDependencies
|
||||
pluginsStart: ServerlessSecurityPluginStartDependencies,
|
||||
productTypes: SecurityProductTypes
|
||||
): GetStartedComponent => {
|
||||
return () => (
|
||||
<KibanaServicesProvider core={core} pluginsStart={pluginsStart}>
|
||||
<GetStarted />
|
||||
<GetStarted productTypes={productTypes} />
|
||||
</KibanaServicesProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,13 +6,14 @@
|
|||
*/
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingLogo } from '@elastic/eui';
|
||||
import { SecurityProductTypes } from '../../../common/config';
|
||||
|
||||
const GetStartedLazy = lazy(() => import('./get_started'));
|
||||
|
||||
const centerLogoStyle = { display: 'flex', margin: 'auto' };
|
||||
|
||||
export const GetStarted = () => (
|
||||
export const GetStarted = ({ productTypes }: { productTypes: SecurityProductTypes }) => (
|
||||
<Suspense fallback={<EuiLoadingLogo logo="logoSecurity" size="xl" style={centerLogoStyle} />}>
|
||||
<GetStartedLazy />
|
||||
<GetStartedLazy productTypes={productTypes} />
|
||||
</Suspense>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { ProductSwitch } from './product_switch';
|
||||
import { EuiThemeComputed } from '@elastic/eui';
|
||||
import { ProductId } from './types';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
|
||||
describe('ProductSwitch', () => {
|
||||
const onProductSwitchChangedMock = jest.fn();
|
||||
|
@ -49,12 +49,12 @@ describe('ProductSwitch', () => {
|
|||
fireEvent.click(analyticsSwitch);
|
||||
|
||||
expect(onProductSwitchChangedMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'analytics' })
|
||||
expect.objectContaining({ id: 'security' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should have checked switches for activeProducts', () => {
|
||||
const activeProducts = new Set([ProductId.analytics, ProductId.endpoint]);
|
||||
const activeProducts = new Set([ProductLine.security, ProductLine.endpoint]);
|
||||
const { getByTestId } = render(
|
||||
<ProductSwitch
|
||||
onProductSwitchChanged={onProductSwitchChangedMock}
|
||||
|
@ -63,7 +63,7 @@ describe('ProductSwitch', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const analyticsSwitch = getByTestId('analytics');
|
||||
const analyticsSwitch = getByTestId('security');
|
||||
const cloudSwitch = getByTestId('cloud');
|
||||
const endpointSwitch = getByTestId('endpoint');
|
||||
|
||||
|
|
|
@ -8,30 +8,30 @@
|
|||
import { EuiPanel, EuiSwitch, EuiText, EuiThemeComputed, EuiTitle } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
import * as i18n from './translations';
|
||||
import { ProductId, Switch } from './types';
|
||||
import { Switch } from './types';
|
||||
|
||||
const switches: Switch[] = [
|
||||
{
|
||||
id: ProductId.analytics,
|
||||
id: ProductLine.security,
|
||||
label: i18n.ANALYTICS_SWITCH_LABEL,
|
||||
},
|
||||
{
|
||||
id: ProductId.cloud,
|
||||
id: ProductLine.cloud,
|
||||
label: i18n.CLOUD_SWITCH_LABEL,
|
||||
},
|
||||
{
|
||||
id: ProductId.endpoint,
|
||||
id: ProductLine.endpoint,
|
||||
label: i18n.ENDPOINT_SWITCH_LABEL,
|
||||
},
|
||||
];
|
||||
|
||||
const ProductSwitchComponent: React.FC<{
|
||||
onProductSwitchChanged: (item: Switch) => void;
|
||||
activeProducts: Set<ProductId>;
|
||||
shadow?: string;
|
||||
activeProducts: Set<ProductLine>;
|
||||
euiTheme: EuiThemeComputed;
|
||||
}> = ({ onProductSwitchChanged, activeProducts, euiTheme, shadow = '' }) => {
|
||||
}> = ({ onProductSwitchChanged, activeProducts, euiTheme }) => {
|
||||
const switchNodes = useMemo(
|
||||
() =>
|
||||
switches.map((item) => (
|
||||
|
@ -58,8 +58,7 @@ const ProductSwitchComponent: React.FC<{
|
|||
paddingSize="none"
|
||||
hasShadow={false}
|
||||
css={css`
|
||||
padding: ${euiTheme.base * 1.25}px ${euiTheme.base * 2.25}px;
|
||||
${shadow};
|
||||
padding: ${euiTheme.base * 1.25}px 0;
|
||||
`}
|
||||
borderRadius="none"
|
||||
>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ProductLine } from '../../../common/config';
|
||||
import {
|
||||
reducer,
|
||||
getFinishedStepsInitialStates,
|
||||
|
@ -12,12 +13,11 @@ import {
|
|||
getActiveCardsInitialStates,
|
||||
} from './reducer';
|
||||
import {
|
||||
ActiveCard,
|
||||
ActiveCards,
|
||||
CardId,
|
||||
GetSetUpCardId,
|
||||
GetStartedPageActions,
|
||||
IntroductionSteps,
|
||||
ProductId,
|
||||
SectionId,
|
||||
StepId,
|
||||
ToggleProductAction,
|
||||
|
@ -28,25 +28,25 @@ import {
|
|||
describe('reducer', () => {
|
||||
it('should toggle section correctly', () => {
|
||||
const initialState = {
|
||||
activeProducts: new Set([ProductId.analytics]),
|
||||
activeProducts: new Set([ProductLine.security]),
|
||||
finishedSteps: {} as Record<CardId, Set<StepId>>,
|
||||
activeCards: {} as Record<SectionId, Record<CardId, ActiveCard>> | null,
|
||||
activeCards: {} as ActiveCards | null,
|
||||
};
|
||||
|
||||
const action: ToggleProductAction = {
|
||||
type: GetStartedPageActions.ToggleProduct,
|
||||
payload: { section: ProductId.analytics },
|
||||
payload: { section: ProductLine.security },
|
||||
};
|
||||
|
||||
const nextState = reducer(initialState, action);
|
||||
|
||||
expect(nextState.activeProducts.has(ProductId.analytics)).toBe(false);
|
||||
expect(nextState.activeProducts.has(ProductLine.security)).toBe(false);
|
||||
expect(nextState.activeCards).toBeNull();
|
||||
});
|
||||
|
||||
it('should add a finished step correctly', () => {
|
||||
const initialState = {
|
||||
activeProducts: new Set([ProductId.analytics]),
|
||||
activeProducts: new Set([ProductLine.security]),
|
||||
finishedSteps: {} as Record<CardId, Set<StepId>>,
|
||||
activeCards: {
|
||||
getSetUp: {
|
||||
|
@ -56,7 +56,7 @@ describe('reducer', () => {
|
|||
timeInMins: 3,
|
||||
},
|
||||
},
|
||||
} as unknown as Record<SectionId, Record<CardId, ActiveCard>> | null,
|
||||
} as unknown as ActiveCards | null,
|
||||
};
|
||||
|
||||
const action: AddFinishedStepAction = {
|
||||
|
@ -103,17 +103,17 @@ describe('getFinishedStepsInitialStates', () => {
|
|||
|
||||
describe('getActiveSectionsInitialStates', () => {
|
||||
it('should return the initial states of active sections correctly', () => {
|
||||
const activeProducts = [ProductId.analytics];
|
||||
const activeProducts = [ProductLine.security];
|
||||
|
||||
const initialStates = getActiveSectionsInitialStates({ activeProducts });
|
||||
|
||||
expect(initialStates.has(ProductId.analytics)).toBe(true);
|
||||
expect(initialStates.has(ProductLine.security)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveCardsInitialStates', () => {
|
||||
it('should return the initial states of active cards correctly', () => {
|
||||
const activeProducts = new Set([ProductId.analytics]);
|
||||
const activeProducts = new Set([ProductLine.security]);
|
||||
const finishedSteps = {
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
} as unknown as Record<CardId, Set<StepId>>;
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ProductLine } from '../../../common/config';
|
||||
import { setupCards, updateCard } from './helpers';
|
||||
import {
|
||||
CardId,
|
||||
GetStartedPageActions,
|
||||
ProductId,
|
||||
StepId,
|
||||
ToggleProductAction,
|
||||
TogglePanelReducer,
|
||||
|
@ -80,13 +80,13 @@ export const getFinishedStepsInitialStates = ({
|
|||
export const getActiveSectionsInitialStates = ({
|
||||
activeProducts,
|
||||
}: {
|
||||
activeProducts: ProductId[];
|
||||
activeProducts: ProductLine[];
|
||||
}) => new Set(activeProducts);
|
||||
|
||||
export const getActiveCardsInitialStates = ({
|
||||
activeProducts,
|
||||
finishedSteps,
|
||||
}: {
|
||||
activeProducts: Set<ProductId>;
|
||||
activeProducts: Set<ProductLine>;
|
||||
finishedSteps: Record<CardId, Set<StepId>>;
|
||||
}) => setupCards(finishedSteps, activeProducts);
|
||||
|
|
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
|
||||
import {
|
||||
Section,
|
||||
ProductId,
|
||||
SectionId,
|
||||
GetMoreFromElasticSecurityCardId,
|
||||
GetSetUpCardId,
|
||||
|
@ -17,12 +16,7 @@ import {
|
|||
import * as i18n from './translations';
|
||||
import respond from './images/respond.svg';
|
||||
import protect from './images/protect.svg';
|
||||
|
||||
export const ActiveConditions = {
|
||||
analyticsToggled: [ProductId.analytics],
|
||||
cloudToggled: [ProductId.cloud],
|
||||
endpointToggled: [ProductId.endpoint],
|
||||
};
|
||||
import { ProductLine } from '../../../common/config';
|
||||
|
||||
export const introductionSteps = [
|
||||
{
|
||||
|
@ -85,11 +79,8 @@ export const sections: Section[] = [
|
|||
{
|
||||
icon: { type: protect, size: 'xl' },
|
||||
title: i18n.PROTECT_YOUR_ENVIRONMENT_TITLE,
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRuntime,
|
||||
productTypeRequired: [
|
||||
...ActiveConditions.cloudToggled,
|
||||
...ActiveConditions.endpointToggled,
|
||||
],
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRealtime,
|
||||
productLineRequired: [ProductLine.cloud, ProductLine.endpoint],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TogglePanel } from './toggle_panel';
|
||||
import { getStartedStorage as mockGetStartedStorage } from '../../lib/get_started/storage';
|
||||
import { useSetUpCardSections } from './use_setup_cards';
|
||||
import { ActiveCards, CardId, GetSetUpCardId, IntroductionSteps, SectionId, StepId } from './types';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
|
||||
jest.mock('@elastic/eui', () => ({
|
||||
...jest.requireActual('@elastic/eui'),
|
||||
|
@ -30,23 +31,57 @@ jest.mock('./use_setup_cards', () => ({
|
|||
useSetUpCardSections: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('TogglePanel', () => {
|
||||
const mockUseSetUpCardSections = { setUpSections: jest.fn(() => null) };
|
||||
const finishedSteps = {
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
} as unknown as Record<CardId, Set<StepId>>;
|
||||
const activeProducts = new Set([ProductLine.security, ProductLine.cloud]);
|
||||
|
||||
const activeCards = {
|
||||
[SectionId.getSetUp]: {
|
||||
[GetSetUpCardId.introduction]: {
|
||||
id: GetSetUpCardId.introduction,
|
||||
timeInMins: 3,
|
||||
stepsLeft: 1,
|
||||
},
|
||||
[GetSetUpCardId.bringInYourData]: {
|
||||
id: GetSetUpCardId.bringInYourData,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.activateAndCreateRules]: {
|
||||
id: GetSetUpCardId.activateAndCreateRules,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.protectYourEnvironmentInRealtime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRealtime,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
} as ActiveCards;
|
||||
|
||||
describe('TogglePanel', () => {
|
||||
const mockUseSetUpCardSections = {
|
||||
setUpSections: jest.fn(() => <div data-test-subj="mock-sections" />),
|
||||
};
|
||||
|
||||
const onStepClicked = jest.fn();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useSetUpCardSections as jest.Mock).mockReturnValue(mockUseSetUpCardSections);
|
||||
});
|
||||
|
||||
it('should render the product switch ', () => {
|
||||
const { getByTestId } = render(<TogglePanel />);
|
||||
|
||||
expect(getByTestId('product-switch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty prompt', () => {
|
||||
const { getByText } = render(<TogglePanel />);
|
||||
const { getByText } = render(
|
||||
<TogglePanel
|
||||
activeProducts={new Set()}
|
||||
finishedSteps={finishedSteps}
|
||||
activeCards={activeCards}
|
||||
onStepClicked={onStepClicked}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText(`Hmm, there doesn't seem to be anything there`)).toBeInTheDocument();
|
||||
expect(
|
||||
|
@ -54,16 +89,16 @@ describe('TogglePanel', () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle active sections when a product switch is changed', () => {
|
||||
const { getByText } = render(<TogglePanel />);
|
||||
it('should render sections', () => {
|
||||
const { getByTestId } = render(
|
||||
<TogglePanel
|
||||
activeProducts={activeProducts}
|
||||
finishedSteps={finishedSteps}
|
||||
activeCards={activeCards}
|
||||
onStepClicked={onStepClicked}
|
||||
/>
|
||||
);
|
||||
|
||||
const analyticsSwitch = getByText('Analytics');
|
||||
const cloudSwitch = getByText('Cloud');
|
||||
|
||||
fireEvent.click(analyticsSwitch);
|
||||
expect(mockGetStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith('analytics');
|
||||
|
||||
fireEvent.click(cloudSwitch);
|
||||
expect(mockGetStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith('cloud');
|
||||
expect(getByTestId(`mock-sections`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,98 +5,46 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useReducer } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, useEuiShadow, useEuiTheme } from '@elastic/eui';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { Switch, GetStartedPageActions, StepId, CardId, SectionId } from './types';
|
||||
import * as i18n from './translations';
|
||||
import { ProductSwitch } from './product_switch';
|
||||
import { useSetUpCardSections } from './use_setup_cards';
|
||||
import { getStartedStorage } from '../../lib/get_started/storage';
|
||||
import {
|
||||
getActiveCardsInitialStates,
|
||||
getActiveSectionsInitialStates,
|
||||
getFinishedStepsInitialStates,
|
||||
reducer,
|
||||
} from './reducer';
|
||||
|
||||
const TogglePanelComponent = () => {
|
||||
import { ActiveCards, CardId, IntroductionSteps, SectionId } from './types';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
|
||||
const TogglePanelComponent: React.FC<{
|
||||
finishedSteps: Record<CardId, Set<IntroductionSteps>>;
|
||||
activeCards: ActiveCards | null;
|
||||
activeProducts: Set<ProductLine>;
|
||||
onStepClicked: ({
|
||||
stepId,
|
||||
cardId,
|
||||
sectionId,
|
||||
}: {
|
||||
stepId: IntroductionSteps;
|
||||
cardId: CardId;
|
||||
sectionId: SectionId;
|
||||
}) => void;
|
||||
}> = ({ finishedSteps, activeCards, activeProducts, onStepClicked }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const shadow = useEuiShadow('s');
|
||||
const {
|
||||
getAllFinishedStepsFromStorage,
|
||||
getActiveProductsFromStorage,
|
||||
toggleActiveProductsInStorage,
|
||||
addFinishedStepToStorage,
|
||||
} = getStartedStorage;
|
||||
const finishedStepsInitialStates = useMemo(
|
||||
() => getFinishedStepsInitialStates({ finishedSteps: getAllFinishedStepsFromStorage() }),
|
||||
[getAllFinishedStepsFromStorage]
|
||||
);
|
||||
|
||||
const activeSectionsInitialStates = useMemo(
|
||||
() => getActiveSectionsInitialStates({ activeProducts: getActiveProductsFromStorage() }),
|
||||
[getActiveProductsFromStorage]
|
||||
);
|
||||
|
||||
const activeCardsInitialStates = useMemo(
|
||||
() =>
|
||||
getActiveCardsInitialStates({
|
||||
activeProducts: activeSectionsInitialStates,
|
||||
finishedSteps: finishedStepsInitialStates,
|
||||
}),
|
||||
[activeSectionsInitialStates, finishedStepsInitialStates]
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
activeProducts: activeSectionsInitialStates,
|
||||
finishedSteps: finishedStepsInitialStates,
|
||||
activeCards: activeCardsInitialStates,
|
||||
});
|
||||
const { setUpSections } = useSetUpCardSections({ euiTheme, shadow });
|
||||
const onStepClicked = useCallback(
|
||||
({ stepId, cardId, sectionId }: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => {
|
||||
dispatch({
|
||||
type: GetStartedPageActions.AddFinishedStep,
|
||||
payload: { stepId, cardId, sectionId },
|
||||
});
|
||||
addFinishedStepToStorage(cardId, stepId);
|
||||
},
|
||||
[addFinishedStepToStorage]
|
||||
);
|
||||
const sectionNodes = setUpSections({
|
||||
onStepClicked,
|
||||
finishedSteps: state.finishedSteps,
|
||||
activeCards: state.activeCards,
|
||||
finishedSteps,
|
||||
activeCards,
|
||||
});
|
||||
const onProductSwitchChanged = useCallback(
|
||||
(section: Switch) => {
|
||||
dispatch({ type: GetStartedPageActions.ToggleProduct, payload: { section: section.id } });
|
||||
toggleActiveProductsInStorage(section.id);
|
||||
},
|
||||
[toggleActiveProductsInStorage]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ProductSwitch
|
||||
onProductSwitchChanged={onProductSwitchChanged}
|
||||
activeProducts={state.activeProducts}
|
||||
euiTheme={euiTheme}
|
||||
shadow={shadow}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
padding: ${euiTheme.size.xs} ${euiTheme.base * 2.25}px;
|
||||
`}
|
||||
grow={1}
|
||||
>
|
||||
{state.activeProducts.size > 0 ? (
|
||||
<EuiFlexItem grow={1}>
|
||||
{activeProducts.size > 0 ? (
|
||||
sectionNodes
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
|
|
|
@ -157,9 +157,9 @@ export const ACTIVATE_AND_CREATE_RULES_TITLE = i18n.translate(
|
|||
);
|
||||
|
||||
export const PROTECT_YOUR_ENVIRONMENT_TITLE = i18n.translate(
|
||||
'xpack.serverlessSecurity.getStarted.togglePanel.protectYourEnvironmentInRuntime.title',
|
||||
'xpack.serverlessSecurity.getStarted.togglePanel.protectYourEnvironmentInRealtime.title',
|
||||
{
|
||||
defaultMessage: 'Protect your environment in runtime',
|
||||
defaultMessage: 'Protect your environment in realtime',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type GetStartedComponentProps = {};
|
||||
|
@ -46,7 +47,7 @@ export interface Step {
|
|||
export type CardId = GetSetUpCardId | GetMoreFromElasticSecurityCardId;
|
||||
|
||||
export interface Card {
|
||||
productTypeRequired?: ProductId[];
|
||||
productLineRequired?: ProductLine[];
|
||||
description?: string | React.ReactNode;
|
||||
icon?: EuiIconProps;
|
||||
id: CardId;
|
||||
|
@ -56,11 +57,7 @@ export interface Card {
|
|||
stepsLeft?: number;
|
||||
}
|
||||
|
||||
export enum ProductId {
|
||||
analytics = 'analytics',
|
||||
cloud = 'cloud',
|
||||
endpoint = 'endpoint',
|
||||
}
|
||||
export type ActiveCards = Record<SectionId, Record<CardId, ActiveCard>>;
|
||||
|
||||
export enum SectionId {
|
||||
getSetUp = 'getSetUp',
|
||||
|
@ -71,7 +68,7 @@ export enum GetSetUpCardId {
|
|||
activateAndCreateRules = 'activateAndCreateRules',
|
||||
bringInYourData = 'bringInYourData',
|
||||
introduction = 'introduction',
|
||||
protectYourEnvironmentInRuntime = 'protectYourEnvironmentInRuntime',
|
||||
protectYourEnvironmentInRealtime = 'protectYourEnvironmentInRealtime',
|
||||
}
|
||||
|
||||
export enum IntroductionSteps {
|
||||
|
@ -90,14 +87,14 @@ export interface ActiveCard {
|
|||
stepsLeft: number;
|
||||
}
|
||||
export interface TogglePanelReducer {
|
||||
activeProducts: Set<ProductId>;
|
||||
activeProducts: Set<ProductLine>;
|
||||
finishedSteps: Record<CardId, Set<StepId>>;
|
||||
activeCards: Record<SectionId, Record<CardId, ActiveCard>> | null;
|
||||
}
|
||||
|
||||
export interface ToggleProductAction {
|
||||
type: GetStartedPageActions.ToggleProduct;
|
||||
payload: { section: ProductId };
|
||||
payload: { section: ProductLine };
|
||||
}
|
||||
|
||||
export interface AddFinishedStepAction {
|
||||
|
@ -106,7 +103,7 @@ export interface AddFinishedStepAction {
|
|||
}
|
||||
|
||||
export interface Switch {
|
||||
id: ProductId;
|
||||
id: ProductLine;
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { renderHook } from '@testing-library/react-hooks';
|
|||
import { EuiThemeComputed } from '@elastic/eui';
|
||||
import { useSetUpCardSections } from './use_setup_cards';
|
||||
import {
|
||||
ActiveCard,
|
||||
ActiveCards,
|
||||
CardId,
|
||||
GetMoreFromElasticSecurityCardId,
|
||||
GetSetUpCardId,
|
||||
|
@ -44,7 +44,7 @@ describe('useSetUpCardSections', () => {
|
|||
id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow,
|
||||
},
|
||||
},
|
||||
} as Record<SectionId, Record<CardId, ActiveCard>>;
|
||||
} as ActiveCards;
|
||||
|
||||
const sections = result.current.setUpSections({
|
||||
activeCards,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { EuiSpacer, EuiThemeComputed } from '@elastic/eui';
|
|||
import React, { useCallback } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { ActiveCard, CardId, SectionId, StepId } from './types';
|
||||
import { ActiveCards, CardId, SectionId, StepId } from './types';
|
||||
|
||||
import { CardItem } from './card_item';
|
||||
import { getSections } from './sections';
|
||||
|
@ -30,7 +30,7 @@ export const useSetUpCardSections = ({
|
|||
}: {
|
||||
onStepClicked: (params: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => void;
|
||||
finishedSteps: Record<CardId, Set<StepId>>;
|
||||
activeCards: Record<SectionId, Record<CardId, ActiveCard>> | null;
|
||||
activeCards: ActiveCards | null;
|
||||
sectionId: SectionId;
|
||||
}) => {
|
||||
const section = activeCards?.[sectionId];
|
||||
|
@ -63,7 +63,7 @@ export const useSetUpCardSections = ({
|
|||
}: {
|
||||
onStepClicked: (params: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => void;
|
||||
finishedSteps: Record<CardId, Set<StepId>>;
|
||||
activeCards: Record<SectionId, Record<CardId, ActiveCard>> | null;
|
||||
activeCards: ActiveCards | null;
|
||||
}) =>
|
||||
getSections().reduce<React.ReactNode[]>((acc, currentSection) => {
|
||||
const cardNodes = setUpCards({
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* 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 { getStartedStorage } from '../../lib/get_started/storage';
|
||||
import { ProductLine, SecurityProductTypes } from '../../../common/config';
|
||||
import {
|
||||
GetMoreFromElasticSecurityCardId,
|
||||
GetSetUpCardId,
|
||||
IntroductionSteps,
|
||||
SectionId,
|
||||
} from './types';
|
||||
|
||||
jest.mock('../../lib/get_started/storage');
|
||||
|
||||
describe('useTogglePanel', () => {
|
||||
const productTypes = [
|
||||
{ product_line: 'security', product_tier: 'essentials' },
|
||||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
] as SecurityProductTypes;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(getStartedStorage.getAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
});
|
||||
(getStartedStorage.getActiveProductsFromStorage 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', () => {
|
||||
(getStartedStorage.getAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({});
|
||||
(getStartedStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const { result } = renderHook(() => useTogglePanel({ productTypes }));
|
||||
|
||||
const { state } = result.current;
|
||||
|
||||
expect(state.activeProducts).toEqual(new Set([ProductLine.security, ProductLine.endpoint]));
|
||||
expect(state.finishedSteps).toEqual({});
|
||||
|
||||
expect(state.activeCards).toEqual(
|
||||
expect.objectContaining({
|
||||
[SectionId.getSetUp]: {
|
||||
[GetSetUpCardId.introduction]: {
|
||||
id: GetSetUpCardId.introduction,
|
||||
timeInMins: 3,
|
||||
stepsLeft: 1,
|
||||
},
|
||||
[GetSetUpCardId.activateAndCreateRules]: {
|
||||
id: GetSetUpCardId.activateAndCreateRules,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.bringInYourData]: {
|
||||
id: GetSetUpCardId.bringInYourData,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.protectYourEnvironmentInRealtime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRealtime,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
[SectionId.getMoreFromElasticSecurity]: {
|
||||
[GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow]: {
|
||||
id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace]: {
|
||||
id: GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetMoreFromElasticSecurityCardId.respondToThreats]: {
|
||||
id: GetMoreFromElasticSecurityCardId.respondToThreats,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should initialize state with correct initial values - when all products active', () => {
|
||||
const { result } = renderHook(() => useTogglePanel({ productTypes }));
|
||||
|
||||
const { state } = result.current;
|
||||
|
||||
expect(state.activeProducts).toEqual(
|
||||
new Set([ProductLine.security, ProductLine.cloud, ProductLine.endpoint])
|
||||
);
|
||||
expect(state.finishedSteps).toEqual({
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
});
|
||||
|
||||
expect(state.activeCards).toEqual(
|
||||
expect.objectContaining({
|
||||
[SectionId.getSetUp]: {
|
||||
[GetSetUpCardId.introduction]: {
|
||||
id: GetSetUpCardId.introduction,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.activateAndCreateRules]: {
|
||||
id: GetSetUpCardId.activateAndCreateRules,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.bringInYourData]: {
|
||||
id: GetSetUpCardId.bringInYourData,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.protectYourEnvironmentInRealtime]: {
|
||||
id: GetSetUpCardId.protectYourEnvironmentInRealtime,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
[SectionId.getMoreFromElasticSecurity]: {
|
||||
[GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow]: {
|
||||
id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace]: {
|
||||
id: GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetMoreFromElasticSecurityCardId.respondToThreats]: {
|
||||
id: GetMoreFromElasticSecurityCardId.respondToThreats,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should initialize state with correct initial values - when only security product active', () => {
|
||||
(getStartedStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([
|
||||
ProductLine.security,
|
||||
]);
|
||||
const { result } = renderHook(() => useTogglePanel({ productTypes }));
|
||||
|
||||
const { state } = result.current;
|
||||
|
||||
expect(state.activeProducts).toEqual(new Set([ProductLine.security]));
|
||||
expect(state.finishedSteps).toEqual({
|
||||
[GetSetUpCardId.introduction]: new Set([IntroductionSteps.watchOverviewVideo]),
|
||||
});
|
||||
|
||||
expect(state.activeCards).toEqual(
|
||||
expect.objectContaining({
|
||||
[SectionId.getSetUp]: {
|
||||
[GetSetUpCardId.introduction]: {
|
||||
id: GetSetUpCardId.introduction,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.activateAndCreateRules]: {
|
||||
id: GetSetUpCardId.activateAndCreateRules,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetSetUpCardId.bringInYourData]: {
|
||||
id: GetSetUpCardId.bringInYourData,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
[SectionId.getMoreFromElasticSecurity]: {
|
||||
[GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow]: {
|
||||
id: GetMoreFromElasticSecurityCardId.masterTheInvestigationsWorkflow,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace]: {
|
||||
id: GetMoreFromElasticSecurityCardId.optimizeYourWorkSpace,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
[GetMoreFromElasticSecurityCardId.respondToThreats]: {
|
||||
id: GetMoreFromElasticSecurityCardId.respondToThreats,
|
||||
timeInMins: 0,
|
||||
stepsLeft: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should call addFinishedStepToStorage', () => {
|
||||
const { result } = renderHook(() => useTogglePanel({ productTypes }));
|
||||
|
||||
const { onStepClicked } = result.current;
|
||||
|
||||
act(() => {
|
||||
onStepClicked({
|
||||
stepId: IntroductionSteps.watchOverviewVideo,
|
||||
cardId: GetSetUpCardId.introduction,
|
||||
sectionId: SectionId.getSetUp,
|
||||
});
|
||||
});
|
||||
|
||||
expect(getStartedStorage.addFinishedStepToStorage).toHaveBeenCalledTimes(1);
|
||||
expect(getStartedStorage.addFinishedStepToStorage).toHaveBeenCalledWith(
|
||||
GetSetUpCardId.introduction,
|
||||
IntroductionSteps.watchOverviewVideo
|
||||
);
|
||||
});
|
||||
|
||||
test('should call toggleActiveProductsInStorage', () => {
|
||||
const { result } = renderHook(() => useTogglePanel({ productTypes }));
|
||||
|
||||
const { onProductSwitchChanged } = result.current;
|
||||
|
||||
act(() => {
|
||||
onProductSwitchChanged({ id: ProductLine.security, label: 'Analytics' });
|
||||
});
|
||||
|
||||
expect(getStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledTimes(1);
|
||||
expect(getStartedStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith(
|
||||
ProductLine.security
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useReducer } from 'react';
|
||||
import { ProductLine, SecurityProductTypes } from '../../../common/config';
|
||||
import { getStartedStorage } from '../../lib/get_started/storage';
|
||||
import {
|
||||
getActiveCardsInitialStates,
|
||||
getActiveSectionsInitialStates,
|
||||
getFinishedStepsInitialStates,
|
||||
reducer,
|
||||
} from './reducer';
|
||||
import { CardId, GetStartedPageActions, SectionId, StepId, Switch } from './types';
|
||||
|
||||
export const useTogglePanel = ({ productTypes }: { productTypes: SecurityProductTypes }) => {
|
||||
const {
|
||||
getAllFinishedStepsFromStorage,
|
||||
getActiveProductsFromStorage,
|
||||
toggleActiveProductsInStorage,
|
||||
addFinishedStepToStorage,
|
||||
} = getStartedStorage;
|
||||
|
||||
const finishedStepsInitialStates = useMemo(
|
||||
() => getFinishedStepsInitialStates({ finishedSteps: getAllFinishedStepsFromStorage() }),
|
||||
[getAllFinishedStepsFromStorage]
|
||||
);
|
||||
|
||||
const activeSectionsInitialStates = useMemo(() => {
|
||||
const activeProductsFromStorage = getActiveSectionsInitialStates({
|
||||
activeProducts: getActiveProductsFromStorage(),
|
||||
});
|
||||
return activeProductsFromStorage.size > 0
|
||||
? activeProductsFromStorage
|
||||
: new Set(productTypes.map(({ product_line: productLine }) => ProductLine[productLine])) ??
|
||||
new Set([ProductLine.security, ProductLine.endpoint, ProductLine.cloud]);
|
||||
}, [getActiveProductsFromStorage, productTypes]);
|
||||
|
||||
const activeCardsInitialStates = useMemo(
|
||||
() =>
|
||||
getActiveCardsInitialStates({
|
||||
activeProducts: activeSectionsInitialStates,
|
||||
finishedSteps: finishedStepsInitialStates,
|
||||
}),
|
||||
[activeSectionsInitialStates, finishedStepsInitialStates]
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
activeProducts: activeSectionsInitialStates,
|
||||
finishedSteps: finishedStepsInitialStates,
|
||||
activeCards: activeCardsInitialStates,
|
||||
});
|
||||
|
||||
const onStepClicked = useCallback(
|
||||
({ stepId, cardId, sectionId }: { stepId: StepId; cardId: CardId; sectionId: SectionId }) => {
|
||||
dispatch({
|
||||
type: GetStartedPageActions.AddFinishedStep,
|
||||
payload: { stepId, cardId, sectionId },
|
||||
});
|
||||
addFinishedStepToStorage(cardId, stepId);
|
||||
},
|
||||
[addFinishedStepToStorage]
|
||||
);
|
||||
|
||||
const onProductSwitchChanged = useCallback(
|
||||
(section: Switch) => {
|
||||
dispatch({ type: GetStartedPageActions.ToggleProduct, payload: { section: section.id } });
|
||||
toggleActiveProductsInStorage(section.id);
|
||||
},
|
||||
[toggleActiveProductsInStorage]
|
||||
);
|
||||
|
||||
return { state, onStepClicked, onProductSwitchChanged };
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
import { UpsellingService } from '@kbn/security-solution-plugin/public';
|
||||
import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-plugin/common';
|
||||
import { registerUpsellings, upsellingPages, upsellingSections } from './register_upsellings';
|
||||
import type { SecurityProductTypes } from '../../../common/config';
|
||||
import { ProductLine, SecurityProductTypes } from '../../../common/config';
|
||||
|
||||
const mockGetProductAppFeatures = jest.fn();
|
||||
jest.mock('../../../common/pli/pli_features', () => ({
|
||||
|
@ -16,9 +16,9 @@ jest.mock('../../../common/pli/pli_features', () => ({
|
|||
}));
|
||||
|
||||
const allProductTypes: SecurityProductTypes = [
|
||||
{ product_line: 'security', product_tier: 'complete' },
|
||||
{ product_line: 'endpoint', product_tier: 'complete' },
|
||||
{ product_line: 'cloud', product_tier: 'complete' },
|
||||
{ product_line: ProductLine.security, product_tier: 'complete' },
|
||||
{ product_line: ProductLine.endpoint, product_tier: 'complete' },
|
||||
{ product_line: ProductLine.cloud, product_tier: 'complete' },
|
||||
];
|
||||
|
||||
describe('registerUpsellings', () => {
|
||||
|
|
|
@ -6,14 +6,10 @@
|
|||
*/
|
||||
|
||||
import { getStartedStorage } from './storage';
|
||||
import {
|
||||
GetSetUpCardId,
|
||||
IntroductionSteps,
|
||||
ProductId,
|
||||
StepId,
|
||||
} from '../../components/get_started/types';
|
||||
import { GetSetUpCardId, IntroductionSteps, StepId } from '../../components/get_started/types';
|
||||
import { storage } from '../storage';
|
||||
import { MockStorage } from '../__mocks__/storage';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
|
||||
jest.mock('../storage');
|
||||
|
||||
|
@ -33,13 +29,13 @@ describe('useStorage', () => {
|
|||
});
|
||||
|
||||
it('should toggle active products in storage', () => {
|
||||
expect(getStartedStorage.toggleActiveProductsInStorage(ProductId.analytics)).toEqual([
|
||||
ProductId.analytics,
|
||||
expect(getStartedStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([
|
||||
ProductLine.security,
|
||||
]);
|
||||
expect(mockStorage.set).toHaveBeenCalledWith('ACTIVE_PRODUCTS', [ProductId.analytics]);
|
||||
expect(mockStorage.set).toHaveBeenCalledWith('ACTIVE_PRODUCTS', [ProductLine.security]);
|
||||
|
||||
mockStorage.set('ACTIVE_PRODUCTS', [ProductId.analytics]);
|
||||
expect(getStartedStorage.toggleActiveProductsInStorage(ProductId.analytics)).toEqual([]);
|
||||
mockStorage.set('ACTIVE_PRODUCTS', [ProductLine.security]);
|
||||
expect(getStartedStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([]);
|
||||
expect(mockStorage.set).toHaveBeenCalledWith('ACTIVE_PRODUCTS', []);
|
||||
});
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CardId, ProductId, StepId } from '../../components/get_started/types';
|
||||
import { ProductLine } from '../../../common/config';
|
||||
import { CardId, StepId } from '../../components/get_started/types';
|
||||
import { storage } from '../storage';
|
||||
|
||||
export const ACTIVE_PRODUCTS_STORAGE_KEY = 'ACTIVE_PRODUCTS';
|
||||
|
@ -13,12 +14,12 @@ export const FINISHED_STEPS_STORAGE_KEY = 'FINISHED_STEPS';
|
|||
|
||||
export const getStartedStorage = {
|
||||
getActiveProductsFromStorage: () => {
|
||||
const activeProducts: ProductId[] = storage.get(ACTIVE_PRODUCTS_STORAGE_KEY);
|
||||
const activeProducts: ProductLine[] = storage.get(ACTIVE_PRODUCTS_STORAGE_KEY);
|
||||
return activeProducts ?? new Array();
|
||||
},
|
||||
toggleActiveProductsInStorage: (productId: ProductId) => {
|
||||
const activeProducts: ProductId[] =
|
||||
storage.get(ACTIVE_PRODUCTS_STORAGE_KEY) ?? new Array<ProductId>();
|
||||
toggleActiveProductsInStorage: (productId: ProductLine) => {
|
||||
const activeProducts: ProductLine[] =
|
||||
storage.get(ACTIVE_PRODUCTS_STORAGE_KEY) ?? new Array<ProductLine>();
|
||||
const index = activeProducts.indexOf(productId);
|
||||
if (index < 0) {
|
||||
activeProducts.push(productId);
|
||||
|
|
|
@ -48,7 +48,9 @@ export class ServerlessSecurityPlugin
|
|||
const { securitySolution, serverless } = startDeps;
|
||||
|
||||
securitySolution.setIsSidebarEnabled(false);
|
||||
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(core, startDeps));
|
||||
securitySolution.setGetStartedPage(
|
||||
getSecurityGetStartedComponent(core, startDeps, this.config.productTypes)
|
||||
);
|
||||
serverless.setProjectHome('/app/security');
|
||||
serverless.setSideNavComponent(getSecuritySideNavComponent(core, startDeps));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue