From 92c8273b48889e11ab7b1bb43f78730906ebaf8d Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Tue, 15 Nov 2022 10:37:58 +0100 Subject: [PATCH] Address guided onboarding feedback for the rules area --- .../rules_management_tour.tsx | 118 ------------ .../guided_onboarding/translations.ts | 36 ---- .../components/rules_table/index.tsx | 2 + .../rules_management_tour.tsx | 180 ++++++++++++++++++ .../guided_onboarding/translations.ts | 65 +++++++ .../use_is_element_mounted.ts | 0 .../rules_table_filters.tsx | 17 +- .../pages/rule_management/index.tsx | 2 - .../load_prepackaged_rules_button.tsx | 2 +- .../components/rules/rule_switch/index.tsx | 2 +- 10 files changed, 264 insertions(+), 160 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/rules_management_tour.tsx delete mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/translations.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/rules_management_tour.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/translations.ts rename x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/{ => rules_table/rules_table}/guided_onboarding/use_is_element_mounted.ts (100%) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/rules_management_tour.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/rules_management_tour.tsx deleted file mode 100644 index 1fcb56a009edb..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/rules_management_tour.tsx +++ /dev/null @@ -1,118 +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 { EuiTourActions, EuiTourStepProps } from '@elastic/eui'; -import { EuiTourStep } from '@elastic/eui'; -import { noop } from 'lodash'; -import React, { useEffect, useMemo } from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { of } from 'rxjs'; -import { useKibana } from '../../../../common/lib/kibana'; -import { useFindRulesQuery } from '../../../rule_management/api/hooks/use_find_rules_query'; -import * as i18n from './translations'; -import { useIsElementMounted } from './use_is_element_mounted'; - -export const INSTALL_PREBUILT_RULES_ANCHOR = 'install-prebuilt-rules-anchor'; -export const SEARCH_FIRST_RULE_ANCHOR = 'search-first-rule-anchor'; - -export interface RulesFeatureTourContextType { - steps: EuiTourStepProps[]; - actions: EuiTourActions; -} - -const GUIDED_ONBOARDING_RULES_FILTER = { - filter: '', - showCustomRules: false, - showElasticRules: true, - tags: ['Guided Onboarding'], -}; - -export enum GuidedOnboardingRulesStatus { - 'inactive' = 'inactive', - 'installRules' = 'installRules', - 'activateRules' = 'activateRules', - 'completed' = 'completed', -} - -export const RulesManagementTour = () => { - const { guidedOnboardingApi } = useKibana().services.guidedOnboarding; - - const isRulesStepActive = useObservable( - guidedOnboardingApi?.isGuideStepActive$('security', 'rules') ?? of(false), - false - ); - - const { data: onboardingRules } = useFindRulesQuery( - { filterOptions: GUIDED_ONBOARDING_RULES_FILTER }, - { enabled: isRulesStepActive } - ); - - const tourStatus = useMemo(() => { - if (!isRulesStepActive || !onboardingRules) { - return GuidedOnboardingRulesStatus.inactive; - } - - if (onboardingRules.total === 0) { - // Onboarding rules are not installed - show the install/update rules step - return GuidedOnboardingRulesStatus.installRules; - } - - if (!onboardingRules.rules.some((rule) => rule.enabled)) { - // None of the onboarding rules is active - show the activate step - return GuidedOnboardingRulesStatus.activateRules; - } - - // Rules are installed and enabled - the tour is completed - return GuidedOnboardingRulesStatus.completed; - }, [isRulesStepActive, onboardingRules]); - - // Synchronize the current "internal" tour step with the global one - useEffect(() => { - if (isRulesStepActive && tourStatus === GuidedOnboardingRulesStatus.completed) { - guidedOnboardingApi?.completeGuideStep('security', 'rules'); - } - }, [guidedOnboardingApi, isRulesStepActive, tourStatus]); - - /** - * 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 isInstallRulesAnchorMounted = useIsElementMounted(INSTALL_PREBUILT_RULES_ANCHOR); - const isSearchFirstRuleAnchorMounted = useIsElementMounted(SEARCH_FIRST_RULE_ANCHOR); - - return ( - <> - {isInstallRulesAnchorMounted && ( - } // Replace "Skip tour" with an empty element - /> - )} - {isSearchFirstRuleAnchorMounted && ( - } // Replace "Skip tour" with an empty element - /> - )} - - ); -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/translations.ts deleted file mode 100644 index 6c8a2880801a6..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/translations.ts +++ /dev/null @@ -1,36 +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 INSTALL_PREBUILT_RULES_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.installPrebuiltRules.title', - { - defaultMessage: 'Load the Elastic prebuilt rules', - } -); - -export const INSTALL_PREBUILT_RULES_CONTENT = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.installPrebuiltRules.content', - { - defaultMessage: 'To get started you need to load the Elastic prebuilt rules.', - } -); - -export const SEARCH_FIRST_RULE_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.searchFirstRule.title', - { - defaultMessage: 'Search for Elastic Defend rules', - } -); - -export const SEARCH_FIRST_RULE_CONTENT = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.searchFirstRule.content', - { - defaultMessage: 'Find the My First Alert rule and enable it.', - } -); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx index 5e93508339980..d19fa01dc8610 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/index.tsx @@ -7,6 +7,7 @@ import { EuiSpacer } from '@elastic/eui'; import React, { useState } from 'react'; +import { RulesManagementTour } from './rules_table/guided_onboarding/rules_management_tour'; import { RulesTables } from './rules_tables'; import { AllRulesTabs, RulesTableToolbar } from './rules_table_toolbar'; @@ -23,6 +24,7 @@ export const AllRules = React.memo(() => { return ( <> + diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/rules_management_tour.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/rules_management_tour.tsx new file mode 100644 index 0000000000000..6a43ba92a9dd7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/rules_management_tour.tsx @@ -0,0 +1,180 @@ +/* + * 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 { EuiTourActions, EuiTourStepProps } from '@elastic/eui'; +import { EuiButton, EuiTourStep } from '@elastic/eui'; +import { noop } from 'lodash'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { of } from 'rxjs'; +import { BulkActionType } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; +import { useKibana } from '../../../../../../common/lib/kibana'; +import { useFindRulesQuery } from '../../../../../rule_management/api/hooks/use_find_rules_query'; +import { useExecuteBulkAction } from '../../../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; +import { useRulesTableContext } from '../rules_table_context'; +import * as i18n from './translations'; +import { useIsElementMounted } from './use_is_element_mounted'; + +export const INSTALL_PREBUILT_RULES_ANCHOR = 'install-prebuilt-rules-anchor'; +export const SEARCH_FIRST_RULE_ANCHOR = 'search-first-rule-anchor'; + +export interface RulesFeatureTourContextType { + steps: EuiTourStepProps[]; + actions: EuiTourActions; +} + +const GUIDED_ONBOARDING_RULES_FILTER = { + filter: '', + showCustomRules: false, + showElasticRules: true, + tags: ['Guided Onboarding'], +}; + +export enum GuidedOnboardingRulesStatus { + 'inactive' = 'inactive', + 'installRules' = 'installRules', + 'searchRules' = 'searchRules', + 'enableRules' = 'enableRules', + 'completed' = 'completed', +} + +export const RulesManagementTour = () => { + const { guidedOnboardingApi } = useKibana().services.guidedOnboarding; + const { executeBulkAction } = useExecuteBulkAction(); + const { actions } = useRulesTableContext(); + + const isRulesStepActive = useObservable( + guidedOnboardingApi?.isGuideStepActive$('security', 'rules') ?? of(false), + false + ); + + const { data: onboardingRules } = useFindRulesQuery( + { filterOptions: GUIDED_ONBOARDING_RULES_FILTER }, + { enabled: isRulesStepActive } + ); + + const demoRule = useMemo(() => { + // Rules are loading, cannot search for rule ID + if (!onboardingRules?.rules.length) { + return; + } + // Return any rule, first one is good enough + return onboardingRules.rules[0]; + }, [onboardingRules]); + + const ruleSwitchAnchor = demoRule ? `rule-switch-${demoRule.id}` : ''; + + /** + * 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 isInstallRulesAnchorMounted = useIsElementMounted(INSTALL_PREBUILT_RULES_ANCHOR); + const isSearchFirstRuleAnchorMounted = useIsElementMounted(SEARCH_FIRST_RULE_ANCHOR); + const isActivateFirstRuleAnchorMounted = useIsElementMounted(ruleSwitchAnchor); + + const tourStatus = useMemo(() => { + if (!isRulesStepActive || !onboardingRules) { + return GuidedOnboardingRulesStatus.inactive; + } + + if (onboardingRules.total === 0) { + // Onboarding rules are not installed - show the install/update rules step + return GuidedOnboardingRulesStatus.installRules; + } + + if (demoRule?.enabled) { + // Rules are installed and enabled - the tour is completed + return GuidedOnboardingRulesStatus.completed; + } + + // Rule is installed but not enabled - show the find and activate steps + if (isActivateFirstRuleAnchorMounted) { + // If rule is visible on the table, show the activation step + return GuidedOnboardingRulesStatus.enableRules; + } else { + // If rule is not visible on the table, show the search step + return GuidedOnboardingRulesStatus.searchRules; + } + }, [demoRule?.enabled, isActivateFirstRuleAnchorMounted, isRulesStepActive, onboardingRules]); + + // Synchronize the current "internal" tour step with the global one + useEffect(() => { + if (isRulesStepActive && tourStatus === GuidedOnboardingRulesStatus.completed) { + guidedOnboardingApi?.completeGuideStep('security', 'rules'); + } + }, [guidedOnboardingApi, isRulesStepActive, tourStatus]); + + const enableDemoRule = useCallback(async () => { + if (demoRule) { + await executeBulkAction({ + type: BulkActionType.enable, + ids: [demoRule.id], + }); + } + }, [demoRule, executeBulkAction]); + + const findDemoRule = useCallback(() => { + if (demoRule) { + actions.setFilterOptions({ + filter: demoRule.name, + }); + } + }, [actions, demoRule]); + + return ( + <> + {isInstallRulesAnchorMounted && ( + } // Replace "Skip tour" with an empty element + /> + )} + {isSearchFirstRuleAnchorMounted && demoRule && ( + + {i18n.NEXT_BUTTON} + + } + /> + )} + {isActivateFirstRuleAnchorMounted && demoRule && ( + + {i18n.NEXT_BUTTON} + + } + /> + )} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/translations.ts new file mode 100644 index 0000000000000..8a62c76add5bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/translations.ts @@ -0,0 +1,65 @@ +/* + * 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 INSTALL_PREBUILT_RULES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.installPrebuiltRules.title', + { + defaultMessage: 'Load the Elastic prebuilt rules', + } +); + +export const INSTALL_PREBUILT_RULES_CONTENT = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.installPrebuiltRules.content', + { + defaultMessage: 'To get started you need to load the Elastic prebuilt rules.', + } +); + +export const SEARCH_FIRST_RULE_TITLE = (name: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.searchFirstRule.title', + { + defaultMessage: 'Search for "{name}" rule', + values: { name }, + } + ); + +export const SEARCH_FIRST_RULE_CONTENT = (name: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.searchFirstRule.content', + { + defaultMessage: 'Find the "{name}" rule.', + values: { name }, + } + ); + +export const ENABLE_FIRST_RULE_TITLE = (name: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.enableFirstRule.title', + { + defaultMessage: 'Enable "{name}" rule', + values: { name }, + } + ); + +export const ENABLE_FIRST_RULE_CONTENT = (name: string) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.enableFirstRule.content', + { + defaultMessage: 'Enable the "{name}" rule.', + values: { name }, + } + ); + +export const NEXT_BUTTON = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.guidedOnboarding.nextButton', + { + defaultMessage: 'Next', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/use_is_element_mounted.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted.ts similarity index 100% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/guided_onboarding/use_is_element_mounted.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx index 143ae37a694d1..34c039c6c9eb7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_filters/rules_table_filters.tsx @@ -13,7 +13,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { RULES_TABLE_ACTIONS } from '../../../../../common/lib/apm/user_actions'; import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction'; @@ -22,7 +22,7 @@ import * as i18n from '../../../../../detections/pages/detection_engine/rules/tr import { useRulesTableContext } from '../rules_table/rules_table_context'; import { TagsFilterPopover } from './tags_filter_popover'; import { useTags } from '../../../../rule_management/logic/use_tags'; -import { SEARCH_FIRST_RULE_ANCHOR } from '../../guided_onboarding/rules_management_tour'; +import { SEARCH_FIRST_RULE_ANCHOR } from '../rules_table/guided_onboarding/rules_management_tour'; const FilterWrapper = styled(EuiFlexGroup)` margin-bottom: ${({ theme }) => theme.eui.euiSizeXS}; @@ -54,6 +54,17 @@ const RulesTableFiltersComponent = () => { const { showCustomRules, showElasticRules, tags: selectedTags } = filterOptions; + const [searchText, setSearchText] = useState(filterOptions.filter); + + useEffect(() => { + setSearchText(filterOptions.filter); + }, [filterOptions.filter]); + + const handleSearchInputChange = useCallback( + (e: React.ChangeEvent) => setSearchText(e.target.value), + [] + ); + const handleOnSearch = useCallback( (filterString) => { startTransaction({ name: RULES_TABLE_ACTIONS.FILTER }); @@ -87,11 +98,13 @@ const RulesTableFiltersComponent = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index 16a30acb29651..4409460574368 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -41,7 +41,6 @@ import { AllRules } from '../../components/rules_table'; import { RulesTableContextProvider } from '../../components/rules_table/rules_table/rules_table_context'; import * as i18n from '../../../../detections/pages/detection_engine/rules/translations'; -import { RulesManagementTour } from '../../components/guided_onboarding/rules_management_tour'; const RulesPageComponent: React.FC = () => { const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState(); @@ -92,7 +91,6 @@ const RulesPageComponent: React.FC = () => { - + {showLoader ? (