diff --git a/x-pack/plugins/observability/public/hooks/use_get_rule_type_definition_from_rule_type.ts b/x-pack/plugins/observability/public/hooks/use_get_rule_type_definition_from_rule_type.ts new file mode 100644 index 0000000000000..e4eb8abc26f63 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_get_rule_type_definition_from_rule_type.ts @@ -0,0 +1,24 @@ +/* + * 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 { useLoadRuleTypes } from '@kbn/triggers-actions-ui-plugin/public'; +import { useGetFilteredRuleTypes } from './use_get_filtered_rule_types'; + +interface UseGetRuleTypeDefinitionFromRuleTypeProps { + ruleTypeId: string | undefined; +} + +export function useGetRuleTypeDefinitionFromRuleType({ + ruleTypeId, +}: UseGetRuleTypeDefinitionFromRuleTypeProps) { + const filteredRuleTypes = useGetFilteredRuleTypes(); + + const { ruleTypes } = useLoadRuleTypes({ + filteredRuleTypes, + }); + + return ruleTypes.find(({ id }) => id === ruleTypeId); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx index 2a07286cf0792..f37cebf9b6821 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx @@ -6,115 +6,98 @@ */ import { EuiConfirmModal } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; +import { bulkDeleteRules } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; -import type { - BulkOperationAttributes, - BulkOperationResponse, -} from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../../utils/kibana_react'; -export function DeleteModalConfirmation({ - idsToDelete, - apiDeleteCall, - onDeleted, - onCancel, - onErrors, - singleTitle, - multipleTitle, - setIsLoadingState, -}: { - idsToDelete: string[]; - apiDeleteCall: ({ ids, http }: BulkOperationAttributes) => Promise; - onDeleted: () => void; +interface DeleteConfirmationPropsModal { + ruleIdToDelete: string | undefined; + title: string; onCancel: () => void; - onErrors: () => void; - singleTitle: string; - multipleTitle: string; - setIsLoadingState: (isLoading: boolean) => void; -}) { - const [deleteModalFlyoutVisible, setDeleteModalVisibility] = useState(false); - - useEffect(() => { - setDeleteModalVisibility(idsToDelete.length > 0); - }, [idsToDelete]); + onDeleted: () => void; + onDeleting: () => void; +} +export function DeleteConfirmationModal({ + ruleIdToDelete, + title, + onCancel, + onDeleted, + onDeleting, +}: DeleteConfirmationPropsModal) { const { http, notifications: { toasts }, } = useKibana().services; - const numIdsToDelete = idsToDelete.length; - if (!deleteModalFlyoutVisible) { - return null; - } - return ( + const [isVisible, setIsVisible] = useState(Boolean(ruleIdToDelete)); + + return isVisible ? ( { - setDeleteModalVisibility(false); - onCancel(); - }} + title={i18n.translate('xpack.observability.rules.deleteConfirmationModal.descriptionText', { + defaultMessage: "You can't recover {title} after deleting.", + values: { title }, + })} + cancelButtonText={i18n.translate( + 'xpack.observability.rules.deleteConfirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.observability.rules.deleteConfirmationModal.deleteButtonLabel', + { + defaultMessage: 'Delete {title}', + values: { title }, + } + )} + onCancel={onCancel} onConfirm={async () => { - setDeleteModalVisibility(false); - setIsLoadingState(true); - const { total, errors } = await apiDeleteCall({ ids: idsToDelete, http }); - setIsLoadingState(false); + if (ruleIdToDelete) { + setIsVisible(false); - const numErrors = errors.length; - const numSuccesses = total - numErrors; + onDeleting(); - if (numSuccesses > 0) { - toasts.addSuccess(deleteSuccessText(numSuccesses, singleTitle, multipleTitle)); - } + const { errors, rules } = await bulkDeleteRules({ ids: [ruleIdToDelete], http }); + + const hasSucceeded = Boolean(rules.length); + const hasErrored = Boolean(errors.length); + + if (hasSucceeded) { + toasts.addSuccess( + i18n.translate( + 'xpack.observability.rules.deleteConfirmationModal.successNotification.descriptionText', + { + defaultMessage: 'Deleted {title}', + values: { title }, + } + ) + ); + } + + if (hasErrored) { + toasts.addDanger( + i18n.translate( + 'xpack.observability.rules.deleteConfirmationModal.errorNotification.descriptionText', + { + defaultMessage: 'Failed to delete {title}', + values: { title }, + } + ) + ); + } - if (numErrors > 0) { - toasts.addDanger(deleteErrorText(numErrors, singleTitle, multipleTitle)); - await onErrors(); + onDeleted(); } - await onDeleted(); }} - cancelButtonText={cancelButtonText} - confirmButtonText={confirmButtonText(numIdsToDelete, singleTitle, multipleTitle)} > - {confirmModalText(numIdsToDelete, singleTitle, multipleTitle)} + {i18n.translate('xpack.observability.rules.deleteConfirmationModal.descriptionText', { + defaultMessage: "You can't recover {title} after deleting.", + values: { title }, + })} - ); + ) : null; } - -const confirmModalText = (numIdsToDelete: number, singleTitle: string, multipleTitle: string) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText', { - defaultMessage: - "You can't recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.", - values: { numIdsToDelete, singleTitle, multipleTitle }, - }); - -const confirmButtonText = (numIdsToDelete: number, singleTitle: string, multipleTitle: string) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsConfirmModal.deleteButtonLabel', { - defaultMessage: - 'Delete {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ', - values: { numIdsToDelete, singleTitle, multipleTitle }, - }); - -const cancelButtonText = i18n.translate( - 'xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } -); - -const deleteSuccessText = (numSuccesses: number, singleTitle: string, multipleTitle: string) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText', { - defaultMessage: - 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}', - values: { numSuccesses, singleTitle, multipleTitle }, - }); - -const deleteErrorText = (numErrors: number, singleTitle: string, multipleTitle: string) => - i18n.translate('xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText', { - defaultMessage: - 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}', - values: { numErrors, singleTitle, multipleTitle }, - }); diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/header_actions.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/header_actions.tsx new file mode 100644 index 0000000000000..77df2a3b63456 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/header_actions.tsx @@ -0,0 +1,97 @@ +/* + * 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 React, { useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface HeaderActionsProps { + loading: boolean; + + onDeleteRule: () => void; + onEditRule: () => void; +} + +export function HeaderActions({ loading, onDeleteRule, onEditRule }: HeaderActionsProps) { + const [isRuleEditPopoverOpen, setIsRuleEditPopoverOpen] = useState(false); + + const togglePopover = () => setIsRuleEditPopoverOpen(!isRuleEditPopoverOpen); + + const handleClosePopover = () => setIsRuleEditPopoverOpen(false); + + const handleEditRule = () => { + setIsRuleEditPopoverOpen(false); + + onEditRule(); + }; + + const handleRemoveRule = () => { + setIsRuleEditPopoverOpen(false); + + onDeleteRule(); + }; + + return ( + + + + {i18n.translate('xpack.observability.ruleDetails.actionsButtonLabel', { + defaultMessage: 'Actions', + })} + + } + > + + + + {i18n.translate('xpack.observability.ruleDetails.editRule', { + defaultMessage: 'Edit rule', + })} + + + + + {i18n.translate('xpack.observability.ruleDetails.deleteRule', { + defaultMessage: 'Delete rule', + })} + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/rule_detail_tabs.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/rule_detail_tabs.tsx new file mode 100644 index 0000000000000..2a07c3beb83f5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/rule_detail_tabs.tsx @@ -0,0 +1,158 @@ +/* + * 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 React, { useEffect, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { Rule, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; +import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; +import type { BoolQuery, Query } from '@kbn/es-query'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; +import { useKibana } from '../../../utils/kibana_react'; +import { ObservabilityAlertSearchbarWithUrlSync } from '../../../components/shared/alert_search_bar'; +import { + ALERTS_TAB, + EXECUTION_TAB, + RULE_DETAILS_ALERTS_SEARCH_BAR_ID, + RULE_DETAILS_PAGE_ID, + SEARCH_BAR_URL_STORAGE_KEY, + TabId, +} from '../rule_details'; +import { getDefaultAlertSummaryTimeRange } from '../../../utils/alert_summary_widget'; +import { fromQuery, toQuery } from '../../../utils/url'; +import { observabilityFeatureId } from '../../../../common'; + +interface RuleDetailTabsProps { + activeTab: TabId; + featureIds: AlertConsumers[] | undefined; + rule: Rule; + ruleId: string; + ruleType: RuleType | undefined; + onChangeTab: (tabId: TabId) => void; +} + +export function RuleDetailTabs({ + activeTab, + featureIds, + rule, + ruleId, + ruleType, + onChangeTab, +}: RuleDetailTabsProps) { + const { + triggersActionsUi: { + alertsTableConfigurationRegistry, + getRuleEventLogList: RuleEventLogList, + getAlertsStateTable: AlertsStateTable, + }, + } = useKibana().services; + const history = useHistory(); + + const ruleQuery = useRef([ + { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, + ]); + + const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); + const [alertSummaryWidgetTimeRange, setAlertSummaryWidgetTimeRange] = useState( + getDefaultAlertSummaryTimeRange + ); + + useEffect(() => { + setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange()); + }, [esQuery]); + + const updateUrl = (nextQuery: { tabId: TabId }) => { + const newTabId = nextQuery.tabId; + const nextSearch = + newTabId === ALERTS_TAB + ? { + ...toQuery(location.search), + ...nextQuery, + } + : { tabId: EXECUTION_TAB }; + + history.replace({ + ...location, + search: fromQuery(nextSearch), + }); + }; + + const onTabIdChange = (newTabId: TabId) => { + onChangeTab(newTabId); + updateUrl({ tabId: newTabId }); + }; + + const tabs: EuiTabbedContentTab[] = [ + { + id: EXECUTION_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { + defaultMessage: 'Execution history', + }), + 'data-test-subj': 'eventLogListTab', + content: ( + + + {rule && ruleType ? : null} + + + ), + }, + { + id: ALERTS_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { + defaultMessage: 'Alerts', + }), + 'data-test-subj': 'ruleAlertListTab', + content: ( + <> + + + + + + {esQuery && featureIds && ( + + )} + + + + ), + }, + ]; + + return ( + tab.id === activeTab)} + onTabClick={(tab) => { + onTabIdChange(tab.id as TabId); + }} + /> + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/helpers/can_edit_rule.ts b/x-pack/plugins/observability/public/pages/rule_details/helpers/can_edit_rule.ts new file mode 100644 index 0000000000000..052d8ee0aedd5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/helpers/can_edit_rule.ts @@ -0,0 +1,42 @@ +/* + * 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 { RuleTypeParams } from '@kbn/alerting-plugin/common'; +import type { Rule, RuleType, RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; +import type { Capabilities } from '@kbn/core-capabilities-common'; +import type { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; +import type { RecursiveReadonly } from '@kbn/utility-types'; +import { hasAllPrivilege } from './has_all_privilege'; +import { hasExecuteActionsCapability } from './has_execute_actions_capability'; + +interface CanEditRuleProps { + rule: Rule | undefined; + ruleTypeDefinition: RuleType | undefined; + capabilities: RecursiveReadonly; + ruleTypeRegistry: TypeRegistry>; +} + +export function canEditRule({ + rule, + ruleTypeDefinition, + capabilities, + ruleTypeRegistry, +}: CanEditRuleProps): boolean { + const canExecuteActions = hasExecuteActionsCapability(capabilities); + + return ( + // can the user save the rule + rule && + hasAllPrivilege(rule, ruleTypeDefinition) && + // if the rule has actions, can the user save the rule's action params + (canExecuteActions || (!canExecuteActions && rule.actions.length === 0)) && + // is this rule type editable from within Rules Management + (ruleTypeRegistry.has(rule.ruleTypeId) + ? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext + : false) + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx index e4241a3755875..43da5e130772c 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -5,57 +5,35 @@ * 2.0. */ -import { estypes } from '@elastic/elasticsearch'; -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { useHistory, useParams, useLocation } from 'react-router-dom'; +import React, { useState, useRef } from 'react'; +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { - EuiText, - EuiSpacer, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiTabbedContent, - EuiSuperSelectOption, - EuiButton, - EuiFlyoutSize, - EuiTabbedContentTab, -} from '@elastic/eui'; - -import { - bulkDeleteRules, - useLoadRuleTypes, - RuleType, - getNotifyWhenOptions, -} from '@kbn/triggers-actions-ui-plugin/public'; -// TODO: use a Delete modal from triggersActionUI when it's sharable +import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { estypes } from '@elastic/elasticsearch'; import { ALERTS_FEATURE_ID, RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common'; -import { Query, BoolQuery } from '@kbn/es-query'; -import { ValidFeatureId } from '@kbn/rule-data-utils'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { fromQuery, toQuery } from '../../utils/url'; -import { - defaultTimeRange, - getDefaultAlertSummaryTimeRange, -} from '../../utils/alert_summary_widget'; -import { ObservabilityAlertSearchbarWithUrlSync } from '../../components/shared/alert_search_bar'; -import { DeleteModalConfirmation } from './components/delete_modal_confirmation'; -import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; - +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { useKibana } from '../../utils/kibana_react'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchRule } from '../../hooks/use_fetch_rule'; +import { DeleteConfirmationModal } from './components/delete_modal_confirmation'; +import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; import { PageTitle } from './components/page_title'; import { NoRuleFoundPanel } from './components/no_rule_found_panel'; +import { RuleDetailTabs } from './components/rule_detail_tabs'; +import { HeaderActions } from './components/header_actions'; import { getHealthColor } from './helpers/get_health_color'; -import { hasExecuteActionsCapability } from './helpers/has_execute_actions_capability'; -import { hasAllPrivilege } from './helpers/has_all_privilege'; +import { + defaultTimeRange, + getDefaultAlertSummaryTimeRange, +} from '../../utils/alert_summary_widget'; +import { toQuery } from '../../utils/url'; import { paths } from '../../config/paths'; import { ALERT_STATUS_ALL } from '../../../common/constants'; -import { observabilityFeatureId, ruleDetailsLocatorID } from '../../../common'; +import { ruleDetailsLocatorID } from '../../../common'; import type { AlertStatus } from '../../../common/typings'; -import type { ObservabilityAppServices } from '../../application/types'; +import { useGetRuleTypeDefinitionFromRuleType } from '../../hooks/use_get_rule_type_definition_from_rule_type'; +import { canEditRule } from './helpers/can_edit_rule'; export const EXECUTION_TAB = 'execution'; export const ALERTS_TAB = 'alerts'; @@ -81,66 +59,69 @@ export function RuleDetailsPage() { }, triggersActionsUi: { actionTypeRegistry, - alertsTableConfigurationRegistry, ruleTypeRegistry, - getAlertsStateTable: AlertsStateTable, getAlertSummaryWidget: AlertSummaryWidget, getEditRuleFlyout: EditRuleFlyout, getRuleDefinition: RuleDefinition, - getRuleEventLogList: RuleEventLogList, getRuleStatusPanel: RuleStatusPanel, }, - } = useKibana().services; - const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); + } = useKibana().services; + const { ObservabilityPageTemplate } = usePluginContext(); const { ruleId } = useParams(); - const history = useHistory(); - const location = useLocation(); - const filteredRuleTypes = useMemo( - () => observabilityRuleTypeRegistry.list(), - [observabilityRuleTypeRegistry] - ); const { isRuleLoading, rule, errorRule, reloadRule } = useFetchRule({ ruleId, http }); - const { ruleTypes } = useLoadRuleTypes({ - filteredRuleTypes, - }); + + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + href: http.basePath.prepend(paths.observability.alerts), + }, + { + href: http.basePath.prepend(paths.observability.rules), + text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { + defaultMessage: 'Rules', + }), + }, + { + text: rule && rule.name, + }, + ]); + + const ruleTypeDefinition = useGetRuleTypeDefinitionFromRuleType({ ruleTypeId: rule?.ruleTypeId }); const chartProps = { theme: charts.theme.useChartsTheme(), baseTheme: charts.theme.useChartsBaseTheme(), }; - const [tabId, setTabId] = useState(() => { + const [activeTab, setActiveTab] = useState(() => { const urlTabId = (toQuery(location.search)?.tabId as TabId) || EXECUTION_TAB; return [EXECUTION_TAB, ALERTS_TAB].includes(urlTabId) ? urlTabId : EXECUTION_TAB; }); - const [featureIds, setFeatureIds] = useState(); - const [ruleType, setRuleType] = useState>(); - const [ruleToDelete, setRuleToDelete] = useState([]); - const [isPageLoading, setIsPageLoading] = useState(false); - const [editFlyoutVisible, setEditFlyoutVisible] = useState(false); - const [isRuleEditPopoverOpen, setIsRuleEditPopoverOpen] = useState(false); - const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); + + const [editRuleFlyoutVisible, setEditRuleFlyoutVisible] = useState(false); + + const [ruleToDelete, setRuleToDelete] = useState(undefined); + const [isRuleDeleting, setIsRuleDeleting] = useState(false); + const [alertSummaryWidgetTimeRange, setAlertSummaryWidgetTimeRange] = useState( getDefaultAlertSummaryTimeRange ); - const ruleQuery = useRef([ - { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, - ]); + const alertSummaryWidgetFilter = useRef({ term: { 'kibana.alert.rule.uuid': ruleId, }, }); - const tabsRef = useRef(null); - useEffect(() => { - setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange()); - }, [esQuery]); + const tabsRef = useRef(null); const onAlertSummaryWidgetClick = async (status: AlertStatus = ALERT_STATUS_ALL) => { setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange()); + await locators.get(ruleDetailsLocatorID)?.navigate( { rangeFrom: defaultTimeRange.from, @@ -153,233 +134,86 @@ export function RuleDetailsPage() { replace: true, } ); - setTabId(ALERTS_TAB); + + setActiveTab(ALERTS_TAB); + tabsRef.current?.scrollIntoView({ behavior: 'smooth' }); }; - const updateUrl = (nextQuery: { tabId: TabId }) => { - const newTabId = nextQuery.tabId; - const nextSearch = - newTabId === ALERTS_TAB - ? { - ...toQuery(location.search), - ...nextQuery, - } - : { tabId: EXECUTION_TAB }; - - history.replace({ - ...location, - search: fromQuery(nextSearch), - }); + const handleEditRule = () => { + setEditRuleFlyoutVisible(true); }; - const onTabIdChange = (newTabId: TabId) => { - setTabId(newTabId); - updateUrl({ tabId: newTabId }); + const handleIsDeletingRule = () => { + setIsRuleDeleting(true); }; - const NOTIFY_WHEN_OPTIONS = useRef>>([]); - useEffect(() => { - const loadNotifyWhenOption = async () => { - NOTIFY_WHEN_OPTIONS.current = await getNotifyWhenOptions(); - }; - loadNotifyWhenOption(); - }, []); - - const togglePopover = () => - setIsRuleEditPopoverOpen((pervIsRuleEditPopoverOpen) => !pervIsRuleEditPopoverOpen); - - const handleClosePopover = () => setIsRuleEditPopoverOpen(false); - - const handleRemoveRule = useCallback(() => { - setIsRuleEditPopoverOpen(false); - if (rule) setRuleToDelete([rule.id]); - }, [rule]); - - const handleEditRule = useCallback(() => { - setIsRuleEditPopoverOpen(false); - setEditFlyoutVisible(true); - }, []); - - useEffect(() => { - if (ruleTypes.length && rule) { - const matchedRuleType = ruleTypes.find((type) => type.id === rule.ruleTypeId); - setRuleType(matchedRuleType); + const handleIsRuleDeleted = () => { + setRuleToDelete(undefined); + setIsRuleDeleting(false); + navigateToUrl(http.basePath.prepend(paths.observability.rules)); + }; - if (rule.consumer === ALERTS_FEATURE_ID && matchedRuleType && matchedRuleType.producer) { - setFeatureIds([matchedRuleType.producer] as ValidFeatureId[]); - } else setFeatureIds([rule.consumer] as ValidFeatureId[]); - } - }, [rule, ruleTypes]); + const handleDeleteRule = () => { + setRuleToDelete(rule?.id); + setEditRuleFlyoutVisible(false); + }; - useBreadcrumbs([ - { - text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { - defaultMessage: 'Alerts', - }), - href: http.basePath.prepend(paths.observability.alerts), - }, - { - href: http.basePath.prepend(paths.observability.rules), - text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { - defaultMessage: 'Rules', - }), - }, - { - text: rule && rule.name, - }, - ]); + const handleChangeTab = (newTab: TabId) => { + setActiveTab(newTab); + }; - const canExecuteActions = hasExecuteActionsCapability(capabilities); + const featureIds = ( + !rule || !ruleTypeDefinition + ? undefined + : rule.consumer === ALERTS_FEATURE_ID && ruleTypeDefinition.producer + ? [ruleTypeDefinition.producer] + : [rule.consumer] + ) as AlertConsumers[]; - const canSaveRule = - rule && - hasAllPrivilege(rule, ruleType) && - // if the rule has actions, can the user save the rule's action params - (canExecuteActions || (!canExecuteActions && rule.actions.length === 0)); + const isRuleEditable = canEditRule({ rule, ruleTypeDefinition, capabilities, ruleTypeRegistry }); - const hasEditButton = - // can the user save the rule - canSaveRule && - // is this rule type editable from within Rules Management - (ruleTypeRegistry.has(rule.ruleTypeId) - ? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext - : false); + if (errorRule) { + toasts.addDanger({ title: errorRule }); + } - const tabs: EuiTabbedContentTab[] = [ - { - id: EXECUTION_TAB, - name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { - defaultMessage: 'Execution history', - }), - 'data-test-subj': 'eventLogListTab', - content: ( - - - {rule && ruleType ? : null} - - - ), - }, - { - id: ALERTS_TAB, - name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { - defaultMessage: 'Alerts', - }), - 'data-test-subj': 'ruleAlertListTab', - content: ( - <> - - - - - - {esQuery && featureIds && ( - - )} - - - - ), - }, - ]; + if (isRuleLoading || isRuleDeleting) return ; - if (isPageLoading || isRuleLoading) return ; if (!rule || errorRule) return ; - const isLicenseError = - rule.executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; - - const statusMessage = isLicenseError - ? i18n.translate('xpack.observability.ruleDetails.ruleStatusLicenseError', { - defaultMessage: 'License Error', - }) - : rulesStatusesTranslationsMapping[rule.executionStatus.status]; - return ( , bottomBorder: false, - rightSideItems: hasEditButton + rightSideItems: isRuleEditable ? [ - - - - {i18n.translate('xpack.observability.ruleDetails.actionsButtonLabel', { - defaultMessage: 'Actions', - })} - - } - > - - - - {i18n.translate('xpack.observability.ruleDetails.editRule', { - defaultMessage: 'Edit rule', - })} - - - - - {i18n.translate('xpack.observability.ruleDetails.deleteRule', { - defaultMessage: 'Delete rule', - })} - - - - - - , + , ] - : [], + : undefined, }} > @@ -403,46 +237,41 @@ export function RuleDetailsPage() {
- tab.id === tabId)} - onTabClick={(tab) => { - onTabIdChange(tab.id as TabId); - }} - /> + {rule && ruleTypeDefinition ? ( + + ) : null} - {editFlyoutVisible && ( + {editRuleFlyoutVisible ? ( { - setEditFlyoutVisible(false); + setEditRuleFlyoutVisible(false); }} onSave={reloadRule} /> - )} - { - setRuleToDelete([]); - navigateToUrl(http.basePath.prepend(paths.observability.rules)); - }} - onErrors={() => { - setRuleToDelete([]); - navigateToUrl(http.basePath.prepend(paths.observability.rules)); - }} - onCancel={() => setRuleToDelete([])} - apiDeleteCall={bulkDeleteRules} - idsToDelete={ruleToDelete} - singleTitle={rule.name} - multipleTitle={rule.name} - setIsLoadingState={() => setIsPageLoading(true)} - /> - {errorRule && toasts.addDanger({ title: errorRule })} + ) : null} + + {ruleToDelete ? ( + setRuleToDelete(undefined)} + onDeleting={handleIsDeletingRule} + onDeleted={handleIsRuleDeleted} + /> + ) : null} ); } -const rulesStatusesTranslationsMapping = { +const rulesStatusesTranslationsMapping: Record = { ok: i18n.translate('xpack.observability.ruleDetails.ruleStatusOk', { defaultMessage: 'Ok', }), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5bf4303e4ea58..d5e92e8dc22f4 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25502,11 +25502,6 @@ "xpack.observability.inspector.stats.queryTimeValue": "{queryTime} ms", "xpack.observability.overview.exploratoryView.missingReportDefinition": "{reportDefinition} manquante", "xpack.observability.overview.exploratoryView.noDataAvailable": "Aucune donnée {dataType} disponible.", -<<<<<<< HEAD -======= - "xpack.observability.profilingElasticsearchPluginDescription": "{technicalPreviewLabel} Si les traces d'appel doivent être chargées à l'aide du plug-in de profilage Elasticsearch.", - "xpack.observability.ruleDetails.executionLogError": "Impossible de charger le log d'exécution de la règle. Raison : {message}", ->>>>>>> 0985ef0fb6a3d235ac30cf2298601a0e845c5b10 "xpack.observability.ruleDetails.ruleLoadError": "Impossible de charger la règle. Raison : {message}", "xpack.observability.rules.deleteSelectedIdsConfirmModal.deleteButtonLabel": "Supprimer {numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}} ", "xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText": "Vous ne pouvez pas récupérer {numIdsToDelete, plural, one {un {singleTitle} supprimé} other {des {multipleTitle} supprimés}}.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2e5f95ea12575..232887a7c5996 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25486,13 +25486,8 @@ "xpack.observability.filters.searchResults": "{total}件の検索結果", "xpack.observability.inspector.stats.queryTimeValue": "{queryTime}ms", "xpack.observability.overview.exploratoryView.missingReportDefinition": "{reportDefinition}が見つかりません", -<<<<<<< HEAD - "xpack.observability.overview.exploratoryView.noDataAvailable": "{dataType}データがありません。", -======= "xpack.observability.overview.exploratoryView.noDataAvailable": "利用可能な{dataType}データがありません。", "xpack.observability.profilingElasticsearchPluginDescription": "{technicalPreviewLabel} Elasticsearchプロファイラープラグインを使用してスタックトレースを読み込むかどうか。", - "xpack.observability.ruleDetails.executionLogError": "ルール実行ログを読み込めません。理由:{message}", ->>>>>>> 0985ef0fb6a3d235ac30cf2298601a0e845c5b10 "xpack.observability.ruleDetails.ruleLoadError": "ルールを読み込めません。理由:{message}", "xpack.observability.slo.alerting.burnRate.reason": "過去{longWindowDuration}のバーンレートは{longWindowBurnRate}で、過去{shortWindowDuration}のバーンレートは{shortWindowBurnRate}です。両期間とも{burnRateThreshold}を超えたらアラート", "xpack.observability.slo.rules.burnRate.errors.invalidThresholdValue": "バーンレートしきい値は1以上{maxBurnRate}以下でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc8c99cab3d01..83ff096102769 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25499,11 +25499,7 @@ "xpack.observability.inspector.stats.queryTimeValue": "{queryTime}ms", "xpack.observability.overview.exploratoryView.missingReportDefinition": "缺少 {reportDefinition}", "xpack.observability.overview.exploratoryView.noDataAvailable": "没有可用的 {dataType} 数据。", -<<<<<<< HEAD -======= "xpack.observability.profilingElasticsearchPluginDescription": "{technicalPreviewLabel} 是否使用 Elasticsearch 分析器插件加载堆栈跟踪。", - "xpack.observability.ruleDetails.executionLogError": "无法加载规则执行日志。原因:{message}", ->>>>>>> 0985ef0fb6a3d235ac30cf2298601a0e845c5b10 "xpack.observability.ruleDetails.ruleLoadError": "无法加载规则。原因:{message}", "xpack.observability.rules.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other {# 个{multipleTitle}}}", "xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText": "无法恢复{numIdsToDelete, plural, one {删除的{singleTitle}} other {删除的{multipleTitle}}}。",