diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx
index 91b06103b8e39..ffb29888a6037 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/page/configuration_router.tsx
@@ -10,7 +10,7 @@ import React from 'react';
import { Routes, Route } from '@kbn/shared-ux-router';
import { Redirect } from 'react-router-dom';
import { AISettings } from '../tabs/ai_settings';
-import { BasicRules } from '../tabs/basic_rules';
+import { PromotionRules } from '../tabs/promotion_rules';
import { CONFIGURATIONS_PATH } from '../../../common/constants';
import { ConfigurationTabs } from '../constants';
import { LazyConfigurationsIntegrationsHome } from '../tabs/integrations';
@@ -28,7 +28,7 @@ export const ConfigurationsRouter = React.memo(() => {
/>
{
- return {'Basic Rules'}
;
-};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules.tsx
new file mode 100644
index 0000000000000..9e66dac38c71c
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 { EuiSpacer } from '@elastic/eui';
+import React from 'react';
+import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
+import { RulesTableContextProvider } from '../../detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context';
+import { PromotionRulesTable } from './promotion_rules/promotion_rules_table';
+
+export const PromotionRules: React.FC = () => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules/promotion_rules_table.tsx b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules/promotion_rules_table.tsx
new file mode 100644
index 0000000000000..5aadf25ab5d47
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules/promotion_rules_table.tsx
@@ -0,0 +1,228 @@
+/*
+ * 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 { EuiBasicTableColumn } from '@elastic/eui';
+import {
+ EuiBasicTable,
+ EuiEmptyPrompt,
+ EuiNotificationBadge,
+ EuiProgress,
+ EuiSpacer,
+ EuiTab,
+ EuiTabs,
+ EuiText,
+} from '@elastic/eui';
+import React, { useCallback, useMemo, useState } from 'react';
+import type { FindRulesSortField } from '../../../../common/api/detection_engine';
+import { Loader } from '../../../common/components/loader';
+import { hasUserCRUDPermission } from '../../../common/utils/privileges';
+import type { EuiBasicTableOnChange } from '../../../detection_engine/common/types';
+import type { Rule } from '../../../detection_engine/rule_management/logic';
+import { useRuleManagementFilters } from '../../../detection_engine/rule_management/logic/use_rule_management_filters';
+import { useIsUpgradingSecurityPackages } from '../../../detection_engine/rule_management/logic/use_upgrade_security_packages';
+import { RULES_TABLE_PAGE_SIZE_OPTIONS } from '../../../detection_engine/rule_management_ui/components/rules_table/constants';
+import { useRulesTableContext } from '../../../detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context';
+import {
+ INDEXING_DURATION_COLUMN,
+ LAST_EXECUTION_COLUMN,
+ RULE_NAME_COLUMN,
+ SEARCH_DURATION_COLUMN,
+ TOTAL_UNFILLED_DURATION_COLUMN,
+ useEnabledColumn,
+ useGapDurationColumn,
+ useRuleExecutionStatusColumn,
+} from '../../../detection_engine/rule_management_ui/components/rules_table/use_columns';
+import { useUserData } from '../../../detections/components/user_info';
+import * as i18n from './translations';
+
+const INITIAL_SORT_FIELD = 'name';
+
+const NO_ITEMS_MESSAGE = (
+ {i18n.NO_RULES}} titleSize="xs" body={i18n.NO_RULES_BODY} />
+);
+
+export enum PromotionRuleTabs {
+ management = 'management',
+ monitoring = 'monitoring',
+}
+
+export const PromotionRulesTable = () => {
+ const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
+ const rulesTableContext = useRulesTableContext();
+ const { data: ruleManagementFilters } = useRuleManagementFilters();
+ const [currentTab, setCurrentTab] = useState(PromotionRuleTabs.management);
+
+ const {
+ state: { rules, isFetched, isRefetching, pagination, sortingOptions },
+ actions: { setPage, setPerPage, setSortingOptions },
+ } = rulesTableContext;
+
+ const paginationMemo = useMemo(() => {
+ return {
+ pageIndex: pagination.page - 1,
+ pageSize: pagination.perPage,
+ totalItemCount: pagination.total,
+ pageSizeOptions: RULES_TABLE_PAGE_SIZE_OPTIONS,
+ };
+ }, [pagination.page, pagination.perPage, pagination.total]);
+
+ const tableOnChangeCallback = useCallback(
+ ({ page, sort }: EuiBasicTableOnChange) => {
+ setSortingOptions({
+ field: (sort?.field as FindRulesSortField) ?? INITIAL_SORT_FIELD,
+ order: sort?.direction ?? 'desc',
+ });
+ setPage(page.index + 1);
+ setPerPage(page.size);
+ },
+ [setPage, setPerPage, setSortingOptions]
+ );
+
+ const rulesColumns = useRulesColumns({ currentTab });
+
+ const handleTabClick = useCallback(
+ (tabId: PromotionRuleTabs) => {
+ setCurrentTab(tabId);
+ rulesTableContext.actions.setPage(1);
+ rulesTableContext.actions.setPerPage(pagination.perPage);
+ rulesTableContext.actions.setSortingOptions({
+ field: INITIAL_SORT_FIELD,
+ order: 'desc',
+ });
+ },
+ [pagination.perPage, rulesTableContext.actions]
+ );
+
+ const installedTotal =
+ (ruleManagementFilters?.rules_summary.custom_count ?? 0) +
+ (ruleManagementFilters?.rules_summary.prebuilt_installed_count ?? 0);
+
+ const ruleTabs = useMemo(
+ () => [
+ {
+ id: PromotionRuleTabs.management,
+ name: i18n.INSTALLED_RULES_TAB,
+ append:
+ installedTotal > 0 ? (
+
+ {installedTotal}
+
+ ) : undefined,
+ isSelected: currentTab === PromotionRuleTabs.management,
+ onClick: () => handleTabClick(PromotionRuleTabs.management),
+ },
+ {
+ id: PromotionRuleTabs.monitoring,
+ name: i18n.RULE_MONITORING_TAB,
+ append:
+ installedTotal > 0 ? (
+
+ {installedTotal}
+
+ ) : undefined,
+ isSelected: currentTab === PromotionRuleTabs.monitoring,
+ onClick: () => handleTabClick(PromotionRuleTabs.monitoring),
+ },
+ ],
+ [currentTab, handleTabClick, installedTotal]
+ );
+
+ const shouldShowLinearProgress = (isFetched && isRefetching) || isUpgradingSecurityPackages;
+ const shouldShowLoadingOverlay = !isFetched && isRefetching;
+
+ return (
+ <>
+
+ {ruleTabs.map((tab) => (
+
+ {tab.name}
+
+ ))}
+
+
+ {shouldShowLinearProgress && (
+
+ )}
+ {shouldShowLoadingOverlay && (
+
+ )}
+
+ >
+ );
+};
+
+interface ColumnsProps {
+ currentTab: PromotionRuleTabs;
+}
+
+const useRulesColumns = ({ currentTab }: ColumnsProps): Array> => {
+ const [{ canUserCRUD }] = useUserData();
+ const hasPermissions = hasUserCRUDPermission(canUserCRUD);
+
+ const enabledColumn = useEnabledColumn({
+ hasCRUDPermissions: hasPermissions,
+ isLoadingJobs: false,
+ mlJobs: [],
+ startMlJobs: async (jobIds: string[] | undefined) => {},
+ });
+ const executionStatusColumn = useRuleExecutionStatusColumn({
+ sortable: true,
+ width: '16%',
+ isLoadingJobs: false,
+ mlJobs: [],
+ });
+ const gapDurationColumn = useGapDurationColumn();
+
+ return useMemo(() => {
+ if (currentTab === PromotionRuleTabs.monitoring) {
+ return [
+ {
+ ...RULE_NAME_COLUMN,
+ render: (value: Rule['name']) => {value},
+ width: '30%',
+ } as EuiBasicTableColumn,
+ INDEXING_DURATION_COLUMN,
+ SEARCH_DURATION_COLUMN,
+ gapDurationColumn,
+ TOTAL_UNFILLED_DURATION_COLUMN,
+ LAST_EXECUTION_COLUMN,
+ executionStatusColumn,
+ enabledColumn,
+ ];
+ }
+
+ return [
+ {
+ ...RULE_NAME_COLUMN,
+ render: (value: Rule['name']) => {value},
+ width: '100%',
+ } as EuiBasicTableColumn,
+ LAST_EXECUTION_COLUMN,
+ executionStatusColumn,
+ enabledColumn,
+ ];
+ }, [currentTab, enabledColumn, executionStatusColumn, gapDurationColumn]);
+};
diff --git a/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules/translations.ts
new file mode 100644
index 0000000000000..a91455d28a84a
--- /dev/null
+++ b/x-pack/solutions/security/plugins/security_solution/public/configurations/tabs/promotion_rules/translations.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 NO_RULES = i18n.translate(
+ 'xpack.securitySolution.ai4soc.configurations.rulesTable.noRules',
+ {
+ defaultMessage: 'No rules found',
+ }
+);
+
+export const NO_RULES_BODY = i18n.translate(
+ 'xpack.securitySolution.ai4soc.configurations.rulesTable.noRulesBody',
+ {
+ defaultMessage: 'No rules are currently installed. To get started, add an integration.',
+ }
+);
+
+export const INSTALLED_RULES_TAB = i18n.translate(
+ 'xpack.securitySolution.ai4soc.configurations.promotionRules.installedRulesTab',
+ {
+ defaultMessage: 'Installed rules',
+ }
+);
+export const RULE_MONITORING_TAB = i18n.translate(
+ 'xpack.securitySolution.ai4soc.configurations.promotionRules.ruleMonitoringTab',
+ {
+ defaultMessage: 'Rule monitoring',
+ }
+);
diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx
index 51578a83bebbe..48c0d5ba7be7b 100644
--- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx
+++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx
@@ -62,7 +62,10 @@ interface ActionColumnsProps {
confirmDeletion: () => Promise;
}
-const useEnabledColumn = ({ hasCRUDPermissions, startMlJobs }: ColumnsProps): TableColumn => {
+export const useEnabledColumn = ({
+ hasCRUDPermissions,
+ startMlJobs,
+}: ColumnsProps): TableColumn => {
const hasMlPermissions = useHasMlPermissions();
const hasActionsPrivileges = useHasActionsPrivileges();
const { loadingRulesAction, loadingRuleIds } = useRulesTableContext().state;
@@ -137,21 +140,16 @@ export const RuleLink = ({ name, id }: Pick) => {
);
};
-const useRuleNameColumn = (): TableColumn => {
- return useMemo(
- () => ({
- field: 'name',
- name: i18n.COLUMN_RULE,
- render: (value: Rule['name'], item: Rule) => ,
- sortable: true,
- truncateText: true,
- width: '38%',
- }),
- []
- );
+export const RULE_NAME_COLUMN: TableColumn = {
+ field: 'name',
+ name: i18n.COLUMN_RULE,
+ render: (value: Rule['name'], item: Rule) => ,
+ sortable: true,
+ truncateText: true,
+ width: '38%',
};
-const useRuleExecutionStatusColumn = ({
+export const useRuleExecutionStatusColumn = ({
sortable,
width,
isLoadingJobs,
@@ -262,6 +260,30 @@ const MODIFIED_COLUMN: TableColumn = {
truncateText: true,
};
+export const LAST_EXECUTION_COLUMN = {
+ field: 'execution_summary.last_execution.date',
+ name: i18n.COLUMN_LAST_COMPLETE_RUN,
+ render: (value: RuleExecutionSummary['last_execution']['date'] | undefined) => {
+ return (
+
+ {value == null ? (
+ getEmptyTagValue()
+ ) : (
+
+ )}
+
+ );
+ },
+ sortable: true,
+ truncateText: true,
+ width: '16%',
+};
+
const useActionsColumn = ({
showExceptionsDuplicateConfirmation,
showManualRuleRunConfirmation,
@@ -292,7 +314,6 @@ export const useRulesColumns = ({
showManualRuleRunConfirmation,
confirmDeletion,
});
- const ruleNameColumn = useRuleNameColumn();
const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING);
const enabledColumn = useEnabledColumn({
@@ -311,7 +332,7 @@ export const useRulesColumns = ({
return useMemo(
() => [
- ruleNameColumn,
+ RULE_NAME_COLUMN,
MODIFIED_COLUMN,
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
@@ -335,29 +356,7 @@ export const useRulesColumns = ({
truncateText: true,
width: '12%',
},
- {
- field: 'execution_summary.last_execution.date',
- name: i18n.COLUMN_LAST_COMPLETE_RUN,
- render: (value: RuleExecutionSummary['last_execution']['date'] | undefined) => {
- return (
-
- {value == null ? (
- getEmptyTagValue()
- ) : (
-
- )}
-
- );
- },
- sortable: true,
- truncateText: true,
- width: '16%',
- },
+ LAST_EXECUTION_COLUMN,
executionStatusColumn,
{
field: 'updated_at',
@@ -383,7 +382,6 @@ export const useRulesColumns = ({
...(hasCRUDPermissions ? [actionsColumn] : []),
],
[
- ruleNameColumn,
showRelatedIntegrations,
executionStatusColumn,
snoozeColumn,
@@ -394,6 +392,100 @@ export const useRulesColumns = ({
);
};
+export const INDEXING_DURATION_COLUMN = {
+ field: 'execution_summary.last_execution.metrics.total_indexing_duration_ms',
+ name: (
+
+ ),
+ render: (value: number | undefined) => (
+
+ {value != null ? value.toFixed() : getEmptyTagValue()}
+
+ ),
+ sortable: true,
+ truncateText: true,
+ width: '16%',
+};
+
+export const SEARCH_DURATION_COLUMN = {
+ field: 'execution_summary.last_execution.metrics.total_search_duration_ms',
+ name: (
+
+ ),
+ render: (value: number | undefined) => (
+
+ {value != null ? value.toFixed() : getEmptyTagValue()}
+
+ ),
+ sortable: true,
+ truncateText: true,
+ width: '14%',
+};
+
+export const useGapDurationColumn = () => {
+ const docLinks = useKibana().services.docLinks;
+
+ return {
+ field: 'execution_summary.last_execution.metrics.execution_gap_duration_s',
+ name: (
+
+
+
+
+ {i18n.COLUMN_GAP_TOOLTIP_SEE_DOCUMENTATION}
+
+ ),
+ }}
+ />
+
+
+
+ }
+ />
+ ),
+ render: (value: number | undefined) => (
+
+ {value != null ? moment.duration(value, 'seconds').humanize() : getEmptyTagValue()}
+
+ ),
+ sortable: true,
+ truncateText: true,
+ width: '14%',
+ };
+};
+
+export const TOTAL_UNFILLED_DURATION_COLUMN = {
+ field: 'gap_info.total_unfilled_duration_ms',
+ name: (
+
+ ),
+ render: (value: number | undefined) => (
+
+ {value != null && value > 0 ? moment.duration(value, 'ms').humanize() : getEmptyTagValue()}
+
+ ),
+ sortable: false,
+ truncateText: true,
+ width: '14%',
+};
+
export const useMonitoringColumns = ({
hasCRUDPermissions,
isLoadingJobs,
@@ -403,13 +495,11 @@ export const useMonitoringColumns = ({
showManualRuleRunConfirmation,
confirmDeletion,
}: UseColumnsProps): TableColumn[] => {
- const docLinks = useKibana().services.docLinks;
const actionsColumn = useActionsColumn({
showExceptionsDuplicateConfirmation,
showManualRuleRunConfirmation,
confirmDeletion,
});
- const ruleNameColumn = useRuleNameColumn();
const [showRelatedIntegrations] = useUiSetting$(SHOW_RELATED_INTEGRATIONS_SETTING);
const enabledColumn = useEnabledColumn({
@@ -424,138 +514,32 @@ export const useMonitoringColumns = ({
isLoadingJobs,
mlJobs,
});
+ const gapDurationColumn = useGapDurationColumn();
return useMemo(
() => [
{
- ...ruleNameColumn,
+ ...RULE_NAME_COLUMN,
width: '28%',
},
MODIFIED_COLUMN,
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
TAGS_COLUMN,
- {
- field: 'execution_summary.last_execution.metrics.total_indexing_duration_ms',
- name: (
-
- ),
- render: (value: number | undefined) => (
-
- {value != null ? value.toFixed() : getEmptyTagValue()}
-
- ),
- sortable: true,
- truncateText: true,
- width: '16%',
- },
- {
- field: 'execution_summary.last_execution.metrics.total_search_duration_ms',
- name: (
-
- ),
- render: (value: number | undefined) => (
-
- {value != null ? value.toFixed() : getEmptyTagValue()}
-
- ),
- sortable: true,
- truncateText: true,
- width: '14%',
- },
- {
- field: 'execution_summary.last_execution.metrics.execution_gap_duration_s',
- name: (
-
-
-
-
- {i18n.COLUMN_GAP_TOOLTIP_SEE_DOCUMENTATION}
-
- ),
- }}
- />
-
-
-
- }
- />
- ),
- render: (value: number | undefined) => (
-
- {value != null ? moment.duration(value, 'seconds').humanize() : getEmptyTagValue()}
-
- ),
- sortable: true,
- truncateText: true,
- width: '14%',
- },
- {
- field: 'gap_info.total_unfilled_duration_ms',
- name: (
-
- ),
- render: (value: number | undefined) => (
-
- {value != null && value > 0
- ? moment.duration(value, 'ms').humanize()
- : getEmptyTagValue()}
-
- ),
- sortable: false,
- truncateText: true,
- width: '14%',
- },
+ INDEXING_DURATION_COLUMN,
+ SEARCH_DURATION_COLUMN,
+ gapDurationColumn,
+ TOTAL_UNFILLED_DURATION_COLUMN,
executionStatusColumn,
- {
- field: 'execution_summary.last_execution.date',
- name: i18n.COLUMN_LAST_COMPLETE_RUN,
- render: (value: RuleExecutionSummary['last_execution']['date'] | undefined) => {
- return (
-
- {value == null ? (
- getEmptyTagValue()
- ) : (
-
- )}
-
- );
- },
- sortable: true,
- truncateText: true,
- width: '16%',
- },
+ LAST_EXECUTION_COLUMN,
enabledColumn,
...(hasCRUDPermissions ? [actionsColumn] : []),
],
[
actionsColumn,
- docLinks.links.siem.troubleshootGaps,
enabledColumn,
executionStatusColumn,
+ gapDurationColumn,
hasCRUDPermissions,
- ruleNameColumn,
showRelatedIntegrations,
]
);