From 6b8e8a5b468746f14beb6e17264e5b9c5bebc67b Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Thu, 15 Oct 2020 18:46:11 +0100
Subject: [PATCH] [Security Solution] Update button text according to status
(#80389)
* update button text according to status
* remove unused translations
* fix functional test
* fixup
* fix unit test
* update unit tests
* update unit test
---
.../load_empty_prompt.test.tsx | 96 ++++++++++-
.../pre_packaged_rules/load_empty_prompt.tsx | 42 +++--
.../rules/pre_packaged_rules/translations.ts | 7 -
.../detection_engine/rules/translations.ts | 54 ++++++
.../rules/use_pre_packaged_rules.test.tsx | 12 ++
.../rules/use_pre_packaged_rules.tsx | 128 +++++++++++++-
.../detection_engine/rules/index.test.tsx | 163 +++++++++++++++++-
.../pages/detection_engine/rules/index.tsx | 68 +++-----
.../detection_engine/rules/translations.ts | 40 -----
.../translations/translations/ja-JP.json | 3 +-
.../translations/translations/zh-CN.json | 3 +-
11 files changed, 492 insertions(+), 124 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx
index a41da908085bc..75ab1524c5c06 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx
@@ -5,10 +5,12 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
+import { waitFor } from '@testing-library/react';
+import { shallow, mount, ReactWrapper } from 'enzyme';
import '../../../../common/mock/match_media';
import { PrePackagedRulesPrompt } from './load_empty_prompt';
+import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -23,16 +25,94 @@ jest.mock('react-router-dom', () => {
jest.mock('../../../../common/components/link_to');
+jest.mock('../../../containers/detection_engine/rules/api', () => ({
+ getPrePackagedRulesStatus: jest.fn().mockResolvedValue({
+ rules_not_installed: 0,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 0,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ }),
+ createPrepackagedRules: jest.fn(),
+}));
+
+const props = {
+ createPrePackagedRules: jest.fn(),
+ loading: false,
+ userHasNoPermissions: false,
+ 'data-test-subj': 'load-prebuilt-rules',
+};
+
describe('PrePackagedRulesPrompt', () => {
it('renders correctly', () => {
- const wrapper = shallow(
-
- );
+ const wrapper = shallow();
expect(wrapper.find('EmptyPrompt')).toHaveLength(1);
});
});
+
+describe('LoadPrebuiltRulesAndTemplatesButton', () => {
+ it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 3,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 3,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount();
+ await waitFor(() => {
+ wrapper.update();
+
+ expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true);
+ expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual(
+ 'Load Elastic prebuilt rules and timeline templates'
+ );
+ });
+ });
+
+ it('renders correct button with correct text - Load Elastic prebuilt rules', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 3,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 0,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount();
+ await waitFor(() => {
+ wrapper.update();
+
+ expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true);
+ expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual(
+ 'Load Elastic prebuilt rules'
+ );
+ });
+ });
+
+ it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 0,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 3,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount();
+ await waitFor(() => {
+ wrapper.update();
+
+ expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true);
+ expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual(
+ 'Load Elastic prebuilt timeline templates'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx
index 99968cd4d9fe8..64b3cfa3aa991 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
-import React, { memo, useCallback } from 'react';
+import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import React, { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
@@ -14,6 +14,8 @@ import * as i18n from './translations';
import { LinkButton } from '../../../../common/components/links';
import { SecurityPageName } from '../../../../app/types';
import { useFormatUrl } from '../../../../common/components/link_to';
+import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
+import { useUserData } from '../../user_info';
const EmptyPrompt = styled(EuiEmptyPrompt)`
align-self: center; /* Corrects horizontal centering in IE11 */
@@ -46,24 +48,36 @@ const PrePackagedRulesPromptComponent: React.FC = (
[history]
);
+ const [
+ { isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite },
+ ] = useUserData();
+
+ const { getLoadPrebuiltRulesAndTemplatesButton } = usePrePackagedRules({
+ canUserCRUD,
+ hasIndexWrite,
+ isSignalIndexExists,
+ isAuthenticated,
+ hasEncryptionKey,
+ });
+
+ const loadPrebuiltRulesAndTemplatesButton = useMemo(
+ () =>
+ getLoadPrebuiltRulesAndTemplatesButton({
+ isDisabled: userHasNoPermissions,
+ onClick: handlePreBuiltCreation,
+ fill: true,
+ 'data-test-subj': 'load-prebuilt-rules',
+ }),
+ [getLoadPrebuiltRulesAndTemplatesButton, handlePreBuiltCreation, userHasNoPermissions]
+ );
+
return (
{i18n.PRE_BUILT_TITLE}}
body={{i18n.PRE_BUILT_MSG}
}
actions={
-
-
- {i18n.PRE_BUILT_ACTION}
-
-
+ {loadPrebuiltRulesAndTemplatesButton}
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton',
+ {
+ values: { missingRules },
+ defaultMessage:
+ 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ',
+ }
+ );
+
+export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton',
+ {
+ values: { missingTimelines },
+ defaultMessage:
+ 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ',
+ }
+ );
+
+export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = (
+ missingRules: number,
+ missingTimelines: number
+) =>
+ i18n.translate(
+ 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton',
+ {
+ values: { missingRules, missingTimelines },
+ defaultMessage:
+ 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ',
+ }
+ );
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
index 92d46a785b034..7f74e92584494 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx
@@ -32,6 +32,10 @@ describe('usePrePackagedRules', () => {
await waitForNextUpdate();
expect(result.current).toEqual({
+ getLoadPrebuiltRulesAndTemplatesButton:
+ result.current.getLoadPrebuiltRulesAndTemplatesButton,
+ getReloadPrebuiltRulesAndTemplatesButton:
+ result.current.getReloadPrebuiltRulesAndTemplatesButton,
createPrePackagedRules: null,
loading: true,
loadingCreatePrePackagedRules: false,
@@ -63,6 +67,10 @@ describe('usePrePackagedRules', () => {
await waitForNextUpdate();
expect(result.current).toEqual({
+ getLoadPrebuiltRulesAndTemplatesButton:
+ result.current.getLoadPrebuiltRulesAndTemplatesButton,
+ getReloadPrebuiltRulesAndTemplatesButton:
+ result.current.getReloadPrebuiltRulesAndTemplatesButton,
createPrePackagedRules: result.current.createPrePackagedRules,
loading: false,
loadingCreatePrePackagedRules: false,
@@ -100,6 +108,10 @@ describe('usePrePackagedRules', () => {
expect(resp).toEqual(true);
expect(spyOnCreatePrepackagedRules).toHaveBeenCalled();
expect(result.current).toEqual({
+ getLoadPrebuiltRulesAndTemplatesButton:
+ result.current.getLoadPrebuiltRulesAndTemplatesButton,
+ getReloadPrebuiltRulesAndTemplatesButton:
+ result.current.getReloadPrebuiltRulesAndTemplatesButton,
createPrePackagedRules: result.current.createPrePackagedRules,
loading: false,
loadingCreatePrePackagedRules: false,
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
index d82d97883a3d0..4d19f44bcfc84 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useState } from 'react';
+import React, { useCallback, useMemo, useState, useEffect } from 'react';
+import { EuiButton } from '@elastic/eui';
import {
errorToToaster,
@@ -14,6 +15,11 @@ import {
import { getPrePackagedRulesStatus, createPrepackagedRules } from './api';
import * as i18n from './translations';
+import {
+ getPrePackagedRuleStatus,
+ getPrePackagedTimelineStatus,
+} from '../../../pages/detection_engine/rules/helpers';
+
type Func = () => void;
export type CreatePreBuiltRules = () => Promise;
@@ -23,6 +29,23 @@ interface ReturnPrePackagedTimelines {
timelinesNotUpdated: number | null;
}
+type GetLoadPrebuiltRulesAndTemplatesButton = (args: {
+ isDisabled: boolean;
+ onClick: () => void;
+ fill?: boolean;
+ 'data-test-subj'?: string;
+}) => React.ReactNode | null;
+
+type GetReloadPrebuiltRulesAndTemplatesButton = ({
+ isDisabled,
+ onClick,
+ fill,
+}: {
+ isDisabled: boolean;
+ onClick: () => void;
+ fill?: boolean;
+}) => React.ReactNode | null;
+
interface ReturnPrePackagedRules {
createPrePackagedRules: null | CreatePreBuiltRules;
loading: boolean;
@@ -32,6 +55,8 @@ interface ReturnPrePackagedRules {
rulesInstalled: number | null;
rulesNotInstalled: number | null;
rulesNotUpdated: number | null;
+ getLoadPrebuiltRulesAndTemplatesButton: GetLoadPrebuiltRulesAndTemplatesButton;
+ getReloadPrebuiltRulesAndTemplatesButton: GetReloadPrebuiltRulesAndTemplatesButton;
}
export type ReturnPrePackagedRulesAndTimelines = ReturnPrePackagedRules &
@@ -89,7 +114,6 @@ export const usePrePackagedRules = ({
const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false);
const [loading, setLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();
-
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
@@ -100,7 +124,6 @@ export const usePrePackagedRules = ({
const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({
signal: abortCtrl.signal,
});
-
if (isSubscribed) {
setPrepackagedDataStatus({
createPrePackagedRules: createElasticRules,
@@ -225,9 +248,108 @@ export const usePrePackagedRules = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]);
+ const prePackagedRuleStatus = useMemo(
+ () =>
+ getPrePackagedRuleStatus(
+ prepackagedDataStatus.rulesInstalled,
+ prepackagedDataStatus.rulesNotInstalled,
+ prepackagedDataStatus.rulesNotUpdated
+ ),
+ [
+ prepackagedDataStatus.rulesInstalled,
+ prepackagedDataStatus.rulesNotInstalled,
+ prepackagedDataStatus.rulesNotUpdated,
+ ]
+ );
+
+ const prePackagedTimelineStatus = useMemo(
+ () =>
+ getPrePackagedTimelineStatus(
+ prepackagedDataStatus.timelinesInstalled,
+ prepackagedDataStatus.timelinesNotInstalled,
+ prepackagedDataStatus.timelinesNotUpdated
+ ),
+ [
+ prepackagedDataStatus.timelinesInstalled,
+ prepackagedDataStatus.timelinesNotInstalled,
+ prepackagedDataStatus.timelinesNotUpdated,
+ ]
+ );
+ const getLoadPrebuiltRulesAndTemplatesButton = useCallback(
+ ({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => {
+ return prePackagedRuleStatus === 'ruleNotInstalled' ||
+ prePackagedTimelineStatus === 'timelinesNotInstalled' ? (
+
+ {prePackagedRuleStatus === 'ruleNotInstalled' &&
+ prePackagedTimelineStatus === 'timelinesNotInstalled' &&
+ i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES}
+
+ {prePackagedRuleStatus === 'ruleNotInstalled' &&
+ prePackagedTimelineStatus !== 'timelinesNotInstalled' &&
+ i18n.LOAD_PREPACKAGED_RULES}
+
+ {prePackagedRuleStatus !== 'ruleNotInstalled' &&
+ prePackagedTimelineStatus === 'timelinesNotInstalled' &&
+ i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES}
+
+ ) : null;
+ },
+ [loadingCreatePrePackagedRules, prePackagedRuleStatus, prePackagedTimelineStatus]
+ );
+
+ const getMissingRulesOrTimelinesButtonTitle = useCallback(
+ (missingRules: number, missingTimelines: number) => {
+ if (missingRules > 0 && missingTimelines === 0)
+ return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules);
+ else if (missingRules === 0 && missingTimelines > 0)
+ return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines);
+ else if (missingRules > 0 && missingTimelines > 0)
+ return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines);
+ },
+ []
+ );
+
+ const getReloadPrebuiltRulesAndTemplatesButton = useCallback(
+ ({ isDisabled, onClick, fill = false }) => {
+ return prePackagedRuleStatus === 'someRuleUninstall' ||
+ prePackagedTimelineStatus === 'someTimelineUninstall' ? (
+
+ {getMissingRulesOrTimelinesButtonTitle(
+ prepackagedDataStatus.rulesNotInstalled ?? 0,
+ prepackagedDataStatus.timelinesNotInstalled ?? 0
+ )}
+
+ ) : null;
+ },
+ [
+ getMissingRulesOrTimelinesButtonTitle,
+ loadingCreatePrePackagedRules,
+ prePackagedRuleStatus,
+ prePackagedTimelineStatus,
+ prepackagedDataStatus.rulesNotInstalled,
+ prepackagedDataStatus.timelinesNotInstalled,
+ ]
+ );
+
return {
loading,
loadingCreatePrePackagedRules,
...prepackagedDataStatus,
+ getLoadPrebuiltRulesAndTemplatesButton,
+ getReloadPrebuiltRulesAndTemplatesButton,
};
};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx
index 886a24dd7cbe8..58e61c5b47486 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx
@@ -5,13 +5,14 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount, ReactWrapper } from 'enzyme';
import '../../../../common/mock/match_media';
import { RulesPage } from './index';
import { useUserData } from '../../../components/user_info';
-import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
-
+import { waitFor } from '@testing-library/react';
+import { TestProviders } from '../../../../common/mock';
+import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -26,16 +27,164 @@ jest.mock('react-router-dom', () => {
jest.mock('../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../common/components/link_to');
jest.mock('../../../components/user_info');
-jest.mock('../../../containers/detection_engine/rules');
+jest.mock('../../../../common/components/toasters', () => {
+ const actual = jest.requireActual('../../../../common/components/toasters');
+ return {
+ ...actual,
+ errorToToaster: jest.fn(),
+ useStateToaster: jest.fn().mockReturnValue([jest.fn(), jest.fn()]),
+ displaySuccessToast: jest.fn(),
+ };
+});
+
+jest.mock('../../../containers/detection_engine/rules/api', () => ({
+ getPrePackagedRulesStatus: jest.fn().mockResolvedValue({
+ rules_not_installed: 0,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 0,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ }),
+ createPrepackagedRules: jest.fn(),
+}));
+
+jest.mock('../../../../common/lib/kibana', () => {
+ return {
+ useToast: jest.fn(),
+ useHttp: jest.fn(),
+ };
+});
+
+jest.mock('../../../components/value_lists_management_modal', () => {
+ return {
+ ValueListsModal: jest.fn().mockReturnValue(),
+ };
+});
+
+jest.mock('./all', () => {
+ return {
+ AllRules: jest.fn().mockReturnValue(),
+ };
+});
+
+jest.mock('../../../../common/utils/route/spy_routes', () => {
+ return {
+ SpyRoute: jest.fn().mockReturnValue(),
+ };
+});
+
+jest.mock('../../../components/rules/pre_packaged_rules/update_callout', () => {
+ return {
+ UpdatePrePackagedRulesCallOut: jest.fn().mockReturnValue(),
+ };
+});
describe('RulesPage', () => {
beforeAll(() => {
(useUserData as jest.Mock).mockReturnValue([{}]);
- (usePrePackagedRules as jest.Mock).mockReturnValue({});
});
- it('renders correctly', () => {
+
+ it('renders AllRules', () => {
const wrapper = shallow();
+ expect(wrapper.find('[data-test-subj="all-rules"]').exists()).toEqual(true);
+ });
+
+ it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 3,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 3,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ wrapper.update();
+
+ expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true);
+ expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual(
+ 'Load Elastic prebuilt rules and timeline templates'
+ );
+ });
+ });
+
+ it('renders correct button with correct text - Load Elastic prebuilt rules', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 3,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 0,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount(
+
+
+
+ );
+
+ await waitFor(() => {
+ wrapper.update();
+
+ expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true);
+ expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual(
+ 'Load Elastic prebuilt rules'
+ );
+ });
+ });
+
+ it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 0,
+ rules_installed: 0,
+ rules_not_updated: 0,
+ timelines_not_installed: 3,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount(
+
+
+
+ );
+
+ await waitFor(() => {
+ wrapper.update();
+ expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true);
+ expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual(
+ 'Load Elastic prebuilt timeline templates'
+ );
+ });
+ });
+
+ it('renders a callout - Update Elastic prebuilt rules', async () => {
+ (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({
+ rules_not_installed: 2,
+ rules_installed: 1,
+ rules_not_updated: 1,
+ timelines_not_installed: 0,
+ timelines_installed: 0,
+ timelines_not_updated: 0,
+ });
+
+ const wrapper: ReactWrapper = mount(
+
+
+
+ );
- expect(wrapper.find('AllRules')).toHaveLength(1);
+ await waitFor(() => {
+ wrapper.update();
+ expect(wrapper.find('[data-test-subj="update-callout-button"]').exists()).toEqual(true);
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
index 53c82569f94ae..8c7cb6a0d9284 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx
@@ -5,7 +5,7 @@
*/
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
-import React, { useCallback, useRef, useState } from 'react';
+import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules';
@@ -70,6 +70,8 @@ const RulesPageComponent: React.FC = () => {
timelinesInstalled,
timelinesNotInstalled,
timelinesNotUpdated,
+ getLoadPrebuiltRulesAndTemplatesButton,
+ getReloadPrebuiltRulesAndTemplatesButton,
} = usePrePackagedRules({
canUserCRUD,
hasIndexWrite,
@@ -113,18 +115,6 @@ const RulesPageComponent: React.FC = () => {
refreshRulesData.current = refreshRule;
}, []);
- const getMissingRulesOrTimelinesButtonTitle = useCallback(
- (missingRules: number, missingTimelines: number) => {
- if (missingRules > 0 && missingTimelines === 0)
- return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules);
- else if (missingRules === 0 && missingTimelines > 0)
- return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines);
- else if (missingRules > 0 && missingTimelines > 0)
- return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines);
- },
- []
- );
-
const goToNewRule = useCallback(
(ev) => {
ev.preventDefault();
@@ -133,6 +123,24 @@ const RulesPageComponent: React.FC = () => {
[history]
);
+ const loadPrebuiltRulesAndTemplatesButton = useMemo(
+ () =>
+ getLoadPrebuiltRulesAndTemplatesButton({
+ isDisabled: userHasNoPermissions(canUserCRUD) || loading,
+ onClick: handleCreatePrePackagedRules,
+ }),
+ [canUserCRUD, getLoadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading]
+ );
+
+ const reloadPrebuiltRulesAndTemplatesButton = useMemo(
+ () =>
+ getReloadPrebuiltRulesAndTemplatesButton({
+ isDisabled: userHasNoPermissions(canUserCRUD) || loading,
+ onClick: handleCreatePrePackagedRules,
+ }),
+ [canUserCRUD, getReloadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading]
+ );
+
if (
redirectToDetections(
isSignalIndexExists,
@@ -177,35 +185,11 @@ const RulesPageComponent: React.FC = () => {
title={i18n.PAGE_TITLE}
>
- {(prePackagedRuleStatus === 'ruleNotInstalled' ||
- prePackagedTimelineStatus === 'timelinesNotInstalled') && (
-
-
- {i18n.LOAD_PREPACKAGED_RULES}
-
-
+ {loadPrebuiltRulesAndTemplatesButton && (
+ {loadPrebuiltRulesAndTemplatesButton}
)}
- {(prePackagedRuleStatus === 'someRuleUninstall' ||
- prePackagedTimelineStatus === 'someTimelineUninstall') && (
-
-
- {getMissingRulesOrTimelinesButtonTitle(
- rulesNotInstalled ?? 0,
- timelinesNotInstalled ?? 0
- )}
-
-
+ {reloadPrebuiltRulesAndTemplatesButton && (
+ {reloadPrebuiltRulesAndTemplatesButton}
)}
@@ -247,6 +231,7 @@ const RulesPageComponent: React.FC = () => {
{(prePackagedRuleStatus === 'ruleNeedUpdate' ||
prePackagedTimelineStatus === 'timelineNeedUpdate') && (
{
)}
- i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton',
- {
- values: { missingRules },
- defaultMessage:
- 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ',
- }
- );
-
-export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) =>
- i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton',
- {
- values: { missingTimelines },
- defaultMessage:
- 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ',
- }
- );
-
-export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = (
- missingRules: number,
- missingTimelines: number
-) =>
- i18n.translate(
- 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton',
- {
- values: { missingRules, missingTimelines },
- defaultMessage:
- 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ',
- }
- );
-
export const IMPORT_RULE_BTN_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle',
{
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 763b26de00940..d0c24b0239b62 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -15574,13 +15574,12 @@
"xpack.securitySolution.detectionEngine.rules.deleteDescription": "削除",
"xpack.securitySolution.detectionEngine.rules.editPageTitle": "編集",
"xpack.securitySolution.detectionEngine.rules.importRuleTitle": "ルールのインポート...",
- "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む",
+ "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む",
"xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "オプション",
"xpack.securitySolution.detectionEngine.rules.pageTitle": "検出ルール",
"xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "独自のルールの作成",
"xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、Elastic Endpoint Securityルールを除くすべての事前構築済みルールが無効になっています。有効にする追加のルールを選択することができます。",
"xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "Elastic事前構築済み検出ルールを読み込む",
- "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "事前構築済み検出ルールおよびタイムラインテンプレートを読み込む",
"xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "リリースノート",
"xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "{missingRules} Elastic事前構築済み{missingRules, plural, =1 {ルール} other {ルール}}と{missingTimelines} Elastic事前構築済み{missingTimelines, plural, =1 {タイムライン} other {タイムライン}}をインストール ",
"xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "{missingRules} Elasticの事前構築済みの{missingRules, plural, =1 {個のルール} other {個のルール}}をインストール ",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 4245b446c5b93..db73cd8043e7e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -15583,13 +15583,12 @@
"xpack.securitySolution.detectionEngine.rules.deleteDescription": "删除",
"xpack.securitySolution.detectionEngine.rules.editPageTitle": "编辑",
"xpack.securitySolution.detectionEngine.rules.importRuleTitle": "导入规则……",
- "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "加载 Elastic 预构建规则和时间线模板",
+ "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton": "加载 Elastic 预构建规则和时间线模板",
"xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "可选",
"xpack.securitySolution.detectionEngine.rules.pageTitle": "检测规则",
"xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "创建自己的规则",
"xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件满足时创建告警。默认情况下,除 Elastic Endpoint Security 规则外,所有预置规则都处于禁用状态。您可以选择其他要激活的规则。",
"xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "加载 Elastic 预构建检测规则",
- "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预置检测规则和时间线模板",
"xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "发行说明",
"xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}}以及 {missingTimelines} 个 Elastic 预构建{missingTimelines, plural, =1 {时间线} other {时间线}} ",
"xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}} ",