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/hooks/use_is_rule_editable.test.ts b/x-pack/plugins/observability/public/hooks/use_is_rule_editable.test.ts new file mode 100644 index 0000000000000..599fc3bfe6e83 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_is_rule_editable.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { Capabilities } from '@kbn/core-capabilities-common'; +import { Rule, RuleAction, RuleType, RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; +import { RecursiveReadonly } from '@kbn/utility-types'; + +import { useIsRuleEditable, UseIsRuleEditableProps } from './use_is_rule_editable'; +import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; + +const mockConsumer = 'mock-consumerId'; +const mockRuleTypeId = 'mock-ruleTypeId'; + +const ruleTypeRegistry = new TypeRegistry(); +ruleTypeRegistry.register({ + id: mockRuleTypeId, + requiresAppContext: false, + actions: [], +} as unknown as RuleTypeModel); + +const renderUseIsRuleEditableHook = (props: UseIsRuleEditableProps) => { + return renderHook(() => useIsRuleEditable({ ...props })); +}; + +const capabilities = { + actions: { + execute: true, + }, +} as unknown as RecursiveReadonly; + +const rule = { + consumer: mockConsumer, + actions: [], + ruleTypeId: mockRuleTypeId, +} as unknown as Rule; + +const ruleType = { + authorizedConsumers: { + [mockConsumer]: { + all: true, + }, + }, +} as unknown as RuleType; + +describe('useIsRuleEditable', () => { + it('should return false if there is no rule', () => { + const { + result: { current: isRuleEditable }, + } = renderUseIsRuleEditableHook({ + capabilities, + rule: undefined, + ruleType, + ruleTypeRegistry, + }); + + expect(isRuleEditable).toBe(false); + }); + + it('should return false if the authorized consumers object of the rule type does not contain the particular rule being passed', () => { + const { + result: { current: isRuleEditable }, + } = renderUseIsRuleEditableHook({ + capabilities, + rule, + ruleType: { + authorizedConsumers: {}, + } as unknown as RuleType, + ruleTypeRegistry, + }); + expect(isRuleEditable).toBe(false); + }); + + it('should return false if the authorized consumers object of the rule type has the id for the particular rule, but all is not set to true', () => { + const { + result: { current: isRuleEditable }, + } = renderUseIsRuleEditableHook({ + capabilities, + rule, + ruleType: { + authorizedConsumers: { + [mockConsumer]: { + all: false, + }, + }, + } as unknown as RuleType, + ruleTypeRegistry, + }); + expect(isRuleEditable).toBe(false); + }); + + it('should return false if the rule has actions to perform but the execute capability is false ', () => { + const { + result: { current: isRuleEditable }, + } = renderUseIsRuleEditableHook({ + capabilities: { + actions: { + execute: false, + }, + } as unknown as RecursiveReadonly, + rule: { + ...rule, + actions: ['123'] as unknown as RuleAction[], + }, + ruleType, + ruleTypeRegistry, + }); + expect(isRuleEditable).toBe(false); + }); + + it('should return false if the rule is not registered in the rule registry', () => { + const { + result: { current: isRuleEditable }, + } = renderUseIsRuleEditableHook({ + capabilities, + rule, + ruleType, + ruleTypeRegistry: new TypeRegistry(), + }); + expect(isRuleEditable).toBe(false); + }); + + it('it should return true if all conditions are met', () => { + const { + result: { current: isRuleEditable }, + } = renderUseIsRuleEditableHook({ + capabilities, + rule, + ruleType, + ruleTypeRegistry, + }); + expect(isRuleEditable).toBe(true); + }); +}); diff --git a/x-pack/plugins/observability/public/hooks/use_is_rule_editable.ts b/x-pack/plugins/observability/public/hooks/use_is_rule_editable.ts new file mode 100644 index 0000000000000..e5c053d742b2c --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_is_rule_editable.ts @@ -0,0 +1,50 @@ +/* + * 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 { Capabilities } from '@kbn/core-capabilities-common'; +import { Rule, RuleType, RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; +import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; +import { RecursiveReadonly } from '@kbn/utility-types'; + +export interface UseIsRuleEditableProps { + capabilities: RecursiveReadonly; + rule: Rule | undefined; + ruleType: RuleType | undefined; + ruleTypeRegistry: TypeRegistry>; +} + +export function useIsRuleEditable({ + capabilities, + rule, + ruleType, + ruleTypeRegistry, +}: UseIsRuleEditableProps): boolean { + if (!rule) { + return false; + } + + // If the authorized consumers object of the rule type does not contain the rule + // being passed, the rule is not editable + if (!ruleType?.authorizedConsumers[rule.consumer]?.all) { + return false; + } + + // If there are no capabilities to execute actions and the rule has 1 or more + // actions to perform, the rule is not editable. + if (!capabilities.actions?.execute && rule.actions.length !== 0) { + return false; + } + + try { + // If the rule has been registered in the ruleTypeRegistry and requiresAppContext + // is set to false, it means the rule is editable. + // Wrapped in try-catch as ruleTypeRegistry will throw an Error if the rule is not registered + return ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext === false; + } catch (e) { + return false; + } +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/center_justified_spinner.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/center_justified_spinner.tsx index 867d530eb4e2f..03758fc23bd14 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/center_justified_spinner.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/center_justified_spinner.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; import { EuiLoadingSpinnerSize } from '@elastic/eui/src/components/loading/loading_spinner'; interface Props { @@ -18,7 +17,9 @@ export function CenterJustifiedSpinner({ size }: Props) { return ( - + + + ); diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/delete_confirmation_modal.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/delete_confirmation_modal.tsx new file mode 100644 index 0000000000000..d954143d398a5 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/delete_confirmation_modal.tsx @@ -0,0 +1,123 @@ +/* + * 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 { EuiConfirmModal } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { HttpSetup } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../utils/kibana_react'; + +interface DeleteConfirmationPropsModal { + apiDeleteCall: ({ + ids, + http, + }: { + ids: string[]; + http: HttpSetup; + }) => Promise<{ successes: string[]; errors: string[] }>; + idToDelete: string | undefined; + title: string; + onCancel: () => void; + onDeleted: () => void; + onDeleting: () => void; + onErrors: () => void; +} + +export function DeleteConfirmationModal({ + apiDeleteCall, + idToDelete, + title, + onCancel, + onDeleted, + onDeleting, + onErrors, +}: DeleteConfirmationPropsModal) { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const [deleteModalFlyoutVisible, setDeleteModalVisibility] = useState(false); + + useEffect(() => { + setDeleteModalVisibility(Boolean(idToDelete)); + }, [idToDelete]); + + if (!deleteModalFlyoutVisible) { + return null; + } + + return ( + { + setDeleteModalVisibility(false); + onCancel(); + }} + onConfirm={async () => { + if (idToDelete) { + setDeleteModalVisibility(false); + onDeleting(); + const { successes, errors } = await apiDeleteCall({ ids: [idToDelete], http }); + + const hasSucceeded = Boolean(successes.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 }, + } + ) + ); + onErrors(); + } + + onDeleted(); + } + }} + > + {i18n.translate('xpack.observability.rules.deleteConfirmationModal.descriptionText', { + defaultMessage: "You can't recover {title} after deleting.", + values: { title }, + })} + + ); +} 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 deleted file mode 100644 index 61eec8b8c3ea8..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 { EuiConfirmModal } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; -import { HttpSetup } from '@kbn/core/public'; -import { useKibana } from '../../../utils/kibana_react'; -import { - confirmModalText, - confirmButtonText, - cancelButtonText, - deleteSuccessText, - deleteErrorText, -} from '../translations'; - -export function DeleteModalConfirmation({ - idsToDelete, - apiDeleteCall, - onDeleted, - onCancel, - onErrors, - singleTitle, - multipleTitle, - setIsLoadingState, -}: { - idsToDelete: string[]; - apiDeleteCall: ({ - ids, - http, - }: { - ids: string[]; - http: HttpSetup; - }) => Promise<{ successes: string[]; errors: string[] }>; - onDeleted: (deleted: string[]) => void; - onCancel: () => void; - onErrors: () => void; - singleTitle: string; - multipleTitle: string; - setIsLoadingState: (isLoading: boolean) => void; -}) { - const [deleteModalFlyoutVisible, setDeleteModalVisibility] = useState(false); - - useEffect(() => { - setDeleteModalVisibility(idsToDelete.length > 0); - }, [idsToDelete]); - - const { - http, - notifications: { toasts }, - } = useKibana().services; - const numIdsToDelete = idsToDelete.length; - if (!deleteModalFlyoutVisible) { - return null; - } - - return ( - { - setDeleteModalVisibility(false); - onCancel(); - }} - onConfirm={async () => { - setDeleteModalVisibility(false); - setIsLoadingState(true); - const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http }); - setIsLoadingState(false); - - const numSuccesses = successes.length; - const numErrors = errors.length; - if (numSuccesses > 0) { - toasts.addSuccess(deleteSuccessText(numSuccesses, singleTitle, multipleTitle)); - } - - if (numErrors > 0) { - toasts.addDanger(deleteErrorText(numErrors, singleTitle, multipleTitle)); - await onErrors(); - } - await onDeleted(successes); - }} - cancelButtonText={cancelButtonText} - confirmButtonText={confirmButtonText(numIdsToDelete, singleTitle, multipleTitle)} - > - {confirmModalText(numIdsToDelete, 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..29ff67bcdca7c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/header_actions.tsx @@ -0,0 +1,100 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiButton, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface HeaderActionsProps { + loading: boolean; + ruleId: string | undefined; + onDeleteRule: (ruleId: string) => void; + onEditRule: () => void; +} + +export function HeaderActions({ loading, ruleId, 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); + + if (ruleId) { + onDeleteRule(ruleId); + } + }; + + return ( + + + + {i18n.translate('xpack.observability.ruleDetails.actionsButtonLabel', { + defaultMessage: 'Actions', + })} + + } + closePopover={handleClosePopover} + isOpen={isRuleEditPopoverOpen} + > + + + + {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/index.ts b/x-pack/plugins/observability/public/pages/rule_details/components/index.ts index e6650e7f5dd07..b9f9547506c5e 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/index.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/components/index.ts @@ -5,4 +5,18 @@ * 2.0. */ -export { PageTitle } from './page_title'; +import { CenterJustifiedSpinner } from './center_justified_spinner'; +import { DeleteConfirmationModal } from './delete_confirmation_modal'; +import { HeaderActions } from './header_actions'; +import { PageTitle } from './page_title'; +import { RuleDetailTabs } from './rule_detail_tabs'; +import { RuleLoadingError } from './rule_loading_error'; + +export { + CenterJustifiedSpinner, + DeleteConfirmationModal, + HeaderActions, + PageTitle, + RuleDetailTabs, + RuleLoadingError, +}; diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx index 5fd1e38b0da6e..bb21c8d8acfac 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx @@ -7,27 +7,34 @@ import React from 'react'; import moment from 'moment'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, EuiSpacer } from '@elastic/eui'; -import { PageHeaderProps } from '../types'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '../../../utils/kibana_react'; import { LAST_UPDATED_MESSAGE, CREATED_WORD, BY_WORD, ON_WORD } from '../translations'; -import { getHealthColor } from '../config'; +import { getHealthColor } from '../helpers/utils'; -export function PageTitle({ rule }: PageHeaderProps) { - const { triggersActionsUi } = useKibana().services; +export interface PageTitleProps { + rule: Rule; +} + +export function PageTitle({ rule }: PageTitleProps) { + const { + triggersActionsUi: { getRuleTagBadge: RuleTagBadge }, + } = useKibana().services; + + const { name, executionStatus, updatedBy, createdBy, updatedAt, createdAt, tags } = rule; return ( <> - {rule.name} + {name} - - {rule.executionStatus.status.charAt(0).toUpperCase() + - rule.executionStatus.status.slice(1)} + + {executionStatus.status.charAt(0).toUpperCase() + executionStatus.status.slice(1)} @@ -35,20 +42,15 @@ export function PageTitle({ rule }: PageHeaderProps) { - {LAST_UPDATED_MESSAGE} {BY_WORD} {rule.updatedBy} {ON_WORD}  - {moment(rule.updatedAt).format('ll')}   - {CREATED_WORD} {BY_WORD} {rule.createdBy} {ON_WORD}  - {moment(rule.createdAt).format('ll')} + {LAST_UPDATED_MESSAGE} {BY_WORD} {updatedBy} {ON_WORD}  + {moment(updatedAt).format('ll')}   + {CREATED_WORD} {BY_WORD} {createdBy} {ON_WORD}  + {moment(createdAt).format('ll')} - {rule.tags.length > 0 && - triggersActionsUi.getRuleTagBadge<'tagsOutPopover'>({ - tagsOutPopover: true, - tags: rule.tags, - })} - + {tags.length ? : null} ); } 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..936d8d790499e --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/rule_detail_tabs.tsx @@ -0,0 +1,145 @@ +/* + * 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, { useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useHistory, useLocation } from 'react-router-dom'; +import { Query, BoolQuery } from '@kbn/es-query'; +import { Rule, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; +import { RuleTypeParams } from '@kbn/alerting-plugin/common'; +import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; + +import { ObservabilityAppServices } from '../../../application/types'; +import { fromQuery, toQuery } from '../../../utils/url'; +import { ObservabilityAlertSearchbarWithUrlSync } from '../../../components/shared/alert_search_bar'; +import { + EXECUTION_TAB, + ALERTS_TAB, + RULE_DETAILS_PAGE_ID, + RULE_DETAILS_ALERTS_SEARCH_BAR_ID, + URL_STORAGE_KEY, +} from '../constants'; +import { TabId } from '../types'; +import { observabilityFeatureId } from '../../../../common'; + +interface TabProps { + rule: Rule; + ruleType: RuleType | undefined; +} + +export function RuleDetailTabs({ rule, ruleType }: TabProps) { + const { + triggersActionsUi: { + alertsTableConfigurationRegistry, + getRuleEventLogList: RuleEventLogList, + getAlertsStateTable: AlertsStateTable, + }, + } = useKibana().services; + + const history = useHistory(); + const location = useLocation(); + + const ruleQuery = useRef([ + { query: `kibana.alert.rule.uuid: ${rule.id}`, language: 'kuery' }, + ] as Query[]); + + const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); + + const [tabId, setTabId] = useState( + (toQuery(location.search)?.tabId as TabId) || EXECUTION_TAB + ); + + const updateUrl = (nextQuery: { tabId: TabId }) => { + history.push({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + ...nextQuery, + }), + }); + }; + + const onTabIdChange = (newTabId: TabId) => { + setTabId(newTabId); + updateUrl({ tabId: newTabId }); + }; + + const features = ( + rule?.consumer === ALERTS_FEATURE_ID && ruleType?.producer ? ruleType.producer : rule?.consumer + ) as AlertConsumers; + + const tabs: EuiTabbedContentTab[] = [ + { + id: EXECUTION_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { + defaultMessage: 'Execution history', + }), + 'data-test-subj': 'eventLogListTab', + content: ( + + + {ruleType ? : null} + + + ), + }, + { + id: ALERTS_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { + defaultMessage: 'Alerts', + }), + 'data-test-subj': 'ruleAlertListTab', + content: ( + <> + + + + + + {esQuery ? ( + + ) : null} + + + + ), + }, + ]; + + return ( + tab.id === tabId)} + onTabClick={(tab) => { + onTabIdChange(tab.id as TabId); + }} + /> + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/rule_loading_error.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/rule_loading_error.tsx new file mode 100644 index 0000000000000..8c26a5c79b447 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/rule_loading_error.tsx @@ -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 React from 'react'; +import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function RuleLoadingError() { + return ( + + + {i18n.translate('xpack.observability.ruleDetails.errorPromptTitle', { + defaultMessage: 'Unable to load rule details', + })} + + } + body={ +

+ {i18n.translate('xpack.observability.ruleDetails.errorPromptBody', { + defaultMessage: 'There was an error loading the rule details.', + })} +

+ } + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/config.ts b/x-pack/plugins/observability/public/pages/rule_details/config.ts deleted file mode 100644 index 410c893aba7a3..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/config.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { RuleExecutionStatuses } from '@kbn/alerting-plugin/common'; -import { RuleType, Rule } from '@kbn/triggers-actions-ui-plugin/public'; - -export function getHealthColor(status: RuleExecutionStatuses) { - switch (status) { - case 'active': - return 'success'; - case 'error': - return 'danger'; - case 'ok': - return 'primary'; - case 'pending': - return 'accent'; - default: - return 'subdued'; - } -} - -type Capabilities = Record; - -export type InitialRule = Partial & - Pick; - -export function hasAllPrivilege(rule: InitialRule, ruleType?: RuleType): boolean { - return ruleType?.authorizedConsumers[rule.consumer]?.all ?? false; -} - -export const hasExecuteActionsCapability = (capabilities: Capabilities) => - capabilities?.actions?.execute; diff --git a/x-pack/plugins/observability/public/pages/rule_details/helpers/utils.ts b/x-pack/plugins/observability/public/pages/rule_details/helpers/utils.ts new file mode 100644 index 0000000000000..09776a40c8e6f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/helpers/utils.ts @@ -0,0 +1,37 @@ +/* + * 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 { + RuleExecutionStatusErrorReasons, + RuleExecutionStatuses, +} from '@kbn/alerting-plugin/common'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; + +import { ALERT_STATUS_LICENSE_ERROR, rulesStatusesTranslationsMapping } from '../translations'; + +export function getHealthColor(status: RuleExecutionStatuses) { + switch (status) { + case 'active': + return 'success'; + case 'error': + return 'danger'; + case 'ok': + return 'primary'; + case 'pending': + return 'accent'; + default: + return 'subdued'; + } +} + +export function getStatusMessage(rule: Rule): string { + return rule?.executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License + ? ALERT_STATUS_LICENSE_ERROR + : rule + ? rulesStatusesTranslationsMapping[rule.executionStatus.status] + : ''; +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.test.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.test.tsx new file mode 100644 index 0000000000000..6a302285479d4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/index.test.tsx @@ -0,0 +1,284 @@ +/* + * 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 from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { CoreStart } from '@kbn/core/public'; +import { ConfigSchema, ObservabilityPublicPluginsStart } from '../../plugin'; +import { RuleDetailsPage } from '.'; +import { kibanaStartMock } from '../../utils/kibana_react.mock'; +import * as pluginContext from '../../hooks/use_plugin_context'; +import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; +import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; +import { AppMountParameters } from '@kbn/core/public'; +import { ALERTS_FEATURE_ID } from '@kbn/alerting-plugin/common'; + +interface SetupProps { + ruleLoading: boolean; + ruleError: boolean; + ruleLoaded: boolean; + ruleEditable: boolean; +} + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); +const mockRuleId = 'mock-rule-id'; + +jest.mock('@kbn/kibana-react-plugin/public', () => ({ + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(() => ({ ruleId: mockRuleId })), + useLocation: jest.fn(() => ({ search: '123' })), +})); + +jest.mock('../../hooks/use_breadcrumbs', () => ({ + useBreadcrumbs: jest.fn(), +})); + +jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({ + useLoadRuleTypes: jest.fn(), +})); + +jest.mock('../../hooks/use_fetch_rule', () => ({ + useFetchRule: jest.fn(), +})); + +jest.mock('../../hooks/use_is_rule_editable', () => ({ + useIsRuleEditable: jest.fn(), +})); + +jest.mock('../../hooks/use_get_filtered_rule_types', () => ({ + useGetFilteredRuleTypes: jest.fn(() => ['123']), +})); + +jest.mock('../../hooks/use_get_rule_type_definition_from_rule_type', () => ({ + useGetRuleTypeDefinitionFromRuleType: jest.fn(), +})); + +jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ + appMountParameters: {} as AppMountParameters, + config: {} as unknown as ConfigSchema, + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: KibanaPageTemplate, + kibanaFeatures: [], + core: {} as CoreStart, + plugins: {} as ObservabilityPublicPluginsStart, +})); + +const { useFetchRule } = jest.requireMock('../../hooks/use_fetch_rule'); +const { useGetRuleTypeDefinitionFromRuleType } = jest.requireMock( + '../../hooks/use_get_rule_type_definition_from_rule_type' +); +const { useIsRuleEditable } = jest.requireMock('../../hooks/use_is_rule_editable'); + +describe('RuleDetailPage', () => { + async function setup({ ruleLoading, ruleError, ruleLoaded, ruleEditable }: SetupProps) { + const mockRuleType = { + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + actionVariables: { + context: [], + state: [], + }, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { + all: true, + }, + }, + defaultActionGroupId: 'default', + enabledInLicense: true, + executionStatus: { + status: 'active', + }, + id: 'test_rule_type', + minimumLicenseRequired: 'basic', + name: 'some rule type', + producer: ALERTS_FEATURE_ID, + recoveryActionGroup: { + id: 'recovered', + name: 'Recovered', + }, + ruleTaskTimeout: '1m', + tags: [], + }; + + const mockRule = { + ruleTypeId: 'metrics.alert.threshold', + createdBy: 'admin', + updatedBy: 'admin', + createdAt: '2022-11-21T13:21:10.555Z', + updatedAt: '2022-11-21T13:21:10.555Z', + apiKeyOwner: 'admin', + notifyWhen: 'onActionGroupChange', + muteAll: false, + mutedInstanceIds: [], + snoozeSchedule: [], + executionStatus: { + lastExecutionDate: '2022-11-22T11:35:37.219Z', + lastDuration: 4190, + status: 'active', + }, + actions: [], + scheduledTaskId: '5c0177b0-699f-11ed-8e77-89558ff3f691', + isSnoozedUntil: null, + lastRun: { + outcomeMsg: null, + alertsCount: { + new: 0, + ignored: 0, + recovered: 0, + active: 1, + }, + warning: null, + outcome: 'succeeded', + }, + nextRun: '2022-11-22T11:36:36.983Z', + id: '5c0177b0-699f-11ed-8e77-89558ff3f691', + consumer: 'alerts', + tags: [], + name: 'foo', + enabled: true, + throttle: null, + schedule: { + interval: '1m', + }, + params: { + criteria: [ + { + aggType: 'avg', + comparator: '>', + threshold: [2], + timeSize: 1, + timeUnit: 'm', + metric: '_score', + }, + ], + sourceId: 'default', + alertOnNoData: true, + alertOnGroupDisappear: true, + }, + monitoring: { + run: { + last_run: { + timestamp: '2022-11-22T11:35:37.219Z', + metrics: { + total_search_duration_ms: null, + total_indexing_duration_ms: null, + total_alerts_detected: null, + total_alerts_created: null, + gap_duration_s: null, + duration: 4190, + }, + }, + history: [], + calculated_metrics: { + success_ratio: 0.9936708860759493, + p99: 5367.439999999997, + p50: 4182, + p95: 5019, + }, + }, + }, + }; + + useFetchRule.mockReturnValue({ + rule: ruleError || ruleLoading || !ruleLoaded ? undefined : mockRule, + isRuleLoading: ruleLoading, + errorRule: ruleError ? 'error loading rule' : undefined, + reloadRule: jest.fn(), + }); + + useGetRuleTypeDefinitionFromRuleType.mockReturnValue(mockRuleType); + + useIsRuleEditable.mockReturnValue(ruleEditable); + + return render(); + } + + describe('when a rule is loading', () => { + it('should render a loader', async () => { + const wrapper = await setup({ + ruleLoading: true, + ruleError: false, + ruleLoaded: false, + ruleEditable: false, + }); + + expect(wrapper.getByTestId('centerJustifiedSpinner')).toBeInTheDocument(); + }); + }); + + const ruleLoadedUnsuccessfully = { + ruleLoading: false, + ruleError: true, + ruleLoaded: false, + ruleEditable: false, + }; + describe('when loading a rule has errored', () => { + it('should render an error state', async () => { + const wrapper = await setup(ruleLoadedUnsuccessfully); + + expect(wrapper.getByTestId('rule-loading-error')).toBeInTheDocument(); + }); + }); + + const ruleLoadedSuccesfully = { + ruleLoading: false, + ruleError: false, + ruleLoaded: true, + ruleEditable: true, + }; + + describe('when a rule has been loaded', () => { + it('should render a page template', async () => { + const wrapper = await setup(ruleLoadedSuccesfully); + expect(wrapper.getByTestId('ruleDetails')).toBeInTheDocument(); + }); + + it('should render header actions when the rule is editable', async () => { + const wrapperRuleNonEditable = await setup({ ...ruleLoadedSuccesfully, ruleEditable: false }); + expect(wrapperRuleNonEditable.queryByTestId('actions')).toBeNull(); + + const wrapperRuleEditable = await setup(ruleLoadedSuccesfully); + expect(wrapperRuleEditable.queryByTestId('actions')).toBeInTheDocument(); + }); + + it('should render a DeleteConfirmationModal when deleting a rule', async () => { + const wrapper = await setup(ruleLoadedSuccesfully); + + fireEvent.click(wrapper.getByTestId('actions')); + fireEvent.click(wrapper.getByTestId('deleteRuleButton')); + + expect(wrapper.getByTestId('deleteIdsConfirmation')).toBeInTheDocument(); + }); + + it('should render RuleDetailTabs', async () => { + const wrapper = await setup(ruleLoadedSuccesfully); + expect(wrapper.getByTestId('rule-detail-tabs')).toBeInTheDocument(); + }); + + it('should render a RuleStatusPanel', async () => { + const wrapper = await setup(ruleLoadedSuccesfully); + expect(wrapper.getByTestId('rule-status-panel')).toBeInTheDocument(); + }); + + it('should render an EditAlertFlyout', async () => { + const wrapper = await setup(ruleLoadedSuccesfully); + + fireEvent.click(wrapper.getByTestId('actions')); + fireEvent.click(wrapper.getByTestId('editRuleButton')); + + expect(wrapper.getByTestId('edit-alert-flyout')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 12732939ad66c..05118e22959e4 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -5,162 +5,67 @@ * 2.0. */ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { useHistory, useParams, useLocation } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { - EuiText, - EuiSpacer, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiPopover, - EuiTabbedContent, - EuiEmptyPrompt, - EuiSuperSelectOption, - EuiButton, - EuiFlyoutSize, - EuiTabbedContentTab, -} from '@elastic/eui'; - -import { - deleteRules, - useLoadRuleTypes, - RuleType, - getNotifyWhenOptions, - RuleEventLogListProps, -} from '@kbn/triggers-actions-ui-plugin/public'; -// TODO: use a Delete modal from triggersActionUI when it's sharable -import { ALERTS_FEATURE_ID, RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common'; -import { Query, BoolQuery } from '@kbn/es-query'; -import { AlertConsumers } from '@kbn/rule-data-utils'; -import { RuleDefinitionProps } from '@kbn/triggers-actions-ui-plugin/public'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { deleteRules } from '@kbn/triggers-actions-ui-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { fromQuery, toQuery } from '../../utils/url'; -import { ObservabilityAlertSearchbarWithUrlSync } from '../../components/shared/alert_search_bar'; -import { DeleteModalConfirmation } from './components/delete_modal_confirmation'; -import { CenterJustifiedSpinner } from './components/center_justified_spinner'; import { - EXECUTION_TAB, - ALERTS_TAB, - RULE_DETAILS_PAGE_ID, - RULE_DETAILS_ALERTS_SEARCH_BAR_ID, - URL_STORAGE_KEY, -} from './constants'; -import { RuleDetailsPathParams, TabId } from './types'; + CenterJustifiedSpinner, + DeleteConfirmationModal, + PageTitle, + HeaderActions, + RuleLoadingError, + RuleDetailTabs, +} from './components'; +import { RuleDetailsPathParams } from './types'; +import { ObservabilityAppServices } from '../../application/types'; import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; -import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchRule } from '../../hooks/use_fetch_rule'; -import { RULES_BREADCRUMB_TEXT } from '../rules/translations'; -import { PageTitle } from './components'; -import { getHealthColor } from './config'; -import { hasExecuteActionsCapability, hasAllPrivilege } from './config'; +import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; +import { useGetRuleTypeDefinitionFromRuleType } from '../../hooks/use_get_rule_type_definition_from_rule_type'; +import { useIsRuleEditable } from '../../hooks/use_is_rule_editable'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { getHealthColor, getStatusMessage } from './helpers/utils'; import { paths } from '../../config/paths'; -import { observabilityFeatureId } from '../../../common'; -import { ALERT_STATUS_LICENSE_ERROR, rulesStatusesTranslationsMapping } from './translations'; -import { ObservabilityAppServices } from '../../application/types'; +import { RULES_BREADCRUMB_TEXT } from '../rules/translations'; export function RuleDetailsPage() { const { + application: { capabilities, navigateToUrl }, http, + notifications: { toasts }, triggersActionsUi: { - alertsTableConfigurationRegistry, + actionTypeRegistry, ruleTypeRegistry, - getEditAlertFlyout, - getRuleEventLogList, - getAlertsStateTable: AlertsStateTable, - getRuleAlertsSummary, - getRuleStatusPanel, - getRuleDefinition, + getEditAlertFlyout: EditAlertFlyout, + getRuleAlertsSummary: RuleAlertsSummary, + getRuleStatusPanel: RuleStatusPanel, + getRuleDefinition: RuleDefinition, }, - application: { capabilities, navigateToUrl }, - notifications: { toasts }, } = useKibana().services; - const { ruleId } = useParams(); - const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); - const history = useHistory(); - const location = useLocation(); - - const filteredRuleTypes = useMemo( - () => observabilityRuleTypeRegistry.list(), - [observabilityRuleTypeRegistry] - ); + const { ObservabilityPageTemplate } = usePluginContext(); - const { isRuleLoading, rule, errorRule, reloadRule } = useFetchRule({ ruleId, http }); - const { ruleTypes } = useLoadRuleTypes({ - filteredRuleTypes, - }); - const [tabId, setTabId] = useState(() => { - const urlTabId = (toQuery(location.search)?.tabId as TabId) || EXECUTION_TAB; - return [EXECUTION_TAB, ALERTS_TAB].includes(urlTabId) ? urlTabId : EXECUTION_TAB; - }); - const [features, setFeatures] = 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 ruleQuery = useRef([ - { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, - ] as Query[]); - - 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) => { - setTabId(newTabId); - updateUrl({ tabId: newTabId }); - }; - - const NOTIFY_WHEN_OPTIONS = useRef>>([]); - useEffect(() => { - const loadNotifyWhenOption = async () => { - NOTIFY_WHEN_OPTIONS.current = await getNotifyWhenOptions(); - }; - loadNotifyWhenOption(); - }, []); - - const togglePopover = () => - setIsRuleEditPopoverOpen((pervIsRuleEditPopoverOpen) => !pervIsRuleEditPopoverOpen); + const { ruleId } = useParams(); - const handleClosePopover = () => setIsRuleEditPopoverOpen(false); + const { rule, isRuleLoading, errorRule, reloadRule } = useFetchRule({ http, ruleId }); + const filteredRuleTypes = useGetFilteredRuleTypes(); - const handleRemoveRule = useCallback(() => { - setIsRuleEditPopoverOpen(false); - if (rule) setRuleToDelete([rule.id]); - }, [rule]); + const ruleTypeDefinition = useGetRuleTypeDefinitionFromRuleType({ ruleTypeId: rule?.ruleTypeId }); - const handleEditRule = useCallback(() => { - setIsRuleEditPopoverOpen(false); - setEditFlyoutVisible(true); - }, []); + const isRuleEditable = useIsRuleEditable({ + capabilities, + rule, + ruleType: ruleTypeDefinition, + ruleTypeRegistry, + }); - useEffect(() => { - if (ruleTypes.length && rule) { - const matchedRuleType = ruleTypes.find((type) => type.id === rule.ruleTypeId); - setRuleType(matchedRuleType); + const [editRuleFlyoutVisible, setEditRuleFlyoutVisible] = useState(false); - if (rule.consumer === ALERTS_FEATURE_ID && matchedRuleType && matchedRuleType.producer) { - setFeatures(matchedRuleType.producer); - } else setFeatures(rule.consumer); - } - }, [rule, ruleTypes]); + const [ruleToDelete, setRuleToDelete] = useState(undefined); + const [isRuleDeleting, setIsRuleDeleting] = useState(false); useBreadcrumbs([ { @@ -174,229 +79,107 @@ export function RuleDetailsPage() { text: RULES_BREADCRUMB_TEXT, }, { - text: rule && rule.name, + text: rule?.name, }, ]); - const canExecuteActions = hasExecuteActionsCapability(capabilities); + const handleEditRule = () => { + setEditRuleFlyoutVisible(true); + }; - 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 handleDeleteRule = () => { + if (rule) { + setRuleToDelete(rule.id); + } + setEditRuleFlyoutVisible(false); + }; - 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: ( - - - {getRuleEventLogList<'default'>({ - ruleId: rule?.id, - ruleType, - } as RuleEventLogListProps)} - - - ), - }, - { - id: ALERTS_TAB, - name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { - defaultMessage: 'Alerts', - }), - 'data-test-subj': 'ruleAlertListTab', - content: ( - <> - - - - - - {esQuery && features && ( - - )} + return ( + <> + {isRuleLoading ? : null} + + {errorRule && !rule ? ( + + ) : rule ? ( + , + bottomBorder: false, + rightSideItems: isRuleEditable + ? [ + , + ] + : undefined, + }} + > + + + - - - ), - }, - ]; - if (isPageLoading || isRuleLoading) return ; - if (!rule || errorRule) - return ( - - - {i18n.translate('xpack.observability.ruleDetails.errorPromptTitle', { - defaultMessage: 'Unable to load rule details', - })} - - } - body={ -

- {i18n.translate('xpack.observability.ruleDetails.errorPromptBody', { - defaultMessage: 'There was an error loading the rule details.', - })} -

- } - /> -
- ); + - const isLicenseError = - rule.executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License; + + + - const statusMessage = isLicenseError - ? ALERT_STATUS_LICENSE_ERROR - : rulesStatusesTranslationsMapping[rule.executionStatus.status]; + - return ( - , - bottomBorder: false, - rightSideItems: hasEditButton - ? [ - - - - {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', - })} - - - - - - , - ] - : [], - }} - > - - - {getRuleStatusPanel({ - rule, - isEditable: hasEditButton, - requestRefresh: reloadRule, - healthColor: getHealthColor(rule.executionStatus.status), - statusMessage, - })} - - - - {getRuleAlertsSummary({ - rule, - filteredRuleTypes, - })} - - - {getRuleDefinition({ rule, onEditRule: () => reloadRule() } as RuleDefinitionProps)} - + +
- - tab.id === tabId)} - onTabClick={(tab) => { - onTabIdChange(tab.id as TabId); - }} - /> - {editFlyoutVisible && - getEditAlertFlyout({ - initialRule: rule, - onClose: () => { - setEditFlyoutVisible(false); - }, - onSave: reloadRule, - })} - { - setRuleToDelete([]); - navigateToUrl(http.basePath.prepend(paths.observability.rules)); - }} - onErrors={() => { - setRuleToDelete([]); - navigateToUrl(http.basePath.prepend(paths.observability.rules)); - }} - onCancel={() => setRuleToDelete([])} - apiDeleteCall={deleteRules} - idsToDelete={ruleToDelete} - singleTitle={rule.name} - multipleTitle={rule.name} - setIsLoadingState={() => setIsPageLoading(true)} - /> - {errorRule && toasts.addDanger({ title: errorRule })} - + + + + + {editRuleFlyoutVisible && ( + { + setEditRuleFlyoutVisible(false); + }} + onSave={reloadRule} + /> + )} + + setIsRuleDeleting(true)} + onDeleted={() => { + setRuleToDelete(undefined); + navigateToUrl(http.basePath.prepend(paths.observability.rules)); + }} + onErrors={() => { + setRuleToDelete(undefined); + navigateToUrl(http.basePath.prepend(paths.observability.rules)); + }} + onCancel={() => setRuleToDelete(undefined)} + /> + + ) : null} + ); } diff --git a/x-pack/plugins/observability/public/pages/rule_details/translations.ts b/x-pack/plugins/observability/public/pages/rule_details/translations.ts index e30178e15cf47..9efd05a549ec4 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/translations.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/translations.ts @@ -41,52 +41,6 @@ export const CREATED_WORD = i18n.translate('xpack.observability.ruleDetails.crea defaultMessage: 'Created', }); -export 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 }, - }); - -export 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 }, - }); - -export const cancelButtonText = i18n.translate( - 'xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } -); - -export 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 }, - }); - -export 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 }, - }); export const ALERT_STATUS_LICENSE_ERROR = i18n.translate( 'xpack.observability.ruleDetails.ruleStatusLicenseError', { diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts index 07407ca217444..db1a467e33a5f 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/types.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/types.ts @@ -14,9 +14,6 @@ export type TabId = typeof ALERTS_TAB | typeof EXECUTION_TAB; export interface RuleDetailsPathParams { ruleId: string; } -export interface PageHeaderProps { - rule: Rule; -} export interface FetchRuleProps { ruleId?: string; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 0fb5e576cfb27..c621b3012b8f5 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -23264,10 +23264,6 @@ "xpack.observability.overview.exploratoryView.noDataAvailable": "Aucune donnée {dataType} disponible.", "xpack.observability.ruleDetails.executionLogError": "Impossible de charger le log d'exécution de la règle. Raison : {message}", "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}}.", - "xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText": "Impossible de supprimer {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}", - "xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText": "Suppression de {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}} effectuée", "xpack.observability.textDefinitionField.placeholder.search": "Rechercher dans {label}", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.urlFilter.wildcard": "Utiliser le caractère générique *{wildcard}*", @@ -23631,7 +23627,6 @@ "xpack.observability.ruleDetails.ruleStatusWarning": "Avertissement", "xpack.observability.ruleDetails.tagsTitle": "Balises", "xpack.observability.rules.addRuleButtonLabel": "Créer une règle", - "xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel": "Annuler", "xpack.observability.rules.docsLinkText": "Documentation", "xpack.observability.rules.loadError": "Impossible de charger les règles", "xpack.observability.rules.rulesTable.changeStatusAriaLabel": "Modifier le statut", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9f34753a45325..2fc1ed951cffd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23243,10 +23243,6 @@ "xpack.observability.overview.exploratoryView.noDataAvailable": "{dataType}データがありません。", "xpack.observability.ruleDetails.executionLogError": "ルール実行ログを読み込めません。理由:{message}", "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 {deleted {multipleTitle}}}を回復できません。", - "xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText": "{numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}を削除できませんでした", - "xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText": "{numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}を削除しました", "xpack.observability.textDefinitionField.placeholder.search": "{label}を検索", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.urlFilter.wildcard": "ワイルドカード*{wildcard}*を使用", @@ -23610,7 +23606,6 @@ "xpack.observability.ruleDetails.ruleStatusWarning": "警告", "xpack.observability.ruleDetails.tagsTitle": "タグ", "xpack.observability.rules.addRuleButtonLabel": "ルールを作成", - "xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.observability.rules.docsLinkText": "ドキュメント", "xpack.observability.rules.loadError": "ルールを読み込めません", "xpack.observability.rules.rulesTable.changeStatusAriaLabel": "ステータスの変更", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7fb9a4c15bb9f..59a334a15514b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23275,9 +23275,6 @@ "xpack.observability.overview.exploratoryView.noDataAvailable": "没有可用的 {dataType} 数据。", "xpack.observability.ruleDetails.executionLogError": "无法加载规则执行日志。原因:{message}", "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}}}。", - "xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText": "无法删除 {numErrors, number} 个{numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}", "xpack.observability.textDefinitionField.placeholder.search": "搜索 {label}", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.urlFilter.wildcard": "使用通配符 *{wildcard}*", @@ -23641,7 +23638,6 @@ "xpack.observability.ruleDetails.ruleStatusWarning": "警告", "xpack.observability.ruleDetails.tagsTitle": "标签", "xpack.observability.rules.addRuleButtonLabel": "创建规则", - "xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.observability.rules.docsLinkText": "文档", "xpack.observability.rules.loadError": "无法加载规则", "xpack.observability.rules.rulesTable.changeStatusAriaLabel": "更改状态",