Skip to content

Commit

Permalink
[Security Solution] Address guided onboarding feedback for the rules …
Browse files Browse the repository at this point in the history
…area (#145223)

**Related to: #144016

## Summary

This follow-up PR addresses guided onboarding feedback mentioned
[here](elastic/security-team#5386) and
[here](#144458).

To summarize:

- We're keeping the first step (install prebuilt rules) intact, but most
users wouldn't see it as the rules are installed automatically during
previous stages. This step is needed to cover edge cases when rules were
deleted for some reason.
- We're splitting the second step into two: 1) search the first rule and
2) activate it.
- We're adding "Next" buttons to these steps.
- For the search step, the "Next" button will automatically filter the
rules table, so the first rule becomes visible.
- For the activate step, the "Next" button automatically activates the
first rule.
- The "Next" button stays optional; we still automatically progress the
guide once user actions satisfy certain conditions, like the user
filtered the rules table manually or activated the first rule by
clicking its toggle.
  • Loading branch information
xcrzx authored Nov 15, 2022
1 parent 768a56a commit 5ad2a36
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 160 deletions.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,6 +24,7 @@ export const AllRules = React.memo(() => {

return (
<>
<RulesManagementTour />
<RulesTableToolbar activeTab={activeTab} onTabChange={setActiveTab} />
<EuiSpacer />
<RulesTables selectedTab={activeTab} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 && (
<EuiTourStep
title={i18n.INSTALL_PREBUILT_RULES_TITLE}
content={i18n.INSTALL_PREBUILT_RULES_CONTENT}
onFinish={noop}
step={1}
stepsTotal={3}
isOpen={tourStatus === GuidedOnboardingRulesStatus.installRules}
anchor={`#${INSTALL_PREBUILT_RULES_ANCHOR}`}
anchorPosition="downCenter"
footerAction={<div />} // Replace "Skip tour" with an empty element
/>
)}
{isSearchFirstRuleAnchorMounted && demoRule && (
<EuiTourStep
title={i18n.SEARCH_FIRST_RULE_TITLE(demoRule.name)}
content={i18n.SEARCH_FIRST_RULE_CONTENT(demoRule.name)}
onFinish={noop}
step={2}
stepsTotal={3}
isOpen={tourStatus === GuidedOnboardingRulesStatus.searchRules}
anchor={`#${SEARCH_FIRST_RULE_ANCHOR}`}
anchorPosition="upCenter"
footerAction={
<EuiButton size="s" color="success" fill onClick={findDemoRule}>
{i18n.NEXT_BUTTON}
</EuiButton>
}
/>
)}
{isActivateFirstRuleAnchorMounted && demoRule && (
<EuiTourStep
title={i18n.ENABLE_FIRST_RULE_TITLE(demoRule.name)}
content={i18n.ENABLE_FIRST_RULE_CONTENT(demoRule.name)}
onFinish={noop}
step={3}
stepsTotal={3}
isOpen={tourStatus === GuidedOnboardingRulesStatus.enableRules}
anchor={`#${ruleSwitchAnchor}`}
anchorPosition="upCenter"
footerAction={
<EuiButton size="s" color="success" fill onClick={enableDemoRule}>
{i18n.NEXT_BUTTON}
</EuiButton>
}
/>
)}
</>
);
};
Loading

0 comments on commit 5ad2a36

Please sign in to comment.