diff --git a/x-pack/plugins/apm/common/rules/apm_rule_types.ts b/x-pack/plugins/apm/common/rules/apm_rule_types.ts index 1989885428db..b18ea1f0c49c 100644 --- a/x-pack/plugins/apm/common/rules/apm_rule_types.ts +++ b/x-pack/plugins/apm/common/rules/apm_rule_types.ts @@ -245,7 +245,7 @@ export const RULE_TYPES_CONFIG: Record< }, [ApmRuleType.Anomaly]: { name: i18n.translate('xpack.apm.anomalyAlert.name', { - defaultMessage: 'Anomaly', + defaultMessage: 'APM Anomaly', }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts index 7940a4128d80..e8f2c6185ebd 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts @@ -55,6 +55,7 @@ export function registerApmRuleTypes( }), requiresAppContext: false, defaultActionMessage: errorCountMessage, + priority: 80, }); observabilityRuleTypeRegistry.register({ @@ -92,6 +93,7 @@ export function registerApmRuleTypes( ), requiresAppContext: false, defaultActionMessage: transactionDurationMessage, + priority: 60, }); observabilityRuleTypeRegistry.register({ @@ -124,6 +126,7 @@ export function registerApmRuleTypes( }), requiresAppContext: false, defaultActionMessage: transactionErrorRateMessage, + priority: 70, }); observabilityRuleTypeRegistry.register({ @@ -153,5 +156,6 @@ export function registerApmRuleTypes( }), requiresAppContext: false, defaultActionMessage: anomalyMessage, + priority: 90, }); } diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts index b25eaba2ba3a..9df7f609dcbb 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts @@ -66,5 +66,6 @@ export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel import('./components/alert_details_app_section')), + priority: 10, }; } diff --git a/x-pack/plugins/observability/public/pages/rules/rules.test.tsx b/x-pack/plugins/observability/public/pages/rules/rules.test.tsx index 4b5ee362e0aa..7a5fa20afa74 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.test.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { CoreStart } from '@kbn/core/public'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { ObservabilityPublicPluginsStart } from '../../plugin'; import { RulesPage } from './rules'; import { kibanaStartMock } from '../../utils/kibana_react.mock'; @@ -104,7 +105,11 @@ describe('RulesPage with all capabilities', () => { ruleTypeIndex, }); - return render(); + return render( + + + + ); } it('should render a page template', async () => { diff --git a/x-pack/plugins/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability/public/pages/rules/rules.tsx index 700f34884275..33a1a67be835 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.tsx @@ -150,7 +150,8 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { setRefresh(new Date()); return Promise.resolve(); }} - useRuleProducer={true} + hideGrouping + useRuleProducer /> )} diff --git a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts index ec5a5b87db3f..aef7c5eca6ce 100644 --- a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts +++ b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts @@ -21,21 +21,27 @@ export type ObservabilityRuleTypeFormatter = (options: { export interface ObservabilityRuleTypeModel extends RuleTypeModel { format: ObservabilityRuleTypeFormatter; + priority?: number; } export function createObservabilityRuleTypeRegistry(ruleTypeRegistry: RuleTypeRegistryContract) { - const formatters: Array<{ typeId: string; fn: ObservabilityRuleTypeFormatter }> = []; + const formatters: Array<{ + typeId: string; + priority: number; + fn: ObservabilityRuleTypeFormatter; + }> = []; return { register: (type: ObservabilityRuleTypeModel) => { - const { format, ...rest } = type; - formatters.push({ typeId: type.id, fn: format }); + const { format, priority, ...rest } = type; + formatters.push({ typeId: type.id, priority: priority || 0, fn: format }); ruleTypeRegistry.register(rest); }, getFormatter: (typeId: string) => { return formatters.find((formatter) => formatter.typeId === typeId)?.fn; }, - list: () => formatters.map((formatter) => formatter.typeId), + list: () => + formatters.sort((a, b) => b.priority - a.priority).map((formatter) => formatter.typeId), }; } diff --git a/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts b/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts index dfac0b91c426..5d042607135a 100644 --- a/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts +++ b/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts @@ -97,6 +97,7 @@ export const registerObservabilityRuleTypes = ( requiresAppContext: false, defaultActionMessage: sloBurnRateDefaultActionMessage, defaultRecoveryMessage: sloBurnRateDefaultRecoveryMessage, + priority: 100, }); if (config.unsafe.thresholdRule.enabled) { @@ -124,6 +125,7 @@ export const registerObservabilityRuleTypes = ( alertDetailsAppSection: lazy( () => import('../components/custom_threshold/components/alert_details_app_section') ), + priority: 110, }); } }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts index e9ec012707c7..bc740e8e878c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts @@ -6,21 +6,16 @@ */ import { RuleTypeModel } from '../../types'; -import { ruleTypeGroupCompare, ruleTypeCompare } from './rule_type_compare'; +import { + RuleTypeGroup, + ruleTypeGroupCompare, + ruleTypeCompare, + ruleTypeUngroupedCompare, +} from './rule_type_compare'; import { IsEnabledResult, IsDisabledResult } from './check_rule_type_enabled'; test('should sort groups by containing enabled rule types first and then by name', async () => { - const ruleTypes: Array< - [ - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - ] - > = [ + const ruleTypes: RuleTypeGroup[] = [ [ 'abc', [ @@ -113,6 +108,102 @@ test('should sort groups by containing enabled rule types first and then by name expect(result[2]).toEqual(ruleTypes[0]); }); +describe('ruleTypeUngroupedCompare', () => { + test('should maintain the order of rules', async () => { + const ruleTypes: RuleTypeGroup[] = [ + [ + 'abc', + [ + { + id: '1', + name: 'test2', + checkEnabledResult: { isEnabled: false, message: 'gold license' }, + ruleTypeItem: { + id: 'ruleTypeItemId1', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + [ + 'bcd', + [ + { + id: '2', + name: 'abc', + checkEnabledResult: { isEnabled: false, message: 'platinum license' }, + ruleTypeItem: { + id: 'ruleTypeItemId2', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + { + id: '3', + name: 'cdf', + checkEnabledResult: { isEnabled: true }, + ruleTypeItem: { + id: 'ruleTypeItemId3', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + [ + 'cde', + [ + { + id: '4', + name: 'cde', + checkEnabledResult: { isEnabled: true }, + ruleTypeItem: { + id: 'ruleTypeItemId4', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + ]; + + const ruleTypesOrder = ['4', '1', '2', '3']; + + const result = [...ruleTypes].sort((left, right) => + ruleTypeUngroupedCompare(left, right, ruleTypesOrder) + ); + + expect(result[0]).toEqual(ruleTypes[2]); + expect(result[1]).toEqual(ruleTypes[1]); + expect(result[2]).toEqual(ruleTypes[0]); + }); +}); + test('should sort rule types by enabled first and then by name', async () => { const ruleTypes: Array<{ id: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts index c04574f7961e..ad3de685463c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts @@ -8,25 +8,19 @@ import { RuleTypeModel } from '../../types'; import { IsEnabledResult, IsDisabledResult } from './check_rule_type_enabled'; +export type RuleTypeGroup = [ + string, + Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + ruleTypeItem: RuleTypeModel; + }> +]; + export function ruleTypeGroupCompare( - left: [ - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - ], - right: [ - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - ], + left: RuleTypeGroup, + right: RuleTypeGroup, groupNames: Map | undefined ) { const groupNameA = left[0]; @@ -54,6 +48,35 @@ export function ruleTypeGroupCompare( : groupNameA.localeCompare(groupNameB); } +export function ruleTypeUngroupedCompare( + left: RuleTypeGroup, + right: RuleTypeGroup, + ruleTypes?: string[] +) { + const leftRuleTypesList = left[1]; + const rightRuleTypesList = right[1]; + + const hasEnabledRuleTypeInListLeft = + leftRuleTypesList.find((ruleTypeItem) => ruleTypeItem.checkEnabledResult.isEnabled) !== + undefined; + + const hasEnabledRuleTypeInListRight = + rightRuleTypesList.find((ruleTypeItem) => ruleTypeItem.checkEnabledResult.isEnabled) !== + undefined; + + if (hasEnabledRuleTypeInListLeft && !hasEnabledRuleTypeInListRight) { + return -1; + } + if (!hasEnabledRuleTypeInListLeft && hasEnabledRuleTypeInListRight) { + return 1; + } + + return ruleTypes + ? ruleTypes.findIndex((frtA) => leftRuleTypesList.some((aRuleType) => aRuleType.id === frtA)) - + ruleTypes.findIndex((frtB) => rightRuleTypesList.some((bRuleType) => bRuleType.id === frtB)) + : 0; +} + export function ruleTypeCompare( a: { id: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 777639d96230..dede9c80d87c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -49,6 +49,7 @@ const RuleAdd = ({ initialValues, reloadRules, onSave, + hideGrouping, hideInterval, metadata: initialMetadata, filteredRuleTypes, @@ -286,6 +287,7 @@ const RuleAdd = ({ ruleTypeRegistry={ruleTypeRegistry} metadata={metadata} filteredRuleTypes={filteredRuleTypes} + hideGrouping={hideGrouping} hideInterval={hideInterval} onChangeMetaData={onChangeMetaData} setConsumer={setSelectedConsumer} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index 88e3639c2dbc..d47b2e62e476 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -72,7 +72,11 @@ import { useKibana } from '../../../common/lib/kibana'; import { recoveredActionGroupMessage, summaryMessage } from '../../constants'; import { IsEnabledResult, IsDisabledResult } from '../../lib/check_rule_type_enabled'; import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled'; -import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compare'; +import { + ruleTypeCompare, + ruleTypeGroupCompare, + ruleTypeUngroupedCompare, +} from '../../lib/rule_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; import { SectionLoading } from '../../components/section_loading'; @@ -137,6 +141,7 @@ interface RuleFormProps> { setConsumer?: (consumer: RuleCreationValidConsumer | null) => void; metadata?: MetaData; filteredRuleTypes?: string[]; + hideGrouping?: boolean; hideInterval?: boolean; connectorFeatureId?: string; validConsumers?: RuleCreationValidConsumer[]; @@ -159,6 +164,7 @@ export const RuleForm = ({ actionTypeRegistry, metadata, filteredRuleTypes: ruleTypeToFilter, + hideGrouping = false, hideInterval, connectorFeatureId = AlertingConnectorFeatureId, validConsumers, @@ -457,85 +463,93 @@ export const RuleForm = ({ {} ); - const ruleTypeNodes = Object.entries(ruleTypesByProducer) - .sort((a, b) => ruleTypeGroupCompare(a, b, solutions)) - .map(([solution, items], groupIndex) => ( - - - - - - {(kibanaFeatures - ? getProducerFeatureName(solution, kibanaFeatures) - : capitalize(solution)) ?? capitalize(solution)} - - - - - {items.length} - - - - - {items - .sort((a, b) => ruleTypeCompare(a, b)) - .map((item, index) => { - const ruleTypeListItemHtml = ( - - {item.name} - -

{item.ruleTypeItem.description}

-
-
- ); - return ( - - {ruleTypeListItemHtml} - - ) - } - isDisabled={!item.checkEnabledResult.isEnabled} - onClick={() => { - setRuleProperty('ruleTypeId', item.id); - setRuleTypeModel(item.ruleTypeItem); - setActions([]); - setRuleProperty('params', {}); - if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { - setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); - } + const sortedRuleTypeNodes = hideGrouping + ? Object.entries(ruleTypesByProducer).sort((a, b) => + ruleTypeUngroupedCompare(a, b, ruleTypeToFilter) + ) + : Object.entries(ruleTypesByProducer).sort((a, b) => ruleTypeGroupCompare(a, b, solutions)); - if (useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(item.id)) { - setConsumer(solution as RuleCreationValidConsumer); - } - }} - /> - ); - })} -
- -
- )); + const ruleTypeNodes = sortedRuleTypeNodes.map(([solution, items], groupIndex) => ( + + {!hideGrouping && ( + <> + + + + + {(kibanaFeatures + ? getProducerFeatureName(solution, kibanaFeatures) + : capitalize(solution)) ?? capitalize(solution)} + + + + + {items.length} + + + + + )} + + {items + .sort((a, b) => ruleTypeCompare(a, b)) + .map((item, index) => { + const ruleTypeListItemHtml = ( + + {item.name} + +

{item.ruleTypeItem.description}

+
+
+ ); + return ( + + {ruleTypeListItemHtml} + + ) + } + isDisabled={!item.checkEnabledResult.isEnabled} + onClick={() => { + setRuleProperty('ruleTypeId', item.id); + setRuleTypeModel(item.ruleTypeItem); + setActions([]); + setRuleProperty('params', {}); + if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { + setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); + } + + if (useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(item.id)) { + setConsumer(solution as RuleCreationValidConsumer); + } + }} + /> + ); + })} +
+ +
+ )); const labelForRuleChecked = [ i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 7716aef34040..460a621918cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -453,6 +453,7 @@ export interface RuleAddProps> { initialValues?: Partial; /** @deprecated use `onSave` as a callback after an alert is saved*/ reloadRules?: () => Promise; + hideGrouping?: boolean; hideInterval?: boolean; onSave?: (metadata?: MetaData) => Promise; metadata?: MetaData;