From ef661d191fa6933849fba92d0f71203af40a2e79 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Fri, 30 Jun 2023 14:16:20 +0200 Subject: [PATCH] [Tech Debt] Refactor Rule Details page (#160229) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/hooks/use_delete_rules.ts | 63 +++ .../public/hooks/use_fetch_rule.ts | 106 ++-- .../public/hooks/use_fetch_rule_summary.ts | 48 -- .../public/hooks/use_fetch_rule_types.ts | 89 ++++ .../public/locators/rule_details.test.ts | 17 +- .../public/locators/rule_details.ts | 14 +- .../pages/alert_details/alert_details.tsx | 1 - .../components/header_actions.tsx | 12 +- .../components/delete_confirmation_modal.tsx | 74 +++ .../components/delete_modal_confirmation.tsx | 91 ---- .../components/header_actions.tsx | 99 ++++ .../pages/rule_details/components/index.ts | 8 - .../components/no_rule_found_panel.tsx | 35 ++ .../rule_details/components/page_title.tsx | 48 +- .../components/rule_details_tabs.tsx | 138 ++++++ .../public/pages/rule_details/config.ts | 35 -- .../public/pages/rule_details/constants.ts | 8 +- .../rule_details/helpers/get_health_color.ts | 23 + .../rule_details/helpers/is_rule_editable.ts | 39 ++ .../public/pages/rule_details/index.tsx | 454 ------------------ .../pages/rule_details/rule_details.tsx | 315 ++++++++++++ .../public/pages/rule_details/translations.ts | 143 ------ .../public/pages/rule_details/types.ts | 56 --- .../observability/public/routes/routes.tsx | 2 +- x-pack/plugins/observability/tsconfig.json | 8 +- .../translations/translations/fr-FR.json | 7 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 28 files changed, 1018 insertions(+), 921 deletions(-) create mode 100644 x-pack/plugins/observability/public/hooks/use_delete_rules.ts delete mode 100644 x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts create mode 100644 x-pack/plugins/observability/public/hooks/use_fetch_rule_types.ts create mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/delete_confirmation_modal.tsx delete mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx create mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/header_actions.tsx delete mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/index.ts create mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/no_rule_found_panel.tsx create mode 100644 x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx delete mode 100644 x-pack/plugins/observability/public/pages/rule_details/config.ts create mode 100644 x-pack/plugins/observability/public/pages/rule_details/helpers/get_health_color.ts create mode 100644 x-pack/plugins/observability/public/pages/rule_details/helpers/is_rule_editable.ts delete mode 100644 x-pack/plugins/observability/public/pages/rule_details/index.tsx create mode 100644 x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx delete mode 100644 x-pack/plugins/observability/public/pages/rule_details/translations.ts delete mode 100644 x-pack/plugins/observability/public/pages/rule_details/types.ts diff --git a/x-pack/plugins/observability/public/hooks/use_delete_rules.ts b/x-pack/plugins/observability/public/hooks/use_delete_rules.ts new file mode 100644 index 0000000000000..eaee7b64ec71b --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_delete_rules.ts @@ -0,0 +1,63 @@ +/* + * 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 { useMutation } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import type { KueryNode } from '@kbn/es-query'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common'; +import { useKibana } from '../utils/kibana_react'; + +export function useDeleteRules() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const deleteRules = useMutation< + string, + string, + { ids: string[]; filter?: KueryNode | null | undefined } + >( + ['deleteRules'], + ({ ids, filter }) => { + try { + const body = JSON.stringify({ + ...(ids?.length ? { ids } : {}), + ...(filter ? { filter: JSON.stringify(filter) } : {}), + }); + return http.patch(`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_delete`, { body }); + } catch (e) { + throw new Error(`Unable to parse bulk delete params: ${e}`); + } + }, + { + onError: (_err, rule, context) => { + toasts.addDanger( + i18n.translate( + 'xpack.observability.rules.deleteConfirmationModal.errorNotification.descriptionText', + { + defaultMessage: 'Failed to delete rule', + } + ) + ); + }, + + onSuccess: () => { + toasts.addSuccess( + i18n.translate( + 'xpack.observability.rules.deleteConfirmationModal.successNotification.descriptionText', + { + defaultMessage: 'Deleted rule', + } + ) + ); + }, + } + ); + + return deleteRules; +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts index 65773198b6843..83dc3e9737de2 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts @@ -5,44 +5,80 @@ * 2.0. */ -import { useEffect, useState, useCallback } from 'react'; -import { loadRule } from '@kbn/triggers-actions-ui-plugin/public'; -import { FetchRuleProps, FetchRule } from '../pages/rule_details/types'; -import { RULE_LOAD_ERROR } from '../pages/rule_details/translations'; +import { + QueryObserverResult, + RefetchOptions, + RefetchQueryFilters, + useQuery, +} from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common'; +import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import type { AsApiContract } from '@kbn/actions-plugin/common'; +import { transformRule } from '@kbn/triggers-actions-ui-plugin/public'; +import { useKibana } from '../utils/kibana_react'; -export function useFetchRule({ ruleId, http }: FetchRuleProps) { - const [ruleSummary, setRuleSummary] = useState({ - isRuleLoading: true, - rule: undefined, - errorRule: undefined, - }); +export interface UseFetchRuleResponse { + isInitialLoading: boolean; + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + rule: Rule | undefined; + refetch: ( + options?: (RefetchOptions & RefetchQueryFilters) | undefined + ) => Promise>; +} + +export function useFetchRule({ ruleId }: { ruleId?: string }): UseFetchRuleResponse { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( + { + queryKey: ['fetchRule', ruleId], + queryFn: async ({ signal }) => { + try { + if (!ruleId) return; - const fetchRuleSummary = useCallback(async () => { - try { - if (!ruleId) return; - const rule = await loadRule({ - http, - ruleId, - }); + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}`, + { + signal, + } + ); - setRuleSummary((oldState: FetchRule) => ({ - ...oldState, - isRuleLoading: false, - rule, - })); - } catch (error) { - setRuleSummary((oldState: FetchRule) => ({ - ...oldState, - isRuleLoading: false, - errorRule: RULE_LOAD_ERROR( - error instanceof Error ? error.message : typeof error === 'string' ? error : '' - ), - })); + return transformRule(res); + } catch (error) { + throw error; + } + }, + keepPreviousData: true, + enabled: Boolean(ruleId), + refetchOnWindowFocus: false, + onError: (error: Error) => { + toasts.addError(error, { + title: i18n.translate('xpack.observability.ruleDetails.ruleLoadError', { + defaultMessage: 'Unable to load rule. Reason: {message}', + values: { + message: + error instanceof Error ? error.message : typeof error === 'string' ? error : '', + }, + }), + }); + }, } - }, [ruleId, http]); - useEffect(() => { - fetchRuleSummary(); - }, [fetchRuleSummary]); + ); - return { ...ruleSummary, reloadRule: fetchRuleSummary }; + return { + rule: data, + isLoading, + isInitialLoading, + isRefetching, + isSuccess, + isError, + refetch, + }; } diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts deleted file mode 100644 index 7e7c71e503329..0000000000000 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rule_summary.ts +++ /dev/null @@ -1,48 +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 { useEffect, useState, useCallback } from 'react'; -import { loadRuleSummary } from '@kbn/triggers-actions-ui-plugin/public'; -import { FetchRuleSummaryProps, FetchRuleSummary } from '../pages/rule_details/types'; -import { RULE_LOAD_ERROR } from '../pages/rule_details/translations'; - -export function useFetchRuleSummary({ ruleId, http }: FetchRuleSummaryProps) { - const [ruleSummary, setRuleSummary] = useState({ - isLoadingRuleSummary: true, - ruleSummary: undefined, - errorRuleSummary: undefined, - }); - - const fetchRuleSummary = useCallback(async () => { - setRuleSummary((oldState: FetchRuleSummary) => ({ ...oldState, isLoading: true })); - - try { - const response = await loadRuleSummary({ - http, - ruleId, - }); - setRuleSummary((oldState: FetchRuleSummary) => ({ - ...oldState, - isLoading: false, - ruleSummary: response, - })); - } catch (error) { - setRuleSummary((oldState: FetchRuleSummary) => ({ - ...oldState, - isLoading: false, - errorRuleSummary: RULE_LOAD_ERROR( - error instanceof Error ? error.message : typeof error === 'string' ? error : '' - ), - })); - } - }, [ruleId, http]); - useEffect(() => { - fetchRuleSummary(); - }, [fetchRuleSummary]); - - return { ...ruleSummary, reloadRuleSummary: fetchRuleSummary }; -} diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule_types.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule_types.ts new file mode 100644 index 0000000000000..079aa32300cc6 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rule_types.ts @@ -0,0 +1,89 @@ +/* + * 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 { + QueryObserverResult, + RefetchOptions, + RefetchQueryFilters, + useQuery, +} from '@tanstack/react-query'; +import { camelCase, mapKeys } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common'; +import type { RuleType } from '@kbn/triggers-actions-ui-plugin/public'; +import type { AsApiContract } from '@kbn/actions-plugin/common'; +import { useKibana } from '../utils/kibana_react'; + +export interface UseFetchRuleTypesResponse { + isInitialLoading: boolean; + isLoading: boolean; + isRefetching: boolean; + isSuccess: boolean; + isError: boolean; + ruleTypes: RuleType[] | undefined; + refetch: ( + options?: (RefetchOptions & RefetchQueryFilters) | undefined + ) => Promise>; +} + +export function useFetchRuleTypes({ + filterByRuleTypeIds, +}: { + filterByRuleTypeIds?: string[] | undefined; +}): UseFetchRuleTypesResponse { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery( + { + queryKey: ['fetchRuleTypes', filterByRuleTypeIds], + queryFn: async ({ signal }) => { + try { + const res = await http.get>>>( + `${BASE_ALERTING_API_PATH}/rule_types`, + { signal } + ); + + const response = res.map((item) => { + return mapKeys(item, (_, k) => camelCase(k)); + }) as unknown as Array>; + + return filterByRuleTypeIds && filterByRuleTypeIds.length > 0 + ? response.filter((item) => filterByRuleTypeIds.includes(item.id)) + : response; + } catch (error) { + throw error; + } + }, + keepPreviousData: true, + refetchOnWindowFocus: false, + onError: (error: Error) => { + toasts.addError(error, { + title: i18n.translate('xpack.observability.ruleDetails.ruleLoadError', { + defaultMessage: 'Unable to load rule. Reason: {message}', + values: { + message: + error instanceof Error ? error.message : typeof error === 'string' ? error : '', + }, + }), + }); + }, + } + ); + + return { + ruleTypes: data, + isLoading, + isInitialLoading, + isRefetching, + isSuccess, + isError, + refetch, + }; +} diff --git a/x-pack/plugins/observability/public/locators/rule_details.test.ts b/x-pack/plugins/observability/public/locators/rule_details.test.ts index faa28f5140ba7..27d9cef8f1d12 100644 --- a/x-pack/plugins/observability/public/locators/rule_details.test.ts +++ b/x-pack/plugins/observability/public/locators/rule_details.test.ts @@ -6,7 +6,10 @@ */ import { ACTIVE_ALERTS } from '../components/alert_search_bar/constants'; -import { EXECUTION_TAB, ALERTS_TAB } from '../pages/rule_details/constants'; +import { + RULE_DETAILS_EXECUTION_TAB, + RULE_DETAILS_ALERTS_TAB, +} from '../pages/rule_details/constants'; import { getRuleDetailsPath, RuleDetailsLocatorDefinition } from './rule_details'; import { RULES_PATH } from '../routes/paths'; @@ -21,12 +24,18 @@ describe('RuleDetailsLocator', () => { }); it('should return correct url when tabId is execution', async () => { - const location = await locator.getLocation({ ruleId: mockedRuleId, tabId: EXECUTION_TAB }); + const location = await locator.getLocation({ + ruleId: mockedRuleId, + tabId: RULE_DETAILS_EXECUTION_TAB, + }); expect(location.path).toEqual(`${RULES_PATH}/${mockedRuleId}?tabId=execution`); }); it('should return correct url when tabId is alerts without extra search params', async () => { - const location = await locator.getLocation({ ruleId: mockedRuleId, tabId: ALERTS_TAB }); + const location = await locator.getLocation({ + ruleId: mockedRuleId, + tabId: RULE_DETAILS_ALERTS_TAB, + }); expect(location.path).toEqual( `${RULES_PATH}/${mockedRuleId}?tabId=alerts&searchBarParams=(kuery:'',rangeFrom:now-15m,rangeTo:now,status:all)` ); @@ -35,7 +44,7 @@ describe('RuleDetailsLocator', () => { it('should return correct url when tabId is alerts with search params', async () => { const location = await locator.getLocation({ ruleId: mockedRuleId, - tabId: ALERTS_TAB, + tabId: RULE_DETAILS_ALERTS_TAB, rangeFrom: 'mockedRangeTo', rangeTo: 'mockedRangeFrom', kuery: 'mockedKuery', diff --git a/x-pack/plugins/observability/public/locators/rule_details.ts b/x-pack/plugins/observability/public/locators/rule_details.ts index 3688fd3535236..867eb97a45b5a 100644 --- a/x-pack/plugins/observability/public/locators/rule_details.ts +++ b/x-pack/plugins/observability/public/locators/rule_details.ts @@ -12,11 +12,11 @@ import { ruleDetailsLocatorID } from '../../common'; import { RULES_PATH } from '../routes/paths'; import { ALL_ALERTS } from '../components/alert_search_bar/constants'; import { - ALERTS_TAB, - EXECUTION_TAB, - SEARCH_BAR_URL_STORAGE_KEY, + RULE_DETAILS_ALERTS_TAB, + RULE_DETAILS_EXECUTION_TAB, + RULE_DETAILS_SEARCH_BAR_URL_STORAGE_KEY, } from '../pages/rule_details/constants'; -import type { TabId } from '../pages/rule_details/types'; +import type { TabId } from '../pages/rule_details/rule_details'; import type { AlertStatus } from '../../common/typings'; export interface RuleDetailsLocatorParams extends SerializableRecord { @@ -52,15 +52,15 @@ export class RuleDetailsLocatorDefinition implements LocatorDefinition(); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx index 5935df7a221c5..d603793528a8b 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/header_actions.tsx @@ -23,15 +23,13 @@ export interface HeaderActionsProps { export function HeaderActions({ alert }: HeaderActionsProps) { const { - http, cases: { hooks: { useCasesAddToExistingCaseModal }, }, triggersActionsUi: { getEditRuleFlyout: EditRuleFlyout, getRuleSnoozeModal: RuleSnoozeModal }, } = useKibana().services; - const { rule, reloadRule } = useFetchRule({ - http, + const { rule, refetch } = useFetchRule({ ruleId: alert?.fields[ALERT_RULE_UUID] || '', }); @@ -142,7 +140,9 @@ export function HeaderActions({ alert }: HeaderActionsProps) { onClose={() => { setRuleConditionsFlyoutOpen(false); }} - onSave={reloadRule} + onSave={async () => { + refetch(); + }} /> ) : null} @@ -150,7 +150,9 @@ export function HeaderActions({ alert }: HeaderActionsProps) { setSnoozeModalOpen(false)} - onRuleChanged={reloadRule} + onRuleChanged={async () => { + refetch(); + }} onLoading={noop} /> ) : null} 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..59b46cfba08db --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/delete_confirmation_modal.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useDeleteRules } from '../../../hooks/use_delete_rules'; + +interface DeleteConfirmationPropsModal { + ruleIdToDelete: string | undefined; + title: string; + onCancel: () => void; + onDeleted: () => void; + onDeleting: () => void; +} + +export function DeleteConfirmationModal({ + ruleIdToDelete, + title, + onCancel, + onDeleted, + onDeleting, +}: DeleteConfirmationPropsModal) { + const [isVisible, setIsVisible] = useState(Boolean(ruleIdToDelete)); + + const { mutateAsync: deleteRules } = useDeleteRules(); + + const handleConfirm = async () => { + if (ruleIdToDelete) { + setIsVisible(false); + + onDeleting(); + + await deleteRules({ ids: [ruleIdToDelete] }); + + onDeleted(); + } + }; + + return isVisible ? ( + + {i18n.translate('xpack.observability.rules.deleteConfirmationModal.descriptionText', { + defaultMessage: "You can't recover {title} after deleting.", + values: { title }, + })} + + ) : null; +} 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 0738362402566..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/components/delete_modal_confirmation.tsx +++ /dev/null @@ -1,91 +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 type { - BulkOperationAttributes, - BulkOperationResponse, -} from '@kbn/triggers-actions-ui-plugin/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 }: BulkOperationAttributes) => Promise; - onDeleted: () => 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 { total, errors } = await apiDeleteCall({ ids: idsToDelete, http }); - setIsLoadingState(false); - - const numErrors = errors.length; - const numSuccesses = total - numErrors; - - if (numSuccesses > 0) { - toasts.addSuccess(deleteSuccessText(numSuccesses, singleTitle, multipleTitle)); - } - - if (numErrors > 0) { - toasts.addDanger(deleteErrorText(numErrors, singleTitle, multipleTitle)); - await onErrors(); - } - await onDeleted(); - }} - 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..9fa0a7631a2ea --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/header_actions.tsx @@ -0,0 +1,99 @@ +/* + * 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 { + isLoading: boolean; + isRuleEditable: boolean; + onDeleteRule: () => void; + onEditRule: () => void; +} + +export function HeaderActions({ + isLoading, + isRuleEditable, + onDeleteRule, + onEditRule, +}: HeaderActionsProps) { + const [isRuleEditPopoverOpen, setIsRuleEditPopoverOpen] = useState(false); + + const togglePopover = () => setIsRuleEditPopoverOpen(!isRuleEditPopoverOpen); + + const handleEditRule = () => { + setIsRuleEditPopoverOpen(false); + onEditRule(); + }; + + const handleRemoveRule = () => { + setIsRuleEditPopoverOpen(false); + onDeleteRule(); + }; + + return 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', + })} + + + + + + + ) : null; +} 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 deleted file mode 100644 index e6650e7f5dd07..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/components/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { PageTitle } from './page_title'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/no_rule_found_panel.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/no_rule_found_panel.tsx new file mode 100644 index 0000000000000..8b450b4e14daa --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/no_rule_found_panel.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 { EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function NoRuleFoundPanel() { + 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/components/page_title.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx index 5fd1e38b0da6e..f951ffdff44ba 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 @@ -6,14 +6,20 @@ */ import React from 'react'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge, EuiSpacer } from '@elastic/eui'; -import { PageHeaderProps } from '../types'; +import type { 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/get_health_color'; -export function PageTitle({ rule }: PageHeaderProps) { - const { triggersActionsUi } = useKibana().services; +interface PageTitleProps { + rule: Rule; +} + +export function PageTitle({ rule }: PageTitleProps) { + const { + triggersActionsUi: { getRuleTagBadge: RuleTagBadge }, + } = useKibana().services; return ( <> @@ -35,20 +41,38 @@ export function PageTitle({ rule }: PageHeaderProps) { - {LAST_UPDATED_MESSAGE} {BY_WORD} {rule.updatedBy} {ON_WORD}  + + {i18n.translate('xpack.observability.ruleDetails.lastUpdatedMessage', { + defaultMessage: 'Last updated', + })} + +   + {BY_WORD} {rule.updatedBy} {ON_WORD}  {moment(rule.updatedAt).format('ll')}   - {CREATED_WORD} {BY_WORD} {rule.createdBy} {ON_WORD}  + + {i18n.translate('xpack.observability.ruleDetails.createdWord', { + defaultMessage: 'Created', + })} + +   + {BY_WORD} {rule.createdBy} {ON_WORD}  {moment(rule.createdAt).format('ll')} - {rule.tags.length > 0 && - triggersActionsUi.getRuleTagBadge<'tagsOutPopover'>({ - tagsOutPopover: true, - tags: rule.tags, - })} + + {rule.tags.length > 0 && } + ); } + +const BY_WORD = i18n.translate('xpack.observability.ruleDetails.byWord', { + defaultMessage: 'by', +}); + +const ON_WORD = i18n.translate('xpack.observability.ruleDetails.onWord', { + defaultMessage: 'on', +}); diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx new file mode 100644 index 0000000000000..eab8bc8c0d139 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/components/rule_details_tabs.tsx @@ -0,0 +1,138 @@ +/* + * 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 } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { RuleTypeParams } from '@kbn/alerting-plugin/common'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; +import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import type { Query, BoolQuery } from '@kbn/es-query'; +import { useKibana } from '../../../utils/kibana_react'; +import { ObservabilityAlertSearchbarWithUrlSync } from '../../../components/alert_search_bar/alert_search_bar_with_url_sync'; +import { observabilityFeatureId } from '../../../../common'; +import { + RULE_DETAILS_ALERTS_TAB, + RULE_DETAILS_EXECUTION_TAB, + RULE_DETAILS_ALERTS_SEARCH_BAR_ID, + RULE_DETAILS_PAGE_ID, + RULE_DETAILS_SEARCH_BAR_URL_STORAGE_KEY, +} from '../constants'; +import type { TabId } from '../rule_details'; + +interface Props { + activeTabId: TabId; + esQuery: + | { + bool: BoolQuery; + } + | undefined; + featureIds: AlertConsumers[] | undefined; + rule: Rule; + ruleId: string; + ruleType: any; + onEsQueryChange: (query: { bool: BoolQuery }) => void; + onSetTabId: (tabId: TabId) => void; +} + +export function RuleDetailsTabs({ + activeTabId, + esQuery, + featureIds, + rule, + ruleId, + ruleType, + onSetTabId, + onEsQueryChange, +}: Props) { + const { + triggersActionsUi: { + alertsTableConfigurationRegistry, + getAlertsStateTable: AlertsStateTable, + getRuleEventLogList: RuleEventLogList, + }, + } = useKibana().services; + + const ruleQuery = useRef([ + { query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' }, + ]); + + const tabs: EuiTabbedContentTab[] = [ + { + id: RULE_DETAILS_EXECUTION_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.eventLogTabText', { + defaultMessage: 'Execution history', + }), + 'data-test-subj': 'eventLogListTab', + content: ( + + + {rule && ruleType ? : null} + + + ), + }, + { + id: RULE_DETAILS_ALERTS_TAB, + name: i18n.translate('xpack.observability.ruleDetails.rule.alertsTabText', { + defaultMessage: 'Alerts', + }), + 'data-test-subj': 'ruleAlertListTab', + content: ( + <> + + + + + + + + {esQuery && featureIds && ( + + )} + + + + ), + }, + ]; + + const handleTabIdChange = (newTabId: TabId) => { + onSetTabId(newTabId); + }; + + return ( + tab.id === activeTabId)} + onTabClick={(tab) => { + handleTabIdChange(tab.id as TabId); + }} + /> + ); +} 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/constants.ts b/x-pack/plugins/observability/public/pages/rule_details/constants.ts index ffca250a9f15f..77d47651e7d8d 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/constants.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/constants.ts @@ -5,9 +5,9 @@ * 2.0. */ -export const EXECUTION_TAB = 'execution'; -export const ALERTS_TAB = 'alerts'; -export const SEARCH_BAR_URL_STORAGE_KEY = 'searchBarParams'; -export const EVENT_ERROR_LOG_TAB = 'rule_error_log_list'; +export const RULE_DETAILS_EXECUTION_TAB = 'execution'; +export const RULE_DETAILS_ALERTS_TAB = 'alerts'; +export const RULE_DETAILS_TAB_URL_STORAGE_KEY = 'tabId'; +export const RULE_DETAILS_SEARCH_BAR_URL_STORAGE_KEY = 'searchBarParams'; export const RULE_DETAILS_PAGE_ID = 'rule-details-alerts-o11y'; export const RULE_DETAILS_ALERTS_SEARCH_BAR_ID = 'rule-details-alerts-search-bar-o11y'; diff --git a/x-pack/plugins/observability/public/pages/rule_details/helpers/get_health_color.ts b/x-pack/plugins/observability/public/pages/rule_details/helpers/get_health_color.ts new file mode 100644 index 0000000000000..52aab86039a3c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/helpers/get_health_color.ts @@ -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 type { RuleExecutionStatuses } from '@kbn/alerting-plugin/common'; + +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'; + } +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/helpers/is_rule_editable.ts b/x-pack/plugins/observability/public/pages/rule_details/helpers/is_rule_editable.ts new file mode 100644 index 0000000000000..39fc111e18e78 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/helpers/is_rule_editable.ts @@ -0,0 +1,39 @@ +/* + * 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 { Capabilities } from '@kbn/core-capabilities-common'; +import type { RuleType, RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; +import type { RecursiveReadonly } from '@kbn/utility-types'; +import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry'; + +interface Props { + capabilities: RecursiveReadonly; + rule: Rule | undefined; + ruleType: RuleType | undefined; + ruleTypeRegistry: TypeRegistry>; +} + +export function isRuleEditable({ capabilities, rule, ruleType, ruleTypeRegistry }: Props) { + const canExecuteActions = capabilities?.actions?.execute; + + const hasAllPrivilege = (rule && ruleType?.authorizedConsumers[rule.consumer]?.all) ?? false; + + const canSaveRule = + rule && + hasAllPrivilege && + // if the rule has actions, can the user save the rule's action params + (canExecuteActions || (!canExecuteActions && rule.actions.length === 0)); + + return Boolean( + // 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) + ); +} diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx deleted file mode 100644 index 5ca95a1b249d2..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ /dev/null @@ -1,454 +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 { estypes } from '@elastic/elasticsearch'; -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { useHistory, useParams, useLocation } 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 { - bulkDeleteRules, - 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 { ValidFeatureId } from '@kbn/rule-data-utils'; -import { RuleDefinitionProps } from '@kbn/triggers-actions-ui-plugin/public'; -import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; -import { useKibana } from '../../utils/kibana_react'; -import { fromQuery, toQuery } from '../../utils/url'; -import { - defaultTimeRange, - getDefaultAlertSummaryTimeRange, -} from '../../utils/alert_summary_widget'; -import { ObservabilityAlertSearchbarWithUrlSync } from '../../components/alert_search_bar/alert_search_bar_with_url_sync'; -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, - SEARCH_BAR_URL_STORAGE_KEY, -} from './constants'; -import { RuleDetailsPathParams, TabId } from './types'; -import { usePluginContext } from '../../hooks/use_plugin_context'; -import { useFetchRule } from '../../hooks/use_fetch_rule'; -import { PageTitle } from './components'; -import { getHealthColor } from './config'; -import { hasExecuteActionsCapability, hasAllPrivilege } from './config'; -import { paths } from '../../routes/paths'; -import { ALERT_STATUS_ALL } from '../../../common/constants'; -import { observabilityFeatureId, ruleDetailsLocatorID } from '../../../common'; -import { ALERT_STATUS_LICENSE_ERROR, rulesStatusesTranslationsMapping } from './translations'; -import type { AlertStatus } from '../../../common/typings'; - -export function RuleDetailsPage() { - const { - charts, - http, - triggersActionsUi: { - alertsTableConfigurationRegistry, - ruleTypeRegistry, - getEditRuleFlyout: EditRuleFlyout, - getRuleEventLogList, - getAlertsStateTable: AlertsStateTable, - getAlertSummaryWidget: AlertSummaryWidget, - getRuleStatusPanel: RuleStatusPanel, - getRuleDefinition, - }, - application: { capabilities, navigateToUrl }, - notifications: { toasts }, - share: { - url: { locators }, - }, - } = useKibana().services; - - const { ruleId } = useParams(); - const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); - const history = useHistory(); - const location = useLocation(); - - const chartProps = { - theme: charts.theme.useChartsTheme(), - baseTheme: charts.theme.useChartsBaseTheme(), - }; - - const filteredRuleTypes = useMemo( - () => observabilityRuleTypeRegistry.list(), - [observabilityRuleTypeRegistry] - ); - - 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 [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 [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 onAlertSummaryWidgetClick = async (status: AlertStatus = ALERT_STATUS_ALL) => { - setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange()); - await locators.get(ruleDetailsLocatorID)?.navigate( - { - rangeFrom: defaultTimeRange.from, - rangeTo: defaultTimeRange.to, - ruleId, - status, - tabId: ALERTS_TAB, - }, - { - replace: true, - } - ); - setTabId(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 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 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); - - if (rule.consumer === ALERTS_FEATURE_ID && matchedRuleType && matchedRuleType.producer) { - setFeatureIds([matchedRuleType.producer] as ValidFeatureId[]); - } else setFeatureIds([rule.consumer] as ValidFeatureId[]); - } - }, [rule, ruleTypes]); - - 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 canExecuteActions = hasExecuteActionsCapability(capabilities); - - 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 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); - - 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 && featureIds && ( - - )} - - - - ), - }, - ]; - - 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', - })} - - - - - - , - ] - : [], - }} - > - - - - - - - - {getRuleDefinition({ rule, onEditRule: reloadRule } as RuleDefinitionProps)} - - - -
- tab.id === tabId)} - onTabClick={(tab) => { - onTabIdChange(tab.id as TabId); - }} - /> - {editFlyoutVisible && ( - { - setEditFlyoutVisible(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 })} - - ); -} 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 new file mode 100644 index 0000000000000..b0180607d495d --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rule_details/rule_details.tsx @@ -0,0 +1,315 @@ +/* + * 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, useEffect, useRef } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ALERTS_FEATURE_ID, RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common'; +import type { BoolQuery } from '@kbn/es-query'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; +import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { useKibana } from '../../utils/kibana_react'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useFetchRule } from '../../hooks/use_fetch_rule'; +import { useFetchRuleTypes } from '../../hooks/use_fetch_rule_types'; +import { PageTitle } from './components/page_title'; +import { DeleteConfirmationModal } from './components/delete_confirmation_modal'; +import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; +import { NoRuleFoundPanel } from './components/no_rule_found_panel'; +import { HeaderActions } from './components/header_actions'; +import { RuleDetailsTabs } from './components/rule_details_tabs'; +import { getHealthColor } from './helpers/get_health_color'; +import { isRuleEditable } from './helpers/is_rule_editable'; +import { ruleDetailsLocatorID } from '../../../common'; +import { ALERT_STATUS_ALL } from '../../../common/constants'; +import { + RULE_DETAILS_EXECUTION_TAB, + RULE_DETAILS_ALERTS_TAB, + RULE_DETAILS_TAB_URL_STORAGE_KEY, +} from './constants'; +import { paths } from '../../routes/paths'; +import { + defaultTimeRange, + getDefaultAlertSummaryTimeRange, +} from '../../utils/alert_summary_widget'; +import type { AlertStatus } from '../../../common/typings'; + +export type TabId = typeof RULE_DETAILS_ALERTS_TAB | typeof RULE_DETAILS_EXECUTION_TAB; + +interface RuleDetailsPathParams { + ruleId: string; +} +export function RuleDetailsPage() { + const { + application: { capabilities, navigateToUrl }, + charts: { + theme: { useChartsBaseTheme, useChartsTheme }, + }, + http: { basePath }, + share: { + url: { locators }, + }, + triggersActionsUi: { + actionTypeRegistry, + ruleTypeRegistry, + getAlertSummaryWidget: AlertSummaryWidget, + getEditRuleFlyout: EditRuleFlyout, + getRuleDefinition: RuleDefinition, + getRuleStatusPanel: RuleStatusPanel, + }, + } = useKibana().services; + const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); + + const { ruleId } = useParams(); + const { search } = useLocation(); + + const theme = useChartsTheme(); + const baseTheme = useChartsBaseTheme(); + + const { rule, isLoading, isError, refetch } = useFetchRule({ ruleId }); + + const { ruleTypes } = useFetchRuleTypes({ + filterByRuleTypeIds: observabilityRuleTypeRegistry.list(), + }); + + useBreadcrumbs([ + { + text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', { + defaultMessage: 'Alerts', + }), + href: basePath.prepend(paths.observability.alerts), + }, + { + href: basePath.prepend(paths.observability.rules), + text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', { + defaultMessage: 'Rules', + }), + }, + { + text: rule && rule.name, + }, + ]); + + const [activeTabId, setActiveTabId] = useState(() => { + const searchParams = new URLSearchParams(search); + const urlTabId = searchParams.get(RULE_DETAILS_TAB_URL_STORAGE_KEY); + + return urlTabId && [RULE_DETAILS_EXECUTION_TAB, RULE_DETAILS_ALERTS_TAB].includes(urlTabId) + ? (urlTabId as TabId) + : RULE_DETAILS_EXECUTION_TAB; + }); + + const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); + + const [alertSummaryWidgetTimeRange, setAlertSummaryWidgetTimeRange] = useState( + getDefaultAlertSummaryTimeRange + ); + + const [isEditRuleFlyoutVisible, setEditRuleFlyoutVisible] = useState(false); + + const [ruleToDelete, setRuleToDelete] = useState(undefined); + const [isRuleDeleting, setIsRuleDeleting] = useState(false); + + const tabsRef = useRef(null); + + useEffect(() => { + setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange()); + }, [esQuery]); + + const handleSetTabId = async (tabId: TabId) => { + setActiveTabId(tabId); + + await locators.get(ruleDetailsLocatorID)?.navigate( + { + ruleId, + tabId, + }, + { + replace: true, + } + ); + }; + + const handleAlertSummaryWidgetClick = async (status: AlertStatus = ALERT_STATUS_ALL) => { + setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange()); + + await locators.get(ruleDetailsLocatorID)?.navigate( + { + rangeFrom: defaultTimeRange.from, + rangeTo: defaultTimeRange.to, + ruleId, + status, + tabId: RULE_DETAILS_ALERTS_TAB, + }, + { + replace: true, + } + ); + + setActiveTabId(RULE_DETAILS_ALERTS_TAB); + + tabsRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + const handleEditRule = () => { + setEditRuleFlyoutVisible(true); + }; + + const handleCloseRuleFlyout = () => { + setEditRuleFlyoutVisible(false); + }; + + const handleDeleteRule = () => { + setRuleToDelete(rule?.id); + setEditRuleFlyoutVisible(false); + }; + + const handleIsDeletingRule = () => { + setIsRuleDeleting(true); + }; + + const handleIsRuleDeleted = () => { + setRuleToDelete(undefined); + setIsRuleDeleting(false); + navigateToUrl(basePath.prepend(paths.observability.rules)); + }; + + const ruleType = ruleTypes?.find((type) => type.id === rule?.ruleTypeId); + + const isEditable = isRuleEditable({ capabilities, rule, ruleType, ruleTypeRegistry }); + + const featureIds = + rule?.consumer === ALERTS_FEATURE_ID && ruleType?.producer + ? [ruleType.producer as AlertConsumers] + : rule + ? [rule.consumer as AlertConsumers] + : []; + + const ruleStatusMessage = + rule?.executionStatus.error?.reason === RuleExecutionStatusErrorReasons.License + ? rulesStatusesTranslationsMapping.noLicense + : rule + ? rulesStatusesTranslationsMapping[rule.executionStatus.status] + : ''; + + if (isLoading || isRuleDeleting) return ; + + if (!rule || isError) return ; + + return ( + , + bottomBorder: false, + rightSideItems: [ + , + ], + }} + > + + + + + + + + + + { + refetch(); + }} + /> + + + + +
+ + + + {isEditRuleFlyoutVisible && ( + { + refetch(); + }} + /> + )} + + {ruleToDelete ? ( + setRuleToDelete(undefined)} + onDeleting={handleIsDeletingRule} + onDeleted={handleIsRuleDeleted} + /> + ) : null} + + ); +} + +const rulesStatusesTranslationsMapping = { + ok: i18n.translate('xpack.observability.ruleDetails.ruleStatusOk', { + defaultMessage: 'Ok', + }), + active: i18n.translate('xpack.observability.ruleDetails.ruleStatusActive', { + defaultMessage: 'Active', + }), + error: i18n.translate('xpack.observability.ruleDetails.ruleStatusError', { + defaultMessage: 'Error', + }), + pending: i18n.translate('xpack.observability.ruleDetails.ruleStatusPending', { + defaultMessage: 'Pending', + }), + unknown: i18n.translate('xpack.observability.ruleDetails.ruleStatusUnknown', { + defaultMessage: 'Unknown', + }), + warning: i18n.translate('xpack.observability.ruleDetails.ruleStatusWarning', { + defaultMessage: 'Warning', + }), + noLicense: i18n.translate('xpack.observability.ruleDetails.ruleStatusLicenseError', { + defaultMessage: 'License Error', + }), +}; diff --git a/x-pack/plugins/observability/public/pages/rule_details/translations.ts b/x-pack/plugins/observability/public/pages/rule_details/translations.ts deleted file mode 100644 index e30178e15cf47..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/translations.ts +++ /dev/null @@ -1,143 +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 { i18n } from '@kbn/i18n'; - -export const RULE_LOAD_ERROR = (errorMessage: string) => - i18n.translate('xpack.observability.ruleDetails.ruleLoadError', { - defaultMessage: 'Unable to load rule. Reason: {message}', - values: { message: errorMessage }, - }); - -export const EXECUTION_LOG_ERROR = (errorMessage: string) => - i18n.translate('xpack.observability.ruleDetails.executionLogError', { - defaultMessage: 'Unable to load rule execution log. Reason: {message}', - values: { message: errorMessage }, - }); - -export const TAGS_TITLE = i18n.translate('xpack.observability.ruleDetails.tagsTitle', { - defaultMessage: 'Tags', -}); - -export const LAST_UPDATED_MESSAGE = i18n.translate( - 'xpack.observability.ruleDetails.lastUpdatedMessage', - { - defaultMessage: 'Last updated', - } -); - -export const BY_WORD = i18n.translate('xpack.observability.ruleDetails.byWord', { - defaultMessage: 'by', -}); - -export const ON_WORD = i18n.translate('xpack.observability.ruleDetails.onWord', { - defaultMessage: 'on', -}); - -export const CREATED_WORD = i18n.translate('xpack.observability.ruleDetails.createdWord', { - 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', - { - defaultMessage: 'License Error', - } -); - -export const ALERT_STATUS_OK = i18n.translate('xpack.observability.ruleDetails.ruleStatusOk', { - defaultMessage: 'Ok', -}); - -export const ALERT_STATUS_ACTIVE = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusActive', - { - defaultMessage: 'Active', - } -); - -export const ALERT_STATUS_ERROR = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusError', - { - defaultMessage: 'Error', - } -); - -export const ALERT_STATUS_PENDING = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusPending', - { - defaultMessage: 'Pending', - } -); - -export const ALERT_STATUS_UNKNOWN = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusUnknown', - { - defaultMessage: 'Unknown', - } -); - -export const ALERT_STATUS_WARNING = i18n.translate( - 'xpack.observability.ruleDetails.ruleStatusWarning', - { - defaultMessage: 'Warning', - } -); - -export const rulesStatusesTranslationsMapping = { - ok: ALERT_STATUS_OK, - active: ALERT_STATUS_ACTIVE, - error: ALERT_STATUS_ERROR, - pending: ALERT_STATUS_PENDING, - unknown: ALERT_STATUS_UNKNOWN, - warning: ALERT_STATUS_WARNING, -}; diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts deleted file mode 100644 index 07407ca217444..0000000000000 --- a/x-pack/plugins/observability/public/pages/rule_details/types.ts +++ /dev/null @@ -1,56 +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 { HttpSetup } from '@kbn/core/public'; -import { Rule, RuleSummary, RuleType } from '@kbn/triggers-actions-ui-plugin/public'; -import { ALERTS_TAB, EXECUTION_TAB } from './constants'; - -export type TabId = typeof ALERTS_TAB | typeof EXECUTION_TAB; - -export interface RuleDetailsPathParams { - ruleId: string; -} -export interface PageHeaderProps { - rule: Rule; -} - -export interface FetchRuleProps { - ruleId?: string; - http: HttpSetup; -} - -export interface FetchRule { - isRuleLoading: boolean; - rule?: Rule; - ruleType?: RuleType; - errorRule?: string; -} - -export interface FetchRuleSummaryProps { - ruleId: string; - http: HttpSetup; -} - -export interface FetchRuleSummary { - isLoadingRuleSummary: boolean; - ruleSummary?: RuleSummary; - errorRuleSummary?: string; -} - -export interface AlertListItemStatus { - label: string; - healthColor: string; - actionGroup?: string; -} -export interface AlertListItem { - alert: string; - status: AlertListItemStatus; - start?: Date; - duration: number; - isMuted: boolean; - sortPriority: number; -} diff --git a/x-pack/plugins/observability/public/routes/routes.tsx b/x-pack/plugins/observability/public/routes/routes.tsx index 9fc1fc8d4b5a2..433e674b73935 100644 --- a/x-pack/plugins/observability/public/routes/routes.tsx +++ b/x-pack/plugins/observability/public/routes/routes.tsx @@ -15,7 +15,7 @@ import { CasesPage } from '../pages/cases/cases'; import { LandingPage } from '../pages/landing/landing'; import { OverviewPage } from '../pages/overview/overview'; import { RulesPage } from '../pages/rules/rules'; -import { RuleDetailsPage } from '../pages/rule_details'; +import { RuleDetailsPage } from '../pages/rule_details/rule_details'; import { SlosPage } from '../pages/slos/slos'; import { SlosWelcomePage } from '../pages/slos_welcome/slos_welcome'; import { SloDetailsPage } from '../pages/slo_details/slo_details'; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 09e809d69542b..08f0bca59390c 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -79,9 +79,9 @@ "@kbn/cloud-chat-plugin", "@kbn/cloud-plugin", "@kbn/stack-alerts-plugin", - "@kbn/data-view-editor-plugin" + "@kbn/data-view-editor-plugin", + "@kbn/actions-plugin", + "@kbn/core-capabilities-common" ], - "exclude": [ - "target/**/*" - ] + "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index d9e041ec102d9..654e2259983cd 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26501,12 +26501,7 @@ "xpack.observability.enableCriticalPathDescription": "{technicalPreviewLabel} Affichez de façon optionnelle le chemin critique d'une trace.", "xpack.observability.inspector.stats.queryTimeValue": "{queryTime} ms", "xpack.observability.pages.alertDetails.pageTitle.title": "{ruleCategory} {ruleCategory, select, Anomaly {détecté} Inventory {seuil dépassé} other {dépassé}}", - "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}} many {# {multipleTitle}} other {# {multipleTitle}}} ", - "xpack.observability.rules.deleteSelectedIdsConfirmModal.descriptionText": "Vous ne pouvez pas récupérer {numIdsToDelete, plural, one {un {singleTitle} supprimé} many {des {multipleTitle} supprimés} other {des {multipleTitle} supprimés}}.", - "xpack.observability.rules.deleteSelectedIdsErrorNotification.descriptionText": "Impossible de supprimer {numErrors, number} {numErrors, plural, one {{singleTitle}} many {{multipleTitle}} other {{multipleTitle}}}", - "xpack.observability.rules.deleteSelectedIdsSuccessNotification.descriptionText": "{numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} many {{multipleTitle}} other {{multipleTitle}}} supprimé(s)", "xpack.observability.slo.alerting.burnRate.reason": "{actionGroupName}: Le taux d'avancement pour le (les) dernier(s) {longWindowDuration} est de {longWindowBurnRate} et pour le (les) dernier(s) {shortWindowDuration} est de {shortWindowBurnRate}. Alerter si supérieur à {burnRateThreshold} pour les deux fenêtres", "xpack.observability.slo.duration.day": "{duration, plural, one {1 jour} many {# jours} other {# jours}}", "xpack.observability.slo.duration.hour": "{duration, plural, one {1 heure} many {# heures} other {# heures}}", @@ -26745,9 +26740,7 @@ "xpack.observability.ruleDetails.ruleStatusPending": "En attente", "xpack.observability.ruleDetails.ruleStatusUnknown": "Inconnu", "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.rulesLinkTitle": "Règles", "xpack.observability.rulesTitle": "Règles", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 499805b310a36..70ae6e7263d36 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26487,7 +26487,6 @@ "xpack.observability.enableCriticalPathDescription": "{technicalPreviewLabel} 任意で、トレースのクリティカルパスを表示します。", "xpack.observability.inspector.stats.queryTimeValue": "{queryTime}ms", "xpack.observability.pages.alertDetails.pageTitle.title": "{ruleCategory} {ruleCategory, select, Anomaly {が検出されました} Inventory {しきい値を超えました} other {を超えました}}", - "xpack.observability.ruleDetails.executionLogError": "ルール実行ログを読み込めません。理由:{message}", "xpack.observability.ruleDetails.ruleLoadError": "ルールを読み込めません。理由:{message}", "xpack.observability.slo.alerting.burnRate.reason": "{actionGroupName}: 過去{longWindowDuration}のバーンレートは{longWindowBurnRate}で、過去{shortWindowDuration}のバーンレートは{shortWindowBurnRate}です。両期間とも{burnRateThreshold}を超えたらアラート", "xpack.observability.slo.duration.day": "{duration, plural, other {#日}}", @@ -26727,9 +26726,7 @@ "xpack.observability.ruleDetails.ruleStatusPending": "保留中", "xpack.observability.ruleDetails.ruleStatusUnknown": "不明", "xpack.observability.ruleDetails.ruleStatusWarning": "警告", - "xpack.observability.ruleDetails.tagsTitle": "タグ", "xpack.observability.rules.addRuleButtonLabel": "ルールを作成", - "xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.observability.rules.docsLinkText": "ドキュメント", "xpack.observability.rulesLinkTitle": "ルール", "xpack.observability.rulesTitle": "ルール", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 08567d2070538..0bba12b9495e1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26485,7 +26485,6 @@ "xpack.observability.enableCriticalPathDescription": "{technicalPreviewLabel}(可选)显示跟踪的关键路径。", "xpack.observability.inspector.stats.queryTimeValue": "{queryTime}ms", "xpack.observability.pages.alertDetails.pageTitle.title": "{ruleCategory} {ruleCategory, select, Anomaly {已检测到} Inventory {超出阈值} other {超出}}", - "xpack.observability.ruleDetails.executionLogError": "无法加载规则执行日志。原因:{message}", "xpack.observability.ruleDetails.ruleLoadError": "无法加载规则。原因:{message}", "xpack.observability.slo.alerting.burnRate.reason": "{actionGroupName}: 过去 {longWindowDuration} 的消耗速度为 {longWindowBurnRate} 且过去 {shortWindowDuration} 为 {shortWindowBurnRate}。两个窗口超出 {burnRateThreshold} 时告警", "xpack.observability.slo.duration.day": "{duration, plural, other {# 天}}", @@ -26725,9 +26724,7 @@ "xpack.observability.ruleDetails.ruleStatusPending": "待处理", "xpack.observability.ruleDetails.ruleStatusUnknown": "未知", "xpack.observability.ruleDetails.ruleStatusWarning": "警告", - "xpack.observability.ruleDetails.tagsTitle": "标签", "xpack.observability.rules.addRuleButtonLabel": "创建规则", - "xpack.observability.rules.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.observability.rules.docsLinkText": "文档", "xpack.observability.rulesLinkTitle": "规则", "xpack.observability.rulesTitle": "规则",