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, ] );