diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts index b81046df99d28..b6f7ea2d0579a 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts @@ -19,7 +19,13 @@ interface RuleState { totalItemCount: number; } -export function useFetchRules({ searchText, ruleLastResponseFilter, page, sort }: FetchRulesProps) { +export function useFetchRules({ + searchText, + ruleLastResponseFilter, + ruleStatusFilter, + page, + sort, +}: FetchRulesProps) { const { http } = useKibana().services; const [rulesState, setRulesState] = useState({ @@ -39,6 +45,7 @@ export function useFetchRules({ searchText, ruleLastResponseFilter, page, sort } searchText, typesFilter: OBSERVABILITY_RULE_TYPES, ruleStatusesFilter: ruleLastResponseFilter, + ruleStatusFilter: [false], sort, }); setRulesState((oldState) => ({ @@ -50,7 +57,7 @@ export function useFetchRules({ searchText, ruleLastResponseFilter, page, sort } } catch (_e) { setRulesState((oldState) => ({ ...oldState, isLoading: false, error: RULES_LOAD_ERROR })); } - }, [http, page, searchText, ruleLastResponseFilter, sort]); + }, [http, page, searchText, ruleLastResponseFilter, ruleStatusFilter, sort]); useEffect(() => { fetchRules(); }, [fetchRules]); diff --git a/x-pack/plugins/observability/public/pages/rules/components/filters/status_filter.tsx b/x-pack/plugins/observability/public/pages/rules/components/filters/status_filter.tsx new file mode 100644 index 0000000000000..8c916380d50d1 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/filters/status_filter.tsx @@ -0,0 +1,77 @@ +/* + * 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, useMemo } from 'react'; +import { EuiFilterGroup, EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { RuleStatus } from '../../types'; +import { statusMap } from '../../config'; + +export function StatusFilter({ selectedStatuses, onChange }) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [selectedValues, setSelectedValues] = useState(selectedStatuses); + + useEffect(() => { + if (onChange) { + onChange(selectedValues); + } + }, [selectedValues, onChange]); + + useEffect(() => { + setSelectedValues(selectedStatuses); + }, [selectedStatuses]); + + const panelItems = useMemo( + () => + Object.values(RuleStatus).map((status: RuleStatus) => ( + { + const isPreviouslyChecked = selectedValues.includes(status); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== status)); + } else { + setSelectedValues(selectedValues.concat(status)); + } + }} + checked={selectedValues.includes(status) ? 'on' : undefined} + data-test-subj={`ruleStatus${status}FilterOption`} + > + {statusMap[status].label} + + )), + [selectedValues] + ); + + return ( + + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="ruleStatusFilterButton" + > + + + } + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="downLeft" + isOpen={isPopoverOpen} + panelPaddingSize="none" + > + {panelItems} + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx b/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx index 5a9be48252909..b56f6c7515162 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/last_response_filter.tsx @@ -74,7 +74,7 @@ export const LastResponseFilter: React.FunctionComponent = ({ } }} checked={selectedValues.includes(item) ? 'on' : undefined} - data-test-subj={`ruleStatus${item}FilerOption`} + data-test-subj={`ruleLastResponse${item}FilterOption`} > {rulesStatusesTranslationsMapping[item]} diff --git a/x-pack/plugins/observability/public/pages/rules/components/name.tsx b/x-pack/plugins/observability/public/pages/rules/components/name.tsx index 2b1f831256910..cbde68ea27eb4 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/name.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/name.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { RuleNameProps } from '../types'; import { useKibana } from '../../../utils/kibana_react'; @@ -34,17 +33,5 @@ export function Name({ name, rule }: RuleNameProps) { ); - return ( - <> - {link} - {rule.enabled && rule.muteAll && ( - - - - )} - - ); + return <>{link}; } diff --git a/x-pack/plugins/observability/public/pages/rules/components/prompts/no_data_prompt.tsx b/x-pack/plugins/observability/public/pages/rules/components/prompts/no_data_prompt.tsx new file mode 100644 index 0000000000000..b9c0e24160004 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/prompts/no_data_prompt.tsx @@ -0,0 +1,69 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { EuiButton, EuiEmptyPrompt, EuiLink, EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui'; + +export function NoDataPrompt({ + onCTAClicked, + documentationLink, +}: { + onCTAClicked: () => void; + documentationLink: string; +}) { + return ( + + + + + } + body={ +

+ +

+ } + actions={[ + + + , + + + Documentation + + , + ]} + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/prompts/no_permission_prompt.tsx b/x-pack/plugins/observability/public/pages/rules/components/prompts/no_permission_prompt.tsx new file mode 100644 index 0000000000000..edfe1c6840d8b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/rules/components/prompts/no_permission_prompt.tsx @@ -0,0 +1,44 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import React from 'react'; +import { EuiEmptyPrompt, EuiPageTemplate } from '@elastic/eui'; + +export function NoPermissionPrompt() { + return ( + + + + + } + body={ +

+ +

+ } + /> +
+ ); +} diff --git a/x-pack/plugins/observability/public/pages/rules/components/status.tsx b/x-pack/plugins/observability/public/pages/rules/components/status.tsx index abc2dc8bfa492..612d6f8f30bdd 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/status.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/status.tsx @@ -5,19 +5,28 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiBadge } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import { StatusProps } from '../types'; import { statusMap } from '../config'; +import { RULES_CHANGE_STATUS } from '../translations'; -export function Status({ type, onClick }: StatusProps) { +export function Status({ type, disabled, onClick }: StatusProps) { + const props = useMemo( + () => ({ + color: statusMap[type].color, + ...(!disabled ? { onClick } : { onClick: noop }), + ...(!disabled ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), + ...(!disabled ? { iconOnClick: onClick } : { iconOnClick: noop }), + }), + [disabled, onClick, type] + ); return ( {statusMap[type].label} diff --git a/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx index 49761d7c43154..c7bd29d85b17a 100644 --- a/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx +++ b/x-pack/plugins/observability/public/pages/rules/components/status_context.tsx @@ -18,19 +18,26 @@ import { statusMap } from '../config'; export function StatusContext({ item, + disabled = false, onStatusChanged, enableRule, disableRule, muteRule, + unMuteRule, }: StatusContextProps) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const togglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]); - const currentStatus = item.enabled ? RuleStatus.enabled : RuleStatus.disabled; + let currentStatus: RuleStatus; + if (item.enabled) { + currentStatus = item.muteAll ? RuleStatus.snoozed : RuleStatus.enabled; + } else { + currentStatus = RuleStatus.disabled; + } const popOverButton = useMemo( - () => , - [currentStatus, togglePopover] + () => , + [disabled, currentStatus, togglePopover] ); const onContextMenuItemClick = useCallback( @@ -41,15 +48,30 @@ export function StatusContext({ if (status === RuleStatus.enabled) { await enableRule({ ...item, enabled: true }); + if (item.muteAll) { + await unMuteRule({ ...item, muteAll: false }); + } } else if (status === RuleStatus.disabled) { await disableRule({ ...item, enabled: false }); + } else if (status === RuleStatus.snoozed) { + await muteRule({ ...item, muteAll: true }); } setIsUpdating(false); onStatusChanged(status); } }, - [item, togglePopover, enableRule, disableRule, currentStatus, onStatusChanged] + [ + item, + togglePopover, + enableRule, + disableRule, + muteRule, + unMuteRule, + currentStatus, + onStatusChanged, + ] ); + const panelItems = useMemo( () => Object.values(RuleStatus).map((status: RuleStatus) => ( @@ -57,6 +79,7 @@ export function StatusContext({ icon={status === currentStatus ? 'check' : 'empty'} key={status} onClick={() => onContextMenuItemClick(status)} + disabled={status === RuleStatus.snoozed && currentStatus === RuleStatus.disabled} > {statusMap[status].label} diff --git a/x-pack/plugins/observability/public/pages/rules/config.ts b/x-pack/plugins/observability/public/pages/rules/config.ts index afff097776e19..736f538ee7b21 100644 --- a/x-pack/plugins/observability/public/pages/rules/config.ts +++ b/x-pack/plugins/observability/public/pages/rules/config.ts @@ -13,6 +13,9 @@ import { RULE_STATUS_PENDING, RULE_STATUS_UNKNOWN, RULE_STATUS_WARNING, + RULE_STATUS_ENABLED, + RULE_STATUS_DISABLED, + RULE_STATUS_SNOOZED_INDEFINITELY, } from './translations'; import { AlertExecutionStatuses } from '../../../../alerting/common'; import { Rule, RuleTypeIndex, RuleType } from '../../../../triggers_actions_ui/public'; @@ -20,11 +23,15 @@ import { Rule, RuleTypeIndex, RuleType } from '../../../../triggers_actions_ui/p export const statusMap: Status = { [RuleStatus.enabled]: { color: 'primary', - label: 'Enabled', + label: RULE_STATUS_ENABLED, }, [RuleStatus.disabled]: { color: 'default', - label: 'Disabled', + label: RULE_STATUS_DISABLED, + }, + [RuleStatus.snoozed]: { + color: 'warning', + label: RULE_STATUS_SNOOZED_INDEFINITELY, }, }; @@ -93,3 +100,8 @@ export function convertRulesToTableItems( enabledInLicense: !!ruleTypeIndex.get(rule.ruleTypeId)?.enabledInLicense, })); } + +type Capabilities = Record; + +export const hasExecuteActionsCapability = (capabilities: Capabilities) => + capabilities?.actions?.execute; diff --git a/x-pack/plugins/observability/public/pages/rules/index.tsx b/x-pack/plugins/observability/public/pages/rules/index.tsx index 8c44fa90fb3d1..4ecf44ce48372 100644 --- a/x-pack/plugins/observability/public/pages/rules/index.tsx +++ b/x-pack/plugins/observability/public/pages/rules/index.tsx @@ -27,11 +27,15 @@ import { useFetchRules } from '../../hooks/use_fetch_rules'; import { RulesTable } from './components/rules_table'; import { Name } from './components/name'; import { LastResponseFilter } from './components/last_response_filter'; +import { StatusFilter } from './components/filters/status_filter'; + import { StatusContext } from './components/status_context'; import { ExecutionStatus } from './components/execution_status'; import { LastRun } from './components/last_run'; import { EditRuleFlyout } from './components/edit_rule_flyout'; import { DeleteModalConfirmation } from './components/delete_modal_confirmation'; +import { NoDataPrompt } from './components/prompts/no_data_prompt'; +import { NoPermissionPrompt } from './components/prompts/no_permission_prompt'; import { deleteRules, RuleTableItem, @@ -39,6 +43,7 @@ import { disableRule, muteRule, useLoadRuleTypes, + unmuteRule, } from '../../../../triggers_actions_ui/public'; import { AlertExecutionStatus, ALERTS_FEATURE_ID } from '../../../../alerting/common'; import { Pagination } from './types'; @@ -46,6 +51,7 @@ import { DEFAULT_SEARCH_PAGE_SIZE, convertRulesToTableItems, OBSERVABILITY_SOLUTIONS, + hasExecuteActionsCapability, } from './config'; import { LAST_RESPONSE_COLUMN_TITLE, @@ -73,9 +79,12 @@ export function RulesPage() { http, docLinks, triggersActionsUi, + application: { capabilities }, notifications: { toasts }, } = useKibana().services; - + const documentationLink = docLinks.links.alerting.guide; + const ruleTypeRegistry = triggersActionsUi.ruleTypeRegistry; + const canExecuteActions = hasExecuteActionsCapability(capabilities); const [page, setPage] = useState({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE }); const [sort, setSort] = useState['sort']>({ field: 'name', @@ -85,11 +94,15 @@ export function RulesPage() { const [searchText, setSearchText] = useState(); const [refreshInterval, setRefreshInterval] = useState(60000); const [isPaused, setIsPaused] = useState(false); + const [ruleStatusFilter, setRuleStatusFilter] = useState([]); const [ruleLastResponseFilter, setRuleLastResponseFilter] = useState([]); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); const [rulesToDelete, setRulesToDelete] = useState([]); const [createRuleFlyoutVisibility, setCreateRuleFlyoutVisibility] = useState(false); + const isRuleTypeEditableInContext = (ruleTypeId: string) => + ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false; + const onRuleEdit = (ruleItem: RuleTableItem) => { setCurrentRuleToEdit(ruleItem); }; @@ -105,11 +118,19 @@ export function RulesPage() { const { rulesState, setRulesState, reload } = useFetchRules({ searchText, ruleLastResponseFilter, + ruleStatusFilter, page, sort, }); const { data: rules, totalItemCount, error } = rulesState; - const { ruleTypeIndex } = useLoadRuleTypes({ filteredSolutions: OBSERVABILITY_SOLUTIONS }); + const { ruleTypeIndex, ruleTypes } = useLoadRuleTypes({ + filteredSolutions: OBSERVABILITY_SOLUTIONS, + }); + const authorizedRuleTypes = [...ruleTypes.values()]; + + const authorizedToCreateAnyRules = authorizedRuleTypes.some( + (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all + ); useEffect(() => { const interval = setInterval(() => { @@ -161,11 +182,13 @@ export function RulesPage() { render: (_enabled: boolean, item: RuleTableItem) => { return ( reload()} enableRule={async () => await enableRule({ http, id: item.id })} disableRule={async () => await disableRule({ http, id: item.id })} muteRule={async () => await muteRule({ http, id: item.id })} + unMuteRule={async () => await unmuteRule({ http, id: item.id })} /> ); }, @@ -180,6 +203,9 @@ export function RulesPage() { { + if (totalItemCount === 0 && !rulesState.isLoading) { + return authorizedToCreateAnyRules ? ( + setCreateRuleFlyoutVisibility(true)} + /> + ) : ( + + ); + } + return ( + <> + + + { + setInputText(e.target.value); + if (e.target.value === '') { + setSearchText(e.target.value); + } + }} + onKeyUp={(e) => { + if (e.keyCode === ENTER_KEY) { + setSearchText(inputText); + } + }} + placeholder={SEARCH_PLACEHOLDER} + /> + + + setRuleLastResponseFilter(ids)} + /> + + + setRuleStatusFilter(ids)} + /> + + + + + + , + + + + + + + + + + + + + + + + setPage(index)} + sort={sort} + onSortChange={(changedSort) => { + setSort(changedSort); + }} + /> + + + + ); + }; + return ( ), rightSideItems: [ - setCreateRuleFlyoutVisibility(true)} - > - - , + authorizedToCreateAnyRules && ( + setCreateRuleFlyoutVisibility(true)} + > + + + ), - - - { - setInputText(e.target.value); - if (e.target.value === '') { - setSearchText(e.target.value); - } - }} - onKeyUp={(e) => { - if (e.keyCode === ENTER_KEY) { - setSearchText(inputText); - } - }} - placeholder={SEARCH_PLACEHOLDER} - /> - - - setRuleLastResponseFilter(ids)} - /> - - - - - - , - - - - - - - - - - - - - - - - setPage(index)} - sort={sort} - onSortChange={(changedSort) => { - setSort(changedSort); - }} - /> - - + {getRulesTable()} {error && toasts.addDanger({ title: error, diff --git a/x-pack/plugins/observability/public/pages/rules/translations.ts b/x-pack/plugins/observability/public/pages/rules/translations.ts index b72d03bf8e566..36f8ff62f1a4c 100644 --- a/x-pack/plugins/observability/public/pages/rules/translations.ts +++ b/x-pack/plugins/observability/public/pages/rules/translations.ts @@ -53,6 +53,27 @@ export const RULE_STATUS_WARNING = i18n.translate( } ); +export const RULE_STATUS_ENABLED = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusEnabled', + { + defaultMessage: 'Enabled', + } +); + +export const RULE_STATUS_DISABLED = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusDisabled', + { + defaultMessage: 'Disabled', + } +); + +export const RULE_STATUS_SNOOZED_INDEFINITELY = i18n.translate( + 'xpack.observability.rules.rulesTable.ruleStatusSnoozedIndefinitely', + { + defaultMessage: 'Snoozed indefinitely', + } +); + export const LAST_RESPONSE_COLUMN_TITLE = i18n.translate( 'xpack.observability.rules.rulesTable.columns.lastResponseTitle', { @@ -144,6 +165,13 @@ export const SEARCH_PLACEHOLDER = i18n.translate( { defaultMessage: 'Search' } ); +export const RULES_CHANGE_STATUS = i18n.translate( + 'xpack.observability.rules.rulesTable.changeStatusAriaLabel', + { + defaultMessage: 'Change status', + } +); + export const confirmModalText = ( numIdsToDelete: number, singleTitle: string, diff --git a/x-pack/plugins/observability/public/pages/rules/types.ts b/x-pack/plugins/observability/public/pages/rules/types.ts index 23443890ad8fa..8c59103d7f2b5 100644 --- a/x-pack/plugins/observability/public/pages/rules/types.ts +++ b/x-pack/plugins/observability/public/pages/rules/types.ts @@ -9,12 +9,14 @@ import { AlertExecutionStatus } from '../../../../alerting/common'; import { RuleTableItem, Rule } from '../../../../triggers_actions_ui/public'; export interface StatusProps { type: RuleStatus; + disabled: boolean; onClick: () => void; } export enum RuleStatus { enabled = 'enabled', disabled = 'disabled', + snoozed = 'snoozed', } export type Status = Record< @@ -27,10 +29,12 @@ export type Status = Record< export interface StatusContextProps { item: RuleTableItem; + disabled: boolean; onStatusChanged: (status: RuleStatus) => void; enableRule: (rule: Rule) => Promise; disableRule: (rule: Rule) => Promise; muteRule: (rule: Rule) => Promise; + unMuteRule: (rule: Rule) => Promise; } export interface StatusFilterProps { @@ -64,6 +68,7 @@ export interface Pagination { export interface FetchRulesProps { searchText: string | undefined; ruleLastResponseFilter: string[]; + ruleStatusFilter: boolean[]; page: Pagination; sort: EuiTableSortingType['sort']; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index d7b22a7a4aee4..9b33592732f37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -9,10 +9,12 @@ export const mapFiltersToKql = ({ typesFilter, actionTypesFilter, ruleStatusesFilter, + ruleStatusFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; ruleStatusesFilter?: string[]; + ruleStatusFilter?: boolean[]; }): string[] => { const filters = []; if (typesFilter && typesFilter.length) { @@ -29,8 +31,12 @@ export const mapFiltersToKql = ({ ].join('') ); } + // TODO rename ruleStatusesFilter to ruleLastResponseFilter in both triggers_actions_ui and observability plugins if (ruleStatusesFilter && ruleStatusesFilter.length) { filters.push(`alert.attributes.executionStatus.status:(${ruleStatusesFilter.join(' or ')})`); } + if (ruleStatusFilter && ruleStatusFilter.length) { + filters.push(`alert.attributes.enabled:(${ruleStatusFilter.join(' or ')})`); + } return filters; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index 97c432a480355..58470f97b3731 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -22,6 +22,7 @@ export async function loadRules({ typesFilter, actionTypesFilter, ruleStatusesFilter, + ruleStatusFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -30,6 +31,7 @@ export async function loadRules({ typesFilter?: string[]; actionTypesFilter?: string[]; ruleStatusesFilter?: string[]; + ruleStatusFilter?: boolean[]; sort?: Sorting; }): Promise<{ page: number; @@ -37,7 +39,12 @@ export async function loadRules({ total: number; data: Rule[]; }> { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter }); + const filters = mapFiltersToKql({ + typesFilter, + actionTypesFilter, + ruleStatusesFilter, + ruleStatusFilter, + }); const res = await http.get< AsApiContract<{ page: number; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index eb346e43cfbc9..b1ef489bfef70 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -55,6 +55,7 @@ export { deleteRules } from './application/lib/rule_api/delete'; export { enableRule } from './application/lib/rule_api/enable'; export { disableRule } from './application/lib/rule_api/disable'; export { muteRule } from './application/lib/rule_api/mute'; +export { unmuteRule } from './application/lib/rule_api/unmute'; export { loadRuleAggregations } from './application/lib/rule_api/aggregate'; export { useLoadRuleTypes } from './application/hooks/use_load_rule_types';