Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,6 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG_PREFIX = `${APP_ID}:limitedConcurrenc
*/
export const RULES_TABLE_MAX_PAGE_SIZE = 100;
export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAGE_SIZE];

export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY =
'securitySolution.rulesManagementPage.newFeaturesTour.v8.1';
29 changes: 26 additions & 3 deletions x-pack/plugins/security_solution/cypress/tasks/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as yaml from 'js-yaml';
import Url, { UrlObject } from 'url';

import { ROLES } from '../../common/test';
import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../common/constants';
import { TIMELINE_FLYOUT_BODY } from '../screens/timeline';
import { hostDetailsUrl, LOGOUT_URL } from '../urls/navigation';

Expand Down Expand Up @@ -284,6 +285,21 @@ export const getEnvAuth = (): User => {
}
};

/**
* Saves in localStorage rules feature tour config with deactivated option
* It prevents tour to appear during tests and cover UI elements
* @param window - browser's window object
*/
const disableRulesFeatureTour = (window: Window) => {
const tourConfig = {
isTourActive: false,
};
window.localStorage.setItem(
RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY,
JSON.stringify(tourConfig)
);
};

/**
* Authenticates with Kibana, visits the specified `url`, and waits for the
* Kibana global nav to be displayed before continuing
Expand All @@ -301,6 +317,7 @@ export const loginAndWaitForPage = (
if (onBeforeLoadCallback) {
onBeforeLoadCallback(win);
}
disableRulesFeatureTour(win);
},
}
);
Expand All @@ -315,21 +332,27 @@ export const waitForPage = (url: string) => {

export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => {
login(role);
cy.visit(role ? getUrlWithRoute(role, url) : url);
cy.visit(role ? getUrlWithRoute(role, url) : url, {
onBeforeLoad: disableRulesFeatureTour,
});
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
};

export const loginWithUserAndWaitForPageWithoutDateRange = (url: string, user: User) => {
loginWithUser(user);
cy.visit(constructUrlWithUser(user, url));
cy.visit(constructUrlWithUser(user, url), {
onBeforeLoad: disableRulesFeatureTour,
});
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
};

export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => {
const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`;

login(role);
cy.visit(role ? getUrlWithRoute(role, route) : route);
cy.visit(role ? getUrlWithRoute(role, route) : route, {
onBeforeLoad: disableRulesFeatureTour,
});
cy.get('[data-test-subj="headerGlobalNav"]');
cy.get(TIMELINE_FLYOUT_BODY).should('be.visible');
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ const Popover = React.memo<UtilityBarActionProps>(
ownFocus,
dataTestSubj,
popoverPanelPaddingSize,
onClick,
}) => {
const [popoverState, setPopoverState] = useState(false);

const closePopover = useCallback(() => setPopoverState(false), [setPopoverState]);

const handleLinkIconClick = useCallback(() => {
onClick?.();
setPopoverState(!popoverState);
}, [popoverState, onClick]);

return (
<EuiPopover
ownFocus={ownFocus}
Expand All @@ -51,13 +57,13 @@ const Popover = React.memo<UtilityBarActionProps>(
iconSide={iconSide}
iconSize={iconSize}
iconType={iconType}
onClick={() => setPopoverState(!popoverState)}
onClick={handleLinkIconClick}
disabled={disabled}
>
{children}
</LinkIcon>
}
closePopover={() => setPopoverState(false)}
closePopover={closePopover}
isOpen={popoverState}
repositionOnScroll
>
Expand Down Expand Up @@ -107,6 +113,7 @@ export const UtilityBarAction = React.memo<UtilityBarActionProps>(
<BarAction data-test-subj={dataTestSubj}>
{popoverContent ? (
<Popover
onClick={onClick}
dataTestSubj={`${dataTestSubj}-popover`}
disabled={disabled}
color={color}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { TagsForm } from './forms/tags_form';

interface BulkEditFlyoutProps {
onClose: () => void;
onConfirm: (bulkactionEditPayload: BulkActionEditPayload) => void;
onConfirm: (bulkActionEditPayload: BulkActionEditPayload) => void;
editAction: BulkActionEditType;
rulesCount: number;
tags: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TestProviders } from '../../../../../common/mock';
import '../../../../../common/mock/formatted_relative';
import '../../../../../common/mock/match_media';
import { AllRules } from './index';
import { RulesFeatureTourContextProvider } from './rules_feature_tour_context';

jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../../common/lib/kibana');
Expand Down Expand Up @@ -67,7 +68,8 @@ describe('AllRules', () => {
rulesNotInstalled={0}
rulesNotUpdated={0}
/>
</TestProviders>
</TestProviders>,
{ wrappingComponent: RulesFeatureTourContextProvider }
);

await waitFor(() => {
Expand All @@ -90,7 +92,8 @@ describe('AllRules', () => {
rulesNotInstalled={0}
rulesNotUpdated={0}
/>
</TestProviders>
</TestProviders>,
{ wrappingComponent: RulesFeatureTourContextProvider }
);

await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const AllRules = React.memo<AllRulesProps>(

return (
<>
<RulesTableToolbar activeTab={activeTab} onTabChange={setActiveTab} />
<RulesTableToolbar activeTab={activeTab} onTabChange={setActiveTab} loading={loading} />
<EuiSpacer />
<RulesTables
createPrePackagedRules={createPrePackagedRules}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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, { FC } from 'react';

import { EuiTourStepProps, EuiTourStep } from '@elastic/eui';

/**
* This component can be used for tour steps, when tour step is optional
* If stepProps are not supplied, step will not be rendered, only children component will be
*/
export const OptionalEuiTourStep: FC<{ stepProps: EuiTourStepProps | undefined }> = ({
children,
stepProps,
}) => {
if (!stepProps) {
return <>{children}</>;
}

return (
<EuiTourStep {...stepProps}>
<>{children}</>
</EuiTourStep>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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, { createContext, useContext, useEffect, useMemo, FC } from 'react';

import { noop } from 'lodash';
import {
useEuiTour,
EuiTourState,
EuiStatelessTourStep,
EuiSpacer,
EuiButton,
EuiTourStepProps,
} from '@elastic/eui';
import { invariant } from '../../../../../../common/utils/invariant';
import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../../../../../common/constants';
import { useKibana } from '../../../../../common/lib/kibana';

import * as i18n from '../translations';

export interface RulesFeatureTourContextType {
steps: {
inMemoryTableStepProps: EuiTourStepProps;
bulkActionsStepProps: EuiTourStepProps;
};
goToNextStep: () => void;
finishTour: () => void;
}

const TOUR_POPOVER_WIDTH = 360;

const featuresTourSteps: EuiStatelessTourStep[] = [
{
step: 1,
title: i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP_TITLE,
content: <></>,
stepsTotal: 2,
children: <></>,
onFinish: noop,
maxWidth: TOUR_POPOVER_WIDTH,
},
{
step: 2,
title: i18n.FEATURE_TOUR_BULK_ACTIONS_STEP_TITLE,
content: <p>{i18n.FEATURE_TOUR_BULK_ACTIONS_STEP}</p>,
stepsTotal: 2,
children: <></>,
onFinish: noop,
anchorPosition: 'rightUp',
maxWidth: TOUR_POPOVER_WIDTH,
},
];

const tourConfig: EuiTourState = {
currentTourStep: 1,
isTourActive: true,
tourPopoverWidth: TOUR_POPOVER_WIDTH,
tourSubtitle: i18n.FEATURE_TOUR_TITLE,
};

const RulesFeatureTourContext = createContext<RulesFeatureTourContextType | null>(null);

/**
* Context for new rules features, displayed in demo tour(euiTour)
* It has a common state in useEuiTour, which allows transition from one step to the next, for components within it[context]
* It also stores tour's state in localStorage
*/
export const RulesFeatureTourContextProvider: FC = ({ children }) => {
const { storage } = useKibana().services;
const initialStore = useMemo<EuiTourState>(
() => ({
...tourConfig,
...(storage.get(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY) ?? tourConfig),
}),
[storage]
);

const [stepProps, actions, reducerState] = useEuiTour(featuresTourSteps, initialStore);

const finishTour = actions.finishTour;
const goToNextStep = actions.incrementStep;

const inMemoryTableStepProps = useMemo(
() => ({
...stepProps[0],
content: (
<>
<p>{i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP}</p>
<EuiSpacer />
<EuiButton color="primary" onClick={goToNextStep}>
{i18n.FEATURE_TOUR_IN_MEMORY_TABLE_STEP_NEXT}
</EuiButton>
</>
),
}),
[stepProps, goToNextStep]
);

useEffect(() => {
const { isTourActive, currentTourStep } = reducerState;
storage.set(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY, { isTourActive, currentTourStep });
}, [reducerState, storage]);

const providerValue = useMemo(
() => ({
steps: {
inMemoryTableStepProps,
bulkActionsStepProps: stepProps[1],
},
finishTour,
goToNextStep,
}),
[finishTour, goToNextStep, inMemoryTableStepProps, stepProps]
);

return (
<RulesFeatureTourContext.Provider value={providerValue}>
{children}
</RulesFeatureTourContext.Provider>
);
};

export const useRulesFeatureTourContext = (): RulesFeatureTourContextType => {
const rulesFeatureTourContext = useContext(RulesFeatureTourContext);
invariant(
rulesFeatureTourContext,
'useRulesFeatureTourContext should be used inside RulesFeatureTourContextProvider'
);

return rulesFeatureTourContext;
};

export const useRulesFeatureTourContextOptional = (): RulesFeatureTourContextType | null => {
const rulesFeatureTourContext = useContext(RulesFeatureTourContext);

return rulesFeatureTourContext;
};
Loading