Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Address guided onboarding feedback for the rules area #145223

Merged
merged 1 commit into from
Nov 15, 2022
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

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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious why enum keys are string literals while it can be started with capital case keys as in the TypeScript docs? So it could be

export enum GuidedOnboardingRulesStatus {
  Inactive = 'inactive',
  InstallRules = 'installRules',
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, no intention here, to be honest. FWIW, in security solution, enum formats are not very consistent, but the most widespread one is CamelCase for names and snake_case for members:

Screenshot 2022-11-15 at 12 19 34

I'd stick to it for consistency.

'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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea that only demoRule is needed so it's possible to check if that rule is loaded. So the functionality can be moved to a separate hook like useDemoRule() which returns demoRule and isLoading which should be enough to properly determine the tourStatus.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I like that idea 👍 We could improve that in a follow-up if you don't mind.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

{ 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