Skip to content

Commit

Permalink
[RAM] Fix bulk snooze select all filter not matching rules list filter (
Browse files Browse the repository at this point in the history
#141996)

* Select all filter now correctly match the rules list filter

* Improve filter kuery node logic

* unit tests

Co-authored-by: Kibana Machine <[email protected]>
(cherry picked from commit ace4f42)
  • Loading branch information
JiaweiWu committed Sep 28, 2022
1 parent bb90d2f commit 5b47ace
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 54 deletions.
11 changes: 2 additions & 9 deletions x-pack/plugins/alerting/server/rules_client/rules_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from 'lodash';
import { i18n } from '@kbn/i18n';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { fromKueryExpression, KueryNode, nodeBuilder } from '@kbn/es-query';
import { KueryNode, nodeBuilder } from '@kbn/es-query';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
Logger,
Expand Down Expand Up @@ -1569,14 +1569,7 @@ export class RulesClient {
);
}

let qNodeQueryFilter: null | KueryNode;
if (!queryFilter) {
qNodeQueryFilter = null;
} else if (typeof queryFilter === 'string') {
qNodeQueryFilter = fromKueryExpression(queryFilter);
} else {
qNodeQueryFilter = queryFilter;
}
const qNodeQueryFilter = buildKueryNodeFilter(queryFilter);

const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter;
let authorizationTuple;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { EuiConfirmModal } from '@elastic/eui';
import { KueryNode } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState, useMemo } from 'react';
import { HttpSetup } from '@kbn/core/public';
Expand All @@ -25,15 +26,15 @@ export const UpdateApiKeyModalConfirmation = ({
}: {
onCancel: () => void;
idsToUpdate: string[];
idsToUpdateFilter?: string;
idsToUpdateFilter?: KueryNode | null | undefined;
numberOfSelectedRules?: number;
apiUpdateApiKeyCall: ({
ids,
http,
filter,
}: {
ids?: string[];
filter?: string;
filter?: KueryNode | null | undefined;
http: HttpSetup;
}) => Promise<BulkEditResponse>;
setIsLoadingState: (isLoading: boolean) => void;
Expand All @@ -50,15 +51,15 @@ export const UpdateApiKeyModalConfirmation = ({
const { showToast } = useBulkEditResponse({ onSearchPopulate });

useEffect(() => {
if (idsToUpdateFilter) {
if (typeof idsToUpdateFilter !== 'undefined') {
setUpdateModalVisibility(true);
} else {
setUpdateModalVisibility(idsToUpdate.length > 0);
}
}, [idsToUpdate, idsToUpdateFilter]);

const numberOfIdsToUpdate = useMemo(() => {
if (idsToUpdateFilter) {
if (typeof idsToUpdateFilter !== 'undefined') {
return numberOfSelectedRules;
}
return idsToUpdate.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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 { renderHook, act } from '@testing-library/react-hooks';
import { useBulkEditSelect } from './use_bulk_edit_select';
import { RuleTableItem } from '../../types';

const items = [
{
id: '1',
isEditable: true,
},
{
id: '2',
isEditable: true,
},
{
id: '3',
isEditable: true,
},
{
id: '4',
isEditable: true,
},
] as RuleTableItem[];

describe('useBulkEditSelectTest', () => {
it('getFilter should return null when nothing is selected', async () => {
const { result } = renderHook(() =>
useBulkEditSelect({
items,
totalItemCount: 4,
})
);

expect(result.current.getFilter()).toEqual(null);
});

it('getFilter should return rule list filter when nothing is selected', async () => {
const { result } = renderHook(() =>
useBulkEditSelect({
items,
totalItemCount: 4,
tagsFilter: ['test: 123'],
searchText: 'rules*',
})
);

expect(result.current.getFilter()?.arguments.length).toEqual(2);
});

it('getFilter should return rule list filter when something is selected', async () => {
const { result } = renderHook(() =>
useBulkEditSelect({
items,
totalItemCount: 4,
tagsFilter: ['test: 123'],
searchText: 'rules*',
})
);

act(() => {
result.current.onSelectRow(items[0]);
});

expect(result.current.getFilter()?.arguments.length).toEqual(2);
expect([...result.current.selectedIds]).toEqual([items[0].id]);
});

it('getFilter should return null when selecting all', async () => {
const { result } = renderHook(() =>
useBulkEditSelect({
items,
totalItemCount: 4,
})
);

act(() => {
result.current.onSelectAll();
});

expect(result.current.getFilter()).toEqual(null);
});

it('getFilter should return rule list filter when selecting all', async () => {
const { result } = renderHook(() =>
useBulkEditSelect({
items,
totalItemCount: 4,
tagsFilter: ['test: 123'],
searchText: 'rules*',
})
);

act(() => {
result.current.onSelectAll();
});

expect(result.current.getFilter()?.arguments.length).toEqual(2);
});

it('getFilter should return rule list filter and exclude ids when selecting all with excluded ids', async () => {
const { result } = renderHook(() =>
useBulkEditSelect({
items,
totalItemCount: 4,
tagsFilter: ['test: 123'],
searchText: 'rules*',
})
);

act(() => {
result.current.onSelectAll();
result.current.onSelectRow(items[0]);
});

expect(result.current.getFilter()?.arguments.length).toEqual(2);
expect(result.current.getFilter()?.arguments[1].arguments[0].arguments).toEqual([
expect.objectContaining({
value: 'alert.id',
}),
expect.objectContaining({
value: 'alert:1',
}),
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* 2.0.
*/
import { useReducer, useMemo, useCallback } from 'react';
import { RuleTableItem } from '../../types';
import { fromKueryExpression, nodeBuilder } from '@kbn/es-query';
import { mapFiltersToKueryNode } from '../lib/rule_api/map_filters_to_kuery_node';
import { RuleTableItem, RuleStatus } from '../../types';

interface BulkEditSelectionState {
selectedIds: Set<string>;
Expand Down Expand Up @@ -71,9 +73,26 @@ const reducer = (state: BulkEditSelectionState, action: Action) => {
interface UseBulkEditSelectProps {
totalItemCount: number;
items: RuleTableItem[];
typesFilter?: string[];
actionTypesFilter?: string[];
tagsFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
searchText?: string;
}

export function useBulkEditSelect({ totalItemCount = 0, items = [] }: UseBulkEditSelectProps) {
export function useBulkEditSelect(props: UseBulkEditSelectProps) {
const {
totalItemCount = 0,
items = [],
typesFilter,
actionTypesFilter,
tagsFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
searchText,
} = props;

const [state, dispatch] = useReducer(reducer, initialState);

const itemIds = useMemo(() => {
Expand Down Expand Up @@ -161,18 +180,55 @@ export function useBulkEditSelect({ totalItemCount = 0, items = [] }: UseBulkEdi
dispatch({ type: ActionTypes.CLEAR_SELECTION });
}, []);

const getFilterKueryNode = useCallback(
(idsToExclude?: string[]) => {
const ruleFilterKueryNode = mapFiltersToKueryNode({
typesFilter,
actionTypesFilter,
tagsFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
searchText,
});

if (idsToExclude && idsToExclude.length) {
if (ruleFilterKueryNode) {
return nodeBuilder.and([
ruleFilterKueryNode,
fromKueryExpression(
`NOT (${idsToExclude.map((id) => `alert.id: "alert:${id}"`).join(' or ')})`
),
]);
}
}

return ruleFilterKueryNode;
},
[
typesFilter,
actionTypesFilter,
tagsFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
searchText,
]
);

const getFilter = useCallback(() => {
const { selectedIds, isAllSelected } = state;
const idsArray = [...selectedIds];

if (isAllSelected) {
// Select all but nothing is selected to exclude
if (idsArray.length === 0) {
return 'alert.id: *';
return getFilterKueryNode();
}
return `NOT (${idsArray.map((id) => `alert.id: "alert:${id}"`).join(' or ')})`;
// Select all, exclude certain alerts
return getFilterKueryNode(idsArray);
}
return '';
}, [state]);

return getFilterKueryNode();
}, [state, getFilterKueryNode]);

return useMemo(() => {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import { KueryNode } from '@kbn/es-query';
import { SnoozeSchedule, BulkEditResponse } from '../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';

Expand Down Expand Up @@ -37,7 +38,7 @@ export async function snoozeRule({

export interface BulkSnoozeRulesProps {
ids?: string[];
filter?: string;
filter?: KueryNode | null | undefined;
snoozeSchedule: SnoozeSchedule;
}

Expand All @@ -51,7 +52,7 @@ export function bulkSnoozeRules({
try {
body = JSON.stringify({
ids: ids?.length ? ids : undefined,
filter,
...(filter ? { filter: JSON.stringify(filter) } : {}),
operations: [
{
operation: 'set',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import { KueryNode } from '@kbn/es-query';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
import { BulkEditResponse } from '../../../types';

Expand All @@ -26,7 +27,7 @@ export async function unsnoozeRule({

export interface BulkUnsnoozeRulesProps {
ids?: string[];
filter?: string;
filter?: KueryNode | null | undefined;
scheduleIds?: string[];
}

Expand All @@ -40,7 +41,7 @@ export function bulkUnsnoozeRules({
try {
body = JSON.stringify({
ids: ids?.length ? ids : undefined,
filter,
...(filter ? { filter: JSON.stringify(filter) } : {}),
operations: [
{
operation: 'delete',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import { KueryNode } from '@kbn/es-query';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
import { BulkEditResponse } from '../../../types';

Expand All @@ -16,7 +17,7 @@ export async function updateAPIKey({ id, http }: { id: string; http: HttpSetup }

export interface BulkUpdateAPIKeyProps {
ids?: string[];
filter?: string;
filter?: KueryNode | null | undefined;
}

export function bulkUpdateAPIKey({
Expand All @@ -28,7 +29,7 @@ export function bulkUpdateAPIKey({
try {
body = JSON.stringify({
ids: ids?.length ? ids : undefined,
filter,
...(filter ? { filter: JSON.stringify(filter) } : {}),
operations: [
{
operation: 'set',
Expand Down
Loading

0 comments on commit 5b47ace

Please sign in to comment.