[Security Solution] [Platform] Data sources guided tour (#138327)

## Summary

Adds a guided tour for data sources
This commit is contained in:
Devin W. Hurley 2022-08-15 17:35:09 -04:00 committed by GitHub
parent 2c96643043
commit 81d7a6f9ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 149 additions and 23 deletions

View file

@ -427,11 +427,17 @@ export const RULES_TABLE_MAX_PAGE_SIZE = 100;
export const RULES_TABLE_PAGE_SIZE_OPTIONS = [5, 10, 20, 50, RULES_TABLE_MAX_PAGE_SIZE];
/**
* A local storage key we use to store the state of the feature tour UI for the Rule Management page.
* Local storage keys we use to store the state of our new features tours we currently show in the app.
*
* NOTE: As soon as we want to show a new tour for features in the current Kibana version,
* we will need to update this constant with the corresponding version.
* NOTE: As soon as we want to show tours for new features in the upcoming release,
* we will need to update these constants with the corresponding version.
*/
export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.4',
RULE_CREATION_PAGE_DEFINE_STEP:
'securitySolution.ruleCreationPage.defineStep.newFeaturesTour.v8.4',
};
export const RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY =
'securitySolution.rulesManagementPage.newFeaturesTour.v8.4';

View file

@ -10,7 +10,7 @@ import type { UrlObject } from 'url';
import Url from 'url';
import type { ROLES } from '../../common/test';
import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../common/constants';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../common/constants';
import { TIMELINE_FLYOUT_BODY } from '../screens/timeline';
import { hostDetailsUrl, LOGOUT_URL, userDetailsUrl } from '../urls/navigation';
@ -287,18 +287,20 @@ 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
* For all the new features tours we show in the app, this method disables them
* by setting their configs in the local storage. It prevents the tours from appearing
* on the page during test runs and covering other UI elements.
* @param window - browser's window object
*/
const disableFeatureTourForRuleManagementPage = (window: Window) => {
const disableNewFeaturesTours = (window: Window) => {
const tourStorageKeys = Object.values(NEW_FEATURES_TOUR_STORAGE_KEYS);
const tourConfig = {
isTourActive: false,
};
window.localStorage.setItem(
RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY,
JSON.stringify(tourConfig)
);
tourStorageKeys.forEach((key) => {
window.localStorage.setItem(key, JSON.stringify(tourConfig));
});
};
/**
@ -326,7 +328,7 @@ export const visit = (
if (onBeforeLoadCallback) {
onBeforeLoadCallback(win);
}
disableFeatureTourForRuleManagementPage(win);
disableNewFeaturesTours(win);
},
}
);
@ -334,20 +336,20 @@ export const visit = (
export const visitWithoutDateRange = (url: string, role?: ROLES) => {
cy.visit(role ? getUrlWithRoute(role, url) : url, {
onBeforeLoad: disableFeatureTourForRuleManagementPage,
onBeforeLoad: disableNewFeaturesTours,
});
};
export const visitWithUser = (url: string, user: User) => {
cy.visit(constructUrlWithUser(user, url), {
onBeforeLoad: disableFeatureTourForRuleManagementPage,
onBeforeLoad: disableNewFeaturesTours,
});
};
export const visitTimeline = (timelineId: string, role?: ROLES) => {
const route = `/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`;
cy.visit(role ? getUrlWithRoute(role, route) : route, {
onBeforeLoad: disableFeatureTourForRuleManagementPage,
onBeforeLoad: disableNewFeaturesTours,
});
cy.get('[data-test-subj="headerGlobalNav"]');
cy.get(TIMELINE_FLYOUT_BODY).should('be.visible');

View file

@ -80,6 +80,7 @@ import { getIsRulePreviewDisabled } from '../rule_preview/helpers';
import { NewTermsFields } from '../new_terms_fields';
import { ScheduleItem } from '../schedule_item_form';
import { DocLink } from '../../../../common/components/links_to_docs/doc_link';
import { StepDefineRuleNewFeaturesTour } from './new_features_tour';
const CommonUseField = getUseField({ component: Field });
@ -511,10 +512,15 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
/>
);
}, [kibanaDataViews]);
const DataSource = useMemo(() => {
return (
<RuleTypeEuiFormRow label={i18n.SOURCE} $isVisible={true} fullWidth>
<EuiFlexGroup direction="column" gutterSize="s">
<RuleTypeEuiFormRow id="dataSourceSelector" label={i18n.SOURCE} $isVisible={true} fullWidth>
<EuiFlexGroup
direction="column"
gutterSize="s"
data-test-subj="dataViewIndexPatternButtonGroupFlexGroup"
>
<EuiFlexItem>
<EuiText size="xs">
<FormattedMessage
@ -582,11 +588,11 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
);
}, [
dataSourceType,
onChangeDataSource,
dataViewIndexPatternToggleButtonOptions,
DataViewSelectorMemo,
indexModified,
handleResetIndices,
onChangeDataSource,
]);
const QueryBarMemo = useMemo(
@ -679,6 +685,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
) : (
<>
<StepContentWrapper addPadding={!isUpdateView}>
<StepDefineRuleNewFeaturesTour />
<Form form={form} data-test-subj="stepDefineRule">
<StyledVisibleContainer isVisible={false}>
<UseField

View file

@ -0,0 +1,89 @@
/*
* 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 { noop } from 'lodash';
import type { FC } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import type { EuiTourState } from '@elastic/eui';
import { EuiSpacer, EuiTourStep, useEuiTour } from '@elastic/eui';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
import * as i18n from './translations';
const TOUR_STORAGE_KEY = NEW_FEATURES_TOUR_STORAGE_KEYS.RULE_CREATION_PAGE_DEFINE_STEP;
const TOUR_POPOVER_WIDTH = 300;
const tourConfig: EuiTourState = {
currentTourStep: 1,
isTourActive: true,
tourPopoverWidth: TOUR_POPOVER_WIDTH,
tourSubtitle: '',
};
const stepsConfig = [
{
step: 1,
title: i18n.DATA_SOURCE_GUIDE_TITLE,
content: (
<span>
<p>{i18n.DATA_SOURCE_GUIDE_CONTENT}</p>
<EuiSpacer />
</span>
),
anchor: `#dataSourceSelector`,
anchorPosition: 'rightCenter' as const,
stepsTotal: 1,
onFinish: noop,
},
];
export const StepDefineRuleNewFeaturesTour: FC = () => {
const { storage } = useKibana().services;
const restoredState = useMemo<EuiTourState>(
() => ({
...tourConfig,
...storage.get(TOUR_STORAGE_KEY),
}),
[storage]
);
const [tourSteps, , tourState] = useEuiTour(stepsConfig, restoredState);
useEffect(() => {
const { isTourActive, currentTourStep } = tourState;
storage.set(TOUR_STORAGE_KEY, { isTourActive, currentTourStep });
}, [tourState, storage]);
const [shouldShowTour, setShouldShowTour] = useState(false);
useEffect(() => {
/**
* Wait until the tour target elements are visible on the page and mount
* EuiTourStep components only after that. Otherwise, the tours would never
* show up on the page.
*/
const observer = new MutationObserver(() => {
if (document.querySelector(stepsConfig[0].anchor)) {
setShouldShowTour(true);
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
return () => observer.disconnect();
}, []);
return shouldShowTour ? <EuiTourStep {...tourSteps[0]} /> : null;
};

View file

@ -99,6 +99,27 @@ export const SOURCE = i18n.translate(
}
);
export const DATA_SOURCE_GUIDE_SUB_TITLE = i18n.translate(
'xpack.securitySolution.detections.dataSource.popover.title',
{
defaultMessage: 'Select a data source',
}
);
export const DATA_SOURCE_GUIDE_TITLE = i18n.translate(
'xpack.securitySolution.detections.dataSource.popover.subTitle',
{
defaultMessage: 'Data sources',
}
);
export const DATA_SOURCE_GUIDE_CONTENT = i18n.translate(
'xpack.securitySolution.detections.dataSource.popover.content',
{
defaultMessage: 'Rules can now query index patterns or data views.',
}
);
export const RULE_PREVIEW_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.rulePreviewTitle',
{

View file

@ -16,12 +16,12 @@ New features and fixes to track:
## How to revive this tour for the next release (if needed)
1. Update Kibana version in `RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY`.
1. Update Kibana version in `NEW_FEATURES_TOUR_STORAGE_KEYS.RULE_MANAGEMENT_PAGE`.
Set it to a version you're going to implement a feature tour for.
2. Define the steps for your tour. See `RulesFeatureTour` and `stepsConfig`.
3. Define and set an anchor `id` for every step's target HTML element.
3. Define and set an anchor `id` for every step's target HTML element.
4. Render `RulesFeatureTour` component somewhere on the Rule Management page.
Only one instance of that component should be present on the page.

View file

@ -23,7 +23,7 @@ import {
import { noop } from 'lodash';
import type { FC } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY } from '../../../../../../../common/constants';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../../common/constants';
import { useKibana } from '../../../../../../common/lib/kibana';
import * as i18n from './translations';
@ -34,6 +34,7 @@ export interface RulesFeatureTourContextType {
export const SEARCH_CAPABILITIES_TOUR_ANCHOR = 'search-capabilities-tour-anchor';
const TOUR_STORAGE_KEY = NEW_FEATURES_TOUR_STORAGE_KEYS.RULE_MANAGEMENT_PAGE;
const TOUR_POPOVER_WIDTH = 400;
const tourConfig: EuiTourState = {
@ -61,7 +62,7 @@ export const RulesFeatureTour: FC = () => {
const restoredState = useMemo<EuiTourState>(
() => ({
...tourConfig,
...storage.get(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY),
...storage.get(TOUR_STORAGE_KEY),
}),
[storage]
);
@ -70,7 +71,7 @@ export const RulesFeatureTour: FC = () => {
useEffect(() => {
const { isTourActive, currentTourStep } = tourState;
storage.set(RULES_MANAGEMENT_FEATURE_TOUR_STORAGE_KEY, { isTourActive, currentTourStep });
storage.set(TOUR_STORAGE_KEY, { isTourActive, currentTourStep });
}, [tourState, storage]);
const [shouldShowSearchCapabilitiesTour, setShouldShowSearchCapabilitiesTour] = useState(false);