Single list of rule types in observability without grouping (#166772)

Closes #166136

## Summary

This PR removes the groups in the rules list in the observability.

|With disabled rule|Without disabled rule|
|---|---|

|![image](aa89d441-6bbd-46d1-8753-bcddcd03c518)|

## 🧪 How to test
- Check the rule type list in observability > rules page
- Check the filter works as before
This commit is contained in:
Maryam Saeidi 2023-10-02 10:13:35 +02:00 committed by GitHub
parent c1aabee73f
commit 566e086963
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 268 additions and 116 deletions

View file

@ -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,

View file

@ -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,
});
}

View file

@ -66,5 +66,6 @@ export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel<Inve
defaultRecoveryMessage: inventoryDefaultRecoveryMessage,
requiresAppContext: false,
format: formatReason,
priority: 20,
};
}

View file

@ -72,5 +72,6 @@ export function createLogThresholdRuleType(
defaultRecoveryMessage: logThresholdDefaultRecoveryMessage,
requiresAppContext: false,
format: createRuleFormatter(logsLocator),
priority: 30,
};
}

View file

@ -67,5 +67,6 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel<Metr
requiresAppContext: false,
format: formatReason,
alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')),
priority: 10,
};
}

View file

@ -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(<RulesPage />);
return render(
<IntlProvider locale="en">
<RulesPage />
</IntlProvider>
);
}
it('should render a page template', async () => {

View file

@ -150,7 +150,8 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) {
setRefresh(new Date());
return Promise.resolve();
}}
useRuleProducer={true}
hideGrouping
useRuleProducer
/>
)}
</ObservabilityPageTemplate>

View file

@ -21,21 +21,27 @@ export type ObservabilityRuleTypeFormatter = (options: {
export interface ObservabilityRuleTypeModel<Params extends RuleTypeParams = RuleTypeParams>
extends RuleTypeModel<Params> {
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<any>) => {
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),
};
}

View file

@ -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,
});
}
};

View file

@ -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;

View file

@ -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<string, string> | 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;

View file

@ -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}

View file

@ -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<MetaData = Record<string, any>> {
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,10 +463,16 @@ export const RuleForm = ({
{}
);
const ruleTypeNodes = Object.entries(ruleTypesByProducer)
.sort((a, b) => ruleTypeGroupCompare(a, b, solutions))
.map(([solution, items], groupIndex) => (
const sortedRuleTypeNodes = hideGrouping
? Object.entries(ruleTypesByProducer).sort((a, b) =>
ruleTypeUngroupedCompare(a, b, ruleTypeToFilter)
)
: Object.entries(ruleTypesByProducer).sort((a, b) => ruleTypeGroupCompare(a, b, solutions));
const ruleTypeNodes = sortedRuleTypeNodes.map(([solution, items], groupIndex) => (
<Fragment key={`group${groupIndex}`}>
{!hideGrouping && (
<>
<EuiFlexGroup
gutterSize="none"
alignItems="center"
@ -484,6 +496,8 @@ export const RuleForm = ({
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule size="full" margin="xs" />
</>
)}
<EuiListGroup flush={true} gutterSize="m" size="m" maxWidth={false}>
{items
.sort((a, b) => ruleTypeCompare(a, b))
@ -533,7 +547,7 @@ export const RuleForm = ({
);
})}
</EuiListGroup>
<EuiSpacer />
<EuiSpacer size="m" />
</Fragment>
));

View file

@ -453,6 +453,7 @@ export interface RuleAddProps<MetaData = Record<string, any>> {
initialValues?: Partial<Rule>;
/** @deprecated use `onSave` as a callback after an alert is saved*/
reloadRules?: () => Promise<void>;
hideGrouping?: boolean;
hideInterval?: boolean;
onSave?: (metadata?: MetaData) => Promise<void>;
metadata?: MetaData;