From b1f983712d858bd37c66dd56e9a1c7e529f72784 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 15 Nov 2022 17:39:20 -0700 Subject: [PATCH 1/7] update and re-enable guided onboarding cypress tests --- .../security_solution/common/constants.ts | 10 -- .../cypress/e2e/guided_onboarding/tour.cy.ts | 128 ++++++++++++------ .../cypress/screens/guided_onboarding.ts | 15 +- .../cypress/tasks/api_calls/tour.ts | 42 ++++++ .../cypress/tasks/guided_onboarding.ts | 72 ++++++++-- .../security_solution/cypress/tasks/login.ts | 31 +---- .../test/security_solution_cypress/config.ts | 1 + 7 files changed, 202 insertions(+), 97 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index aec2bc40a4824e..3bea72d636fded 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -442,16 +442,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]; -/** - * 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 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', -}; - export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY = 'securitySolution.ruleDetails.ruleExecutionLog.showMetrics.v8.2'; diff --git a/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts index 0339445bc8240b..e1dc50d8d28c84 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts @@ -5,58 +5,100 @@ * 2.0. */ -import { login, visit } from '../../tasks/login'; -import { completeTour, goToNextStep, skipTour } from '../../tasks/guided_onboarding'; -import { OVERVIEW_URL } from '../../urls/navigation'; +import { navigateFromHeaderTo } from '../../tasks/security_header'; +import { ALERTS, TIMELINES } from '../../screens/security_header'; +import { closeAlertFlyout, expandFirstAlert } from '../../tasks/alerts'; import { - WELCOME_STEP, - MANAGE_STEP, - ALERTS_STEP, - CASES_STEP, - DATA_STEP, -} from '../../screens/guided_onboarding'; - -before(() => { - login(); -}); + assertTourStepExist, + assertTourStepNotExist, + closeCreateCaseFlyout, + completeTourWithActions, + completeTourWithNextButton, + addToCase, + finishTour, + goToStep, + startTour, +} from '../../tasks/guided_onboarding'; +import { cleanKibana } from '../../tasks/common'; +import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { getNewRule } from '../../objects/rule'; +import { ALERTS_URL, DASHBOARDS_URL } from '../../urls/navigation'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { login, visit } from '../../tasks/login'; +import { quitGlobalTour, startAlertsCasesTour } from '../../tasks/api_calls/tour'; +import { AlertsCasesTourSteps } from '../../../public/common/components/guided_onboarding_tour/tour_config'; -// need to redo these tests for new implementation -describe.skip('Guided onboarding tour', () => { - describe('Tour is enabled', () => { - beforeEach(() => { - visit(OVERVIEW_URL); - }); +describe('Guided onboarding tour', () => { + before(() => { + cleanKibana(); + login(); + createCustomRuleEnabled({ ...getNewRule(), customQuery: 'user.name:*' }); + }); + beforeEach(() => { + startAlertsCasesTour(); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + }); + after(() => { + quitGlobalTour(); + }); + it('Completes the tour with next button clicks', () => { + startTour(); + completeTourWithNextButton(); + finishTour(); + cy.url().should('include', DASHBOARDS_URL); + }); - it('can be completed', () => { - // Step 1: Overview - cy.get(WELCOME_STEP).should('be.visible'); - goToNextStep(WELCOME_STEP); + it('Completes the tour with action clicks', () => { + startTour(); + completeTourWithActions(); + finishTour(); + cy.url().should('include', DASHBOARDS_URL); + }); - // Step 2: Manage - cy.get(MANAGE_STEP).should('be.visible'); - goToNextStep(MANAGE_STEP); + // unhappy paths + it('Resets the tour to step 1 when we navigate away', () => { + startTour(); + goToStep(AlertsCasesTourSteps.expandEvent); + assertTourStepExist(AlertsCasesTourSteps.expandEvent); + assertTourStepNotExist(AlertsCasesTourSteps.pointToAlertName); + navigateFromHeaderTo(TIMELINES); + navigateFromHeaderTo(ALERTS); + assertTourStepNotExist(AlertsCasesTourSteps.expandEvent); + assertTourStepExist(AlertsCasesTourSteps.pointToAlertName); + }); - // Step 3: Alerts - cy.get(ALERTS_STEP).should('be.visible'); - goToNextStep(ALERTS_STEP); + describe('persists tour steps in flyout on flyout toggle', () => { + const stepsInAlertsFlyout = [ + AlertsCasesTourSteps.reviewAlertDetailsFlyout, + AlertsCasesTourSteps.addAlertToCase, + AlertsCasesTourSteps.viewCase, + ]; - // Step 4: Cases - cy.get(CASES_STEP).should('be.visible'); - goToNextStep(CASES_STEP); + const stepsInCasesFlyout = [AlertsCasesTourSteps.createCase, AlertsCasesTourSteps.submitCase]; - // Step 5: Add data - cy.get(DATA_STEP).should('be.visible'); - completeTour(); + stepsInAlertsFlyout.forEach((step) => { + it(`step: ${step}, resets to ${step}`, () => { + startTour(); + goToStep(step); + assertTourStepExist(step); + closeAlertFlyout(); + assertTourStepNotExist(step); + expandFirstAlert(); + assertTourStepExist(step); + }); }); - it('can be skipped', () => { - cy.get(WELCOME_STEP).should('be.visible'); - - skipTour(); - // step 1 is not displayed - cy.get(WELCOME_STEP).should('not.exist'); - // step 2 is not displayed - cy.get(MANAGE_STEP).should('not.exist'); + stepsInCasesFlyout.forEach((step) => { + it(`step: ${step}, resets to ${AlertsCasesTourSteps.createCase}`, () => { + startTour(); + goToStep(step); + assertTourStepExist(step); + closeCreateCaseFlyout(); + assertTourStepNotExist(step); + addToCase(); + assertTourStepExist(AlertsCasesTourSteps.createCase); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts b/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts index 6b3f4bc20ac037..f11c18817e1ba6 100644 --- a/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts +++ b/x-pack/plugins/security_solution/cypress/screens/guided_onboarding.ts @@ -5,12 +5,13 @@ * 2.0. */ -export const WELCOME_STEP = '[data-test-subj="welcomeStep"]'; -export const MANAGE_STEP = '[data-test-subj="manageStep"]'; -export const ALERTS_STEP = '[data-test-subj="alertsStep"]'; -export const CASES_STEP = '[data-test-subj="casesStep"]'; -export const DATA_STEP = '[data-test-subj="dataStep"]'; +export const ALERTS_STEP_GUIDE_BUTTON = '[data-test-subj="onboarding--stepButton--siem--step3"]'; +export const COMPLETE_SIEM_GUIDE_BUTTON = + '[data-test-subj="onboarding--completeGuideButton--siem"]'; export const NEXT_STEP_BUTTON = '[data-test-subj="onboarding--securityTourNextStepButton"]'; -export const END_TOUR_BUTTON = '[data-test-subj="onboarding--securityTourEndButton"]'; -export const SKIP_TOUR_BUTTON = '[data-test-subj="onboarding--securityTourSkipButton"]'; +export const COMPLETION_POPOVER = '[data-test-subj="manualCompletionPopover"]'; + +export const GLOBAL_TOUR_BUTTON = `[data-test-subj="guideButton"]`; + +export const CLOSE_CREATE_CASE_FLYOUT = `[data-test-subj="create-case-flyout"] [data-test-subj="euiFlyoutCloseButton"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts new file mode 100644 index 00000000000000..5eac1af18745ff --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/tour.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +const alertsGuideActiveState = { + isActive: true, + status: 'in_progress', + steps: [ + { id: 'add_data', status: 'complete' }, + { id: 'rules', status: 'complete' }, + { id: 'alertsCases', status: 'active' }, + ], + guideId: 'security', +}; + +export const startAlertsCasesTour = () => + cy.request({ + method: 'PUT', + url: 'api/guided_onboarding/state', + headers: { 'kbn-xsrf': 'cypress-creds' }, + body: { + status: 'in_progress', + guide: alertsGuideActiveState, + }, + }); + +export const quitGlobalTour = () => + cy.request({ + method: 'PUT', + url: 'api/guided_onboarding/state', + headers: { 'kbn-xsrf': 'cypress-creds' }, + body: { + status: 'quit', + guide: { + ...alertsGuideActiveState, + isActive: false, + }, + }, + }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts b/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts index 2e5c54a396b240..fe3170b31e9510 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/guided_onboarding.ts @@ -5,21 +5,75 @@ * 2.0. */ +import { ATTACH_TO_NEW_CASE_BUTTON, TAKE_ACTION_BTN } from '../screens/alerts'; +import { createCase } from './create_new_case'; import { NEXT_STEP_BUTTON, - END_TOUR_BUTTON, - DATA_STEP, - SKIP_TOUR_BUTTON, + GLOBAL_TOUR_BUTTON, + ALERTS_STEP_GUIDE_BUTTON, + COMPLETION_POPOVER, + COMPLETE_SIEM_GUIDE_BUTTON, + CLOSE_CREATE_CASE_FLYOUT, } from '../screens/guided_onboarding'; +import { expandFirstAlert } from './alerts'; -export const goToNextStep = (currentStep: string) => { - cy.get(`${currentStep} ${NEXT_STEP_BUTTON}`).click(); +export const goToNextStep = (currentStep: number) => { + cy.get( + `[data-test-subj="tourStepAnchor-alertsCases-${currentStep}"] ${NEXT_STEP_BUTTON}` + ).click(); }; -export const completeTour = () => { - cy.get(`${DATA_STEP} ${END_TOUR_BUTTON}`).click(); +export const startTour = () => { + cy.get(GLOBAL_TOUR_BUTTON).click(); + cy.get(ALERTS_STEP_GUIDE_BUTTON).click(); }; -export const skipTour = () => { - cy.get(SKIP_TOUR_BUTTON).click(); +export const finishTour = () => { + cy.get(COMPLETION_POPOVER).should('exist'); + cy.get(GLOBAL_TOUR_BUTTON).click(); + cy.get(ALERTS_STEP_GUIDE_BUTTON).click(); + cy.get(COMPLETE_SIEM_GUIDE_BUTTON).click(); }; + +export const completeTourWithNextButton = () => { + for (let i = 1; i < 6; i++) { + goToNextStep(i); + } + createCase(); + goToNextStep(7); +}; + +export const addToCase = () => { + cy.get(TAKE_ACTION_BTN).click(); + cy.get(ATTACH_TO_NEW_CASE_BUTTON).click(); +}; + +export const completeTourWithActions = () => { + goToNextStep(1); + expandFirstAlert(); + goToNextStep(3); + addToCase(); + goToNextStep(5); + createCase(); + goToNextStep(7); +}; + +export const goToStep = (step: number) => { + for (let i = 1; i < 6; i++) { + if (i === step) { + break; + } + goToNextStep(i); + } + if (step === 7) { + createCase(); + } +}; + +export const assertTourStepExist = (step: number) => + cy.get(`[data-test-subj="tourStepAnchor-alertsCases-${step}"]`).should('exist'); + +export const assertTourStepNotExist = (step: number) => + cy.get(`[data-test-subj="tourStepAnchor-alertsCases-${step}"]`).should('not.exist'); + +export const closeCreateCaseFlyout = () => cy.get(CLOSE_CREATE_CASE_FLYOUT).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index d7568d1c2e8802..ef60c583262786 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -10,7 +10,6 @@ import type { UrlObject } from 'url'; import Url from 'url'; import type { ROLES } from '../../common/test'; -import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../common/constants'; import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; import { hostDetailsUrl, LOGOUT_URL, userDetailsUrl } from '../urls/navigation'; @@ -286,23 +285,6 @@ export const getEnvAuth = (): User => { } }; -/** - * 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 disableNewFeaturesTours = (window: Window) => { - const tourStorageKeys = Object.values(NEW_FEATURES_TOUR_STORAGE_KEYS); - const tourConfig = { - isTourActive: false, - }; - - tourStorageKeys.forEach((key) => { - window.localStorage.setItem(key, JSON.stringify(tourConfig)); - }); -}; - /** * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing @@ -328,29 +310,22 @@ export const visit = ( if (onBeforeLoadCallback) { onBeforeLoadCallback(win); } - disableNewFeaturesTours(win); }, } ); }; export const visitWithoutDateRange = (url: string, role?: ROLES) => { - cy.visit(role ? getUrlWithRoute(role, url) : url, { - onBeforeLoad: disableNewFeaturesTours, - }); + cy.visit(role ? getUrlWithRoute(role, url) : url); }; export const visitWithUser = (url: string, user: User) => { - cy.visit(constructUrlWithUser(user, url), { - onBeforeLoad: disableNewFeaturesTours, - }); + cy.visit(constructUrlWithUser(user, url)); }; 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: disableNewFeaturesTours, - }); + cy.visit(role ? getUrlWithRoute(role, route) : route); cy.get('[data-test-subj="headerGlobalNav"]'); cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 18d7577516fd9e..8bd090ad2cf25c 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -52,6 +52,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'alertDetailsPageEnabled', ])}`, + '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, ], }, From 3c87ff67ea756f97cb3f3aa04b74d20c233f869d Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 16 Nov 2022 07:56:06 -0700 Subject: [PATCH 2/7] delete unused tour dir --- .../rules_table/feature_tour/README.md | 44 ----- .../feature_tour/rules_feature_tour.tsx | 151 ------------------ .../rules_table/feature_tour/translations.ts | 44 ----- 3 files changed, 239 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md deleted file mode 100644 index 95628008407cde..00000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Feature Tour on the Rule Management Page - -This folder contains an implementation of a feature tour UI for new features introduced in `8.1.0`. -This implementaion is currently unused - all usages have been removed from React components. -We might revisit this implementation in the next releases when we have something new for the user -to demonstrate on the Rule Management Page. - -## A new way of building tours - -The EUI Tour has evolved and continues to do so. - -New features and fixes to track: - -- Support for previous, next and go to step [#4831][1] -- Built-in 'Next' button [#5715][2] - -## How to revive this tour for the next release (if needed) - -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. - -4. Render `RulesFeatureTour` component somewhere on the Rule Management page. - Only one instance of that component should be present on the page. - -5. Consider abstracting away persistence in Local Storage and other functionality that - may be common to tours on different pages. - -## Useful links - -Docs: [`EuiTour`](https://elastic.github.io/eui/#/display/tour). - -For reference, PRs where this Tour has been introduced or changed: - -- added in `8.1.0` ([PR](https://github.com/elastic/kibana/pull/124343)) -- removed in `8.2.0` ([PR](https://github.com/elastic/kibana/pull/128398)) - - - -[1]: https://github.com/elastic/eui/issues/4831 -[2]: https://github.com/elastic/eui/issues/5715 diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx deleted file mode 100644 index d2f64ec882840f..00000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx +++ /dev/null @@ -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 type { - EuiStatelessTourStep, - EuiTourActions, - EuiTourState, - EuiTourStepProps, -} from '@elastic/eui'; -import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiText, - EuiTourStep, - useEuiTour, -} from '@elastic/eui'; -import { noop } from 'lodash'; -import type { FC } from 'react'; -import React, { useEffect, useMemo, useState } from 'react'; -import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constants'; -import { useKibana } from '../../../../../common/lib/kibana'; -import * as i18n from './translations'; - -export interface RulesFeatureTourContextType { - steps: EuiTourStepProps[]; - actions: EuiTourActions; -} - -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 = { - currentTourStep: 1, - isTourActive: true, - tourPopoverWidth: TOUR_POPOVER_WIDTH, - tourSubtitle: i18n.TOUR_TITLE, -}; - -const stepsConfig: EuiStatelessTourStep[] = [ - { - step: 1, - title: i18n.SEARCH_CAPABILITIES_TITLE, - content: {i18n.SEARCH_CAPABILITIES_DESCRIPTION}, - stepsTotal: 1, - children: <>, - onFinish: noop, - maxWidth: TOUR_POPOVER_WIDTH, - }, -]; - -export const RulesFeatureTour: FC = () => { - const { storage } = useKibana().services; - - const restoredState = useMemo( - () => ({ - ...tourConfig, - ...storage.get(TOUR_STORAGE_KEY), - }), - [storage] - ); - - const [tourSteps, tourActions, tourState] = useEuiTour(stepsConfig, restoredState); - - useEffect(() => { - const { isTourActive, currentTourStep } = tourState; - storage.set(TOUR_STORAGE_KEY, { isTourActive, currentTourStep }); - }, [tourState, storage]); - - const [shouldShowSearchCapabilitiesTour, setShouldShowSearchCapabilitiesTour] = 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(`#${SEARCH_CAPABILITIES_TOUR_ANCHOR}`)) { - setShouldShowSearchCapabilitiesTour(true); - observer.disconnect(); - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - return () => observer.disconnect(); - }, []); - - const enhancedSteps = useMemo( - () => - tourSteps.map((item, index) => ({ - ...item, - content: ( - <> - {item.content} - {tourSteps.length > 1 && ( - <> - - - - - - - - - - - )} - - ), - })), - [tourSteps, tourActions] - ); - - return shouldShowSearchCapabilitiesTour ? ( - - ) : null; -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts deleted file mode 100644 index 45715c6ca76d84..00000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts +++ /dev/null @@ -1,44 +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 TOUR_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle', - { - defaultMessage: "What's new", - } -); - -export const PREVIOUS_STEP_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel', - { - defaultMessage: 'Go to previous step', - } -); - -export const NEXT_STEP_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel', - { - defaultMessage: 'Go to next step', - } -); - -export const SEARCH_CAPABILITIES_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle', - { - defaultMessage: 'Enhanced search capabilities', - } -); - -export const SEARCH_CAPABILITIES_DESCRIPTION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription', - { - defaultMessage: - 'It is now possible to search rules by index patterns, like "filebeat-*", or by MITRE ATT&CK™ tactics or techniques, like "Defense Evasion" or "TA0005".', - } -); From af42eb1f92c8936cb7f3581e9dccf418311c5837 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 16 Nov 2022 11:44:37 -0700 Subject: [PATCH 3/7] temp fix for api throwing and getting repeatedly called --- .../server/helpers/plugin_state_utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts index f24fdf814f83b4..0b3a619ed2ad54 100644 --- a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts +++ b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts @@ -40,7 +40,13 @@ export const getPluginState = async (savedObjectsClient: SavedObjectsClient) => return pluginState; } else { // create a SO to keep track of the correct creation date - await updatePluginStatus(savedObjectsClient, 'not_started'); + try { + await updatePluginStatus(savedObjectsClient, 'not_started'); + // @yulia, we need to add a user permissions + // check here instead of swallowing this error + // eslint-disable-next-line no-empty + } catch (e) {} + return { status: 'not_started', isActivePeriod: true, From 74f2296051b02e47c1370295de08c4b771e81378 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 16 Nov 2022 13:12:23 -0700 Subject: [PATCH 4/7] i18n fixes --- x-pack/plugins/translations/translations/fr-FR.json | 5 ----- x-pack/plugins/translations/translations/ja-JP.json | 5 ----- x-pack/plugins/translations/translations/zh-CN.json | 5 ----- 3 files changed, 15 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2a006ccb2cc8b5..f0b42cc4fbcc33 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27375,11 +27375,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "Balises", "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "Version", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel": "Aller à l'étape suivante", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel": "Revenir à l'étape précédente", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription": "Il est maintenant possible de rechercher des règles par modèle d'indexation, tel que \"filebeat-*\", ou par tactique ou technique MITRE ATT&CK™, telle que \"Évasion par la défense \" ou \"TA0005\".", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle": "Capacités de recherche améliorées", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle": "Nouveautés", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "Règles personnalisées", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Règles Elastic", "xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle": "Nous n'avons trouvé aucune règle avec les filtres ci-dessus.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a105bd0aa0d8f1..646dec57a43ff9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27350,11 +27350,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "タグ", "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "バージョン", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel": "次のステップに進む", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel": "前のステップに戻る", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription": "「filebeat-*」などのインデックスパターンや、「Defense Evasion」や「TA0005」などのMITRE ATT&CK™方式または手法でルールを検索できます。", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle": "拡張検索機能", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle": "新機能", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "カスタムルール", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Elasticルール", "xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle": "上記のフィルターでルールが見つかりませんでした。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index de988524c860b3..326968941033c0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27384,11 +27384,6 @@ "xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle": "标签", "xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle": "版本", "xpack.securitySolution.detectionEngine.rules.allRules.exportFilenameTitle": "rules_export", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel": "前往下一步", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel": "前往上一步", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription": "现在可以按搜索模式(如“filebeat-*”) 或者 MITRE ATT&CK™ 策略或技术(如“Defense Evasion”或“TA0005”)搜索规则。", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle": "已增强搜索功能", - "xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle": "最新动态", "xpack.securitySolution.detectionEngine.rules.allRules.filters.customRulesTitle": "定制规则", "xpack.securitySolution.detectionEngine.rules.allRules.filters.elasticRulesTitle": "Elastic 规则", "xpack.securitySolution.detectionEngine.rules.allRules.filters.noRulesBodyTitle": "使用上述筛选,我们无法找到任何规则。", From 27238c5b1c82fd75ec0bdc372dde0a3cd86937f4 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 16 Nov 2022 16:53:35 -0700 Subject: [PATCH 5/7] add issue link --- .../guided_onboarding/server/helpers/plugin_state_utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts index 0b3a619ed2ad54..06fc211f1864f4 100644 --- a/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts +++ b/src/plugins/guided_onboarding/server/helpers/plugin_state_utils.ts @@ -44,6 +44,7 @@ export const getPluginState = async (savedObjectsClient: SavedObjectsClient) => await updatePluginStatus(savedObjectsClient, 'not_started'); // @yulia, we need to add a user permissions // check here instead of swallowing this error + // see issue: https://github.com/elastic/kibana/issues/145434 // eslint-disable-next-line no-empty } catch (e) {} From 0b202a2269d11f13bd1d93ff32e0b60e844c47e5 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 17 Nov 2022 10:22:21 -0700 Subject: [PATCH 6/7] revert delete rules tour --- .../security_solution/common/constants.ts | 10 ++ .../security_solution/cypress/tasks/login.ts | 31 +++- .../rules_table/feature_tour/README.md | 44 +++++ .../feature_tour/rules_feature_tour.tsx | 151 ++++++++++++++++++ .../rules_table/feature_tour/translations.ts | 44 +++++ 5 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 3bea72d636fded..aec2bc40a4824e 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -442,6 +442,16 @@ 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]; +/** + * 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 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', +}; + export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY = 'securitySolution.ruleDetails.ruleExecutionLog.showMetrics.v8.2'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index ef60c583262786..d7568d1c2e8802 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -10,6 +10,7 @@ import type { UrlObject } from 'url'; import Url from 'url'; import type { ROLES } from '../../common/test'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../common/constants'; import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; import { hostDetailsUrl, LOGOUT_URL, userDetailsUrl } from '../urls/navigation'; @@ -285,6 +286,23 @@ export const getEnvAuth = (): User => { } }; +/** + * 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 disableNewFeaturesTours = (window: Window) => { + const tourStorageKeys = Object.values(NEW_FEATURES_TOUR_STORAGE_KEYS); + const tourConfig = { + isTourActive: false, + }; + + tourStorageKeys.forEach((key) => { + window.localStorage.setItem(key, JSON.stringify(tourConfig)); + }); +}; + /** * Authenticates with Kibana, visits the specified `url`, and waits for the * Kibana global nav to be displayed before continuing @@ -310,22 +328,29 @@ export const visit = ( if (onBeforeLoadCallback) { onBeforeLoadCallback(win); } + disableNewFeaturesTours(win); }, } ); }; export const visitWithoutDateRange = (url: string, role?: ROLES) => { - cy.visit(role ? getUrlWithRoute(role, url) : url); + cy.visit(role ? getUrlWithRoute(role, url) : url, { + onBeforeLoad: disableNewFeaturesTours, + }); }; export const visitWithUser = (url: string, user: User) => { - cy.visit(constructUrlWithUser(user, url)); + cy.visit(constructUrlWithUser(user, url), { + 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); + cy.visit(role ? getUrlWithRoute(role, route) : route, { + onBeforeLoad: disableNewFeaturesTours, + }); cy.get('[data-test-subj="headerGlobalNav"]'); cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md new file mode 100644 index 00000000000000..95628008407cde --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/README.md @@ -0,0 +1,44 @@ +# Feature Tour on the Rule Management Page + +This folder contains an implementation of a feature tour UI for new features introduced in `8.1.0`. +This implementaion is currently unused - all usages have been removed from React components. +We might revisit this implementation in the next releases when we have something new for the user +to demonstrate on the Rule Management Page. + +## A new way of building tours + +The EUI Tour has evolved and continues to do so. + +New features and fixes to track: + +- Support for previous, next and go to step [#4831][1] +- Built-in 'Next' button [#5715][2] + +## How to revive this tour for the next release (if needed) + +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. + +4. Render `RulesFeatureTour` component somewhere on the Rule Management page. + Only one instance of that component should be present on the page. + +5. Consider abstracting away persistence in Local Storage and other functionality that + may be common to tours on different pages. + +## Useful links + +Docs: [`EuiTour`](https://elastic.github.io/eui/#/display/tour). + +For reference, PRs where this Tour has been introduced or changed: + +- added in `8.1.0` ([PR](https://github.com/elastic/kibana/pull/124343)) +- removed in `8.2.0` ([PR](https://github.com/elastic/kibana/pull/128398)) + + + +[1]: https://github.com/elastic/eui/issues/4831 +[2]: https://github.com/elastic/eui/issues/5715 diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx new file mode 100644 index 00000000000000..d2f64ec882840f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/rules_feature_tour.tsx @@ -0,0 +1,151 @@ +/* + * 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 { + EuiStatelessTourStep, + EuiTourActions, + EuiTourState, + EuiTourStepProps, +} from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTourStep, + useEuiTour, +} from '@elastic/eui'; +import { noop } from 'lodash'; +import type { FC } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; +import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../../common/constants'; +import { useKibana } from '../../../../../common/lib/kibana'; +import * as i18n from './translations'; + +export interface RulesFeatureTourContextType { + steps: EuiTourStepProps[]; + actions: EuiTourActions; +} + +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 = { + currentTourStep: 1, + isTourActive: true, + tourPopoverWidth: TOUR_POPOVER_WIDTH, + tourSubtitle: i18n.TOUR_TITLE, +}; + +const stepsConfig: EuiStatelessTourStep[] = [ + { + step: 1, + title: i18n.SEARCH_CAPABILITIES_TITLE, + content: {i18n.SEARCH_CAPABILITIES_DESCRIPTION}, + stepsTotal: 1, + children: <>, + onFinish: noop, + maxWidth: TOUR_POPOVER_WIDTH, + }, +]; + +export const RulesFeatureTour: FC = () => { + const { storage } = useKibana().services; + + const restoredState = useMemo( + () => ({ + ...tourConfig, + ...storage.get(TOUR_STORAGE_KEY), + }), + [storage] + ); + + const [tourSteps, tourActions, tourState] = useEuiTour(stepsConfig, restoredState); + + useEffect(() => { + const { isTourActive, currentTourStep } = tourState; + storage.set(TOUR_STORAGE_KEY, { isTourActive, currentTourStep }); + }, [tourState, storage]); + + const [shouldShowSearchCapabilitiesTour, setShouldShowSearchCapabilitiesTour] = 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(`#${SEARCH_CAPABILITIES_TOUR_ANCHOR}`)) { + setShouldShowSearchCapabilitiesTour(true); + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => observer.disconnect(); + }, []); + + const enhancedSteps = useMemo( + () => + tourSteps.map((item, index) => ({ + ...item, + content: ( + <> + {item.content} + {tourSteps.length > 1 && ( + <> + + + + + + + + + + + )} + + ), + })), + [tourSteps, tourActions] + ); + + return shouldShowSearchCapabilitiesTour ? ( + + ) : null; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts new file mode 100644 index 00000000000000..45715c6ca76d84 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/feature_tour/translations.ts @@ -0,0 +1,44 @@ +/* + * 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 TOUR_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.tourTitle', + { + defaultMessage: "What's new", + } +); + +export const PREVIOUS_STEP_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.previousStepLabel', + { + defaultMessage: 'Go to previous step', + } +); + +export const NEXT_STEP_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.nextStepLabel', + { + defaultMessage: 'Go to next step', + } +); + +export const SEARCH_CAPABILITIES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesTitle', + { + defaultMessage: 'Enhanced search capabilities', + } +); + +export const SEARCH_CAPABILITIES_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.featureTour.searchCapabilitiesDescription', + { + defaultMessage: + 'It is now possible to search rules by index patterns, like "filebeat-*", or by MITRE ATT&CK™ tactics or techniques, like "Defense Evasion" or "TA0005".', + } +); From a67e4fc01a059ed56a314511cc2097e76d9ae463 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 17 Nov 2022 10:23:33 -0700 Subject: [PATCH 7/7] add code comment --- x-pack/test/security_solution_cypress/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 8bd090ad2cf25c..843d6457b55f11 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -52,6 +52,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.securitySolution.enableExperimental=${JSON.stringify([ 'alertDetailsPageEnabled', ])}`, + // mock cloud to enable the guided onboarding tour in e2e tests '--xpack.cloud.id=test', `--home.disableWelcomeScreen=true`, ],