Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RAM] Add shareable rule status filter #130705

Merged
merged 14 commits into from
May 3, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function useFetchRules({
page,
searchText,
typesFilter: typesFilter.length > 0 ? typesFilter : OBSERVABILITY_RULE_TYPES,
ruleStatusesFilter: ruleLastResponseFilter,
ruleExecutionStatusesFilter: ruleLastResponseFilter,
sort,
});
setRulesState((oldState) => ({
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -29364,7 +29364,6 @@
"xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "Actif",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel": "Modifier le statut de la règle ou répéter",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "Erreur",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "Dernière réponse",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "Erreur de licence",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "Ok",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "En attente",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -29433,7 +29433,6 @@
"xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "アクティブ",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel": "ルールステータスまたはスヌーズを変更",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "エラー",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "前回の応答",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "ライセンスエラー",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "OK",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "保留中",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -29468,7 +29468,6 @@
"xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "活动",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel": "更改规则状态或暂停",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "错误",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "上次响应",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "许可证错误",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "确定",
"xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "待处理",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({
rulesListDatagrid: true,
internalAlertsTable: false,
internalShareableComponentsSandbox: false,
ruleStatusFilter: false,
rulesDetailLogs: true,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { RuleStatusFilterProps } from '../../../types';
import { getRuleStatusFilterLazy } from '../../../common/get_rule_status_filter';

export const RuleStatusFilterSandbox = () => {
const [selectedStatuses, setSelectedStatuses] = useState<
RuleStatusFilterProps['selectedStatuses']
>([]);

return (
<div style={{ flex: 1 }}>
{getRuleStatusFilterLazy({
selectedStatuses,
onChange: setSelectedStatuses,
})}
<div>Selected states: {JSON.stringify(selectedStatuses)}</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

import React from 'react';
import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox';
import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox';
import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox';

export const InternalShareableComponentsSandbox: React.FC<{}> = () => {
return (
<>
<RuleStatusDropdownSandbox />
<RuleStatusFilterSandbox />
<RuleTagBadgeSandbox />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,84 @@ describe('loadRuleAggregations', () => {
]
`);
});

test('should call aggregate API with ruleStatusesFilter', async () => {
const resolvedValue = {
rule_execution_status: {
ok: 4,
active: 2,
error: 1,
pending: 1,
unknown: 0,
},
};
http.get.mockResolvedValue(resolvedValue);

let result = await loadRuleAggregations({
http,
ruleStatusesFilter: ['enabled'],
});

expect(result).toEqual({
ruleExecutionStatus: {
ok: 4,
active: 2,
error: 1,
pending: 1,
unknown: 0,
},
});

expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"search": undefined,
"search_fields": undefined,
},
},
]
`);

result = await loadRuleAggregations({
http,
ruleStatusesFilter: ['enabled', 'snoozed'],
});

expect(http.get.mock.calls[1]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"search": undefined,
"search_fields": undefined,
},
},
]
`);

result = await loadRuleAggregations({
http,
ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'],
});

expect(http.get.mock.calls[1]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rules/_aggregate",
Object {
"query": Object {
"default_search_operator": "AND",
"filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)",
"search": undefined,
"search_fields": undefined,
},
},
]
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { HttpSetup } from '@kbn/core/public';
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
import { RuleAggregations } from '../../../types';
import { RuleAggregations, RuleStatus } from '../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
import { mapFiltersToKql } from './map_filters_to_kql';

Expand All @@ -29,15 +29,22 @@ export async function loadRuleAggregations({
searchText,
typesFilter,
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
}: {
http: HttpSetup;
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
ruleStatusesFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
}): Promise<RuleAggregations> {
const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter });
Copy link
Contributor

@mgiota mgiota Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JiaweiWu Out of curiosity I was wondering why the new ruleStatusesFilter is not included here. Is it because the options of the rule status filter is a static hardcoded list (enabled, disabled, snoozed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to filter the aggregate results as well? I suppose we do, I can add it

const filters = mapFiltersToKql({
typesFilter,
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
});
const res = await http.get<AsApiContract<RuleAggregations>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,62 @@ describe('mapFiltersToKql', () => {
]);
});

test('should handle ruleStatusesFilter', () => {
test('should handle ruleExecutionStatusesFilter', () => {
expect(
mapFiltersToKql({
ruleStatusesFilter: ['alert', 'statuses', 'filter'],
ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'],
})
).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']);
});

test('should handle ruleStatusesFilter', () => {
expect(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a test for just disabled too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, good idea!

mapFiltersToKql({
ruleStatusesFilter: ['enabled'],
})
).toEqual([
'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
]);

expect(
mapFiltersToKql({
ruleStatusesFilter: ['disabled'],
})
).toEqual([
'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
]);

expect(
mapFiltersToKql({
ruleStatusesFilter: ['snoozed'],
})
).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)']);

expect(
mapFiltersToKql({
ruleStatusesFilter: ['enabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
]);

expect(
mapFiltersToKql({
ruleStatusesFilter: ['disabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
]);

expect(
mapFiltersToKql({
ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'],
})
).toEqual([
'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
]);
});

test('should handle typesFilter and actionTypesFilter', () => {
expect(
mapFiltersToKql({
Expand All @@ -52,12 +100,12 @@ describe('mapFiltersToKql', () => {
]);
});

test('should handle typesFilter, actionTypesFilter and ruleStatusesFilter', () => {
test('should handle typesFilter, actionTypesFilter and ruleExecutionStatusesFilter', () => {
expect(
mapFiltersToKql({
typesFilter: ['type', 'filter'],
actionTypesFilter: ['action', 'types', 'filter'],
ruleStatusesFilter: ['alert', 'statuses', 'filter'],
ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'],
})
).toEqual([
'alert.attributes.alertTypeId:(type or filter)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,34 @@
* 2.0.
*/

import { RuleStatus } from '../../../types';

const getEnablementFilter = (ruleStatusFilter: RuleStatus[] = []) => {
const enablementFilters = ruleStatusFilter.reduce<string[]>((result, filter) => {
if (filter === 'enabled') {
return [...result, 'true'];
}
if (filter === 'disabled') {
return [...result, 'false'];
}
return result;
}, []);
return `alert.attributes.enabled:(${enablementFilters.join(' or ')})`;
};

export const mapFiltersToKql = ({
typesFilter,
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
}: {
typesFilter?: string[];
actionTypesFilter?: string[];
ruleStatusesFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
}): string[] => {
const filters = [];

if (typesFilter && typesFilter.length) {
filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`);
}
Expand All @@ -29,8 +47,27 @@ export const mapFiltersToKql = ({
].join('')
);
}
if (ruleExecutionStatusesFilter && ruleExecutionStatusesFilter.length) {
filters.push(
`alert.attributes.executionStatus.status:(${ruleExecutionStatusesFilter.join(' or ')})`
);
}

if (ruleStatusesFilter && ruleStatusesFilter.length) {
filters.push(`alert.attributes.executionStatus.status:(${ruleStatusesFilter.join(' or ')})`);
const enablementFilter = getEnablementFilter(ruleStatusesFilter);
const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)`;
const hasEnablement =
ruleStatusesFilter.includes('enabled') || ruleStatusesFilter.includes('disabled');
const hasSnoozed = ruleStatusesFilter.includes('snoozed');

if (hasEnablement && !hasSnoozed) {
filters.push(`${enablementFilter} and not ${snoozedFilter}`);
} else if (!hasEnablement && hasSnoozed) {
filters.push(snoozedFilter);
} else {
filters.push(`${enablementFilter} or ${snoozedFilter}`);
}
}

return filters;
};
Loading