Skip to content

Commit

Permalink
[RAM] Allow wildcard search on rule's name and tags (#136312)
Browse files Browse the repository at this point in the history
* bring wildcard search for rules page

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* convert test + add few

* fix unit test

* to minimyze bundle

* miss an import to be specific

* remove kbn query to avoid bundle size to grow

* metrics II

* fix import

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
XavierM and kibanamachine committed Jul 20, 2022
1 parent 48b3894 commit d85438f
Show file tree
Hide file tree
Showing 20 changed files with 710 additions and 151 deletions.
@@ -0,0 +1,37 @@
/*
* 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 { fromKueryExpression } from '@kbn/es-query';
import { buildKueryNodeFilter } from './build_kuery_node_filter';

describe('buildKueryNodeFilter', () => {
test('should convert KQL string into Kuery', () => {
expect(buildKueryNodeFilter('foo: "bar"')).toEqual({
arguments: [
{ type: 'literal', value: 'foo' },
{ type: 'literal', value: 'bar' },
{ type: 'literal', value: true },
],
function: 'is',
type: 'function',
});
});

test('should NOT do anything if filter is KueryNode', () => {
expect(buildKueryNodeFilter(fromKueryExpression('foo: "bar"'))).toEqual(
fromKueryExpression('foo: "bar"')
);
});

test('should return null if filter is not defined', () => {
expect(buildKueryNodeFilter()).toEqual(null);
});

test('should return null if filter is null', () => {
expect(buildKueryNodeFilter(null)).toEqual(null);
});
});
@@ -0,0 +1,25 @@
/*
* 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 { fromKueryExpression, KueryNode } from '@kbn/es-query';

export const buildKueryNodeFilter = (filter?: string | KueryNode | null): KueryNode | null => {
let optionsFilter: KueryNode | string | null = filter ?? null;
try {
if (optionsFilter != null && typeof optionsFilter === 'string') {
// FUTURE ENGINEER -> if I can parse it that mean it is a KueryNode or it is a string
optionsFilter = JSON.parse(optionsFilter);
}
} catch (e) {
optionsFilter = filter ?? null;
}
return optionsFilter
? typeof optionsFilter === 'string'
? fromKueryExpression(optionsFilter)
: optionsFilter
: null;
};
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/server/rules_client/lib/index.ts
Expand Up @@ -9,3 +9,4 @@ export { mapSortField } from './map_sort_field';
export { validateOperationOnAttributes } from './validate_attributes';
export { retryIfBulkEditConflicts } from './retry_if_bulk_edit_conflicts';
export { applyBulkEditOperation } from './apply_bulk_edit_operation';
export { buildKueryNodeFilter } from './build_kuery_node_filter';
16 changes: 10 additions & 6 deletions x-pack/plugins/alerting/server/rules_client/rules_client.ts
Expand Up @@ -88,6 +88,7 @@ import {
validateOperationOnAttributes,
retryIfBulkEditConflicts,
applyBulkEditOperation,
buildKueryNodeFilter,
} from './lib';
import { getRuleExecutionStatusPending } from '../lib/rule_execution_status';
import { Alert } from '../alert';
Expand Down Expand Up @@ -211,7 +212,7 @@ export interface FindOptions extends IndexType {
id: string;
};
fields?: string[];
filter?: string;
filter?: string | KueryNode;
}

export type BulkEditFields = keyof Pick<
Expand Down Expand Up @@ -280,7 +281,7 @@ export interface AggregateOptions extends IndexType {
type: string;
id: string;
};
filter?: string;
filter?: string | KueryNode;
}

interface IndexType {
Expand Down Expand Up @@ -895,7 +896,8 @@ export class RulesClient {
}

const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple;
const filterKueryNode = options.filter ? fromKueryExpression(options.filter) : null;

const filterKueryNode = buildKueryNodeFilter(options.filter);
let sortField = mapSortField(options.sortField);
if (excludeFromPublicApi) {
try {
Expand Down Expand Up @@ -1009,12 +1011,14 @@ export class RulesClient {
}

const { filter: authorizationFilter } = authorizationTuple;
const filterKueryNode = buildKueryNodeFilter(filter);

const resp = await this.unsecuredSavedObjectsClient.find<RawRule, RuleAggregation>({
...options,
filter:
(authorizationFilter && filter
? nodeBuilder.and([fromKueryExpression(filter), authorizationFilter as KueryNode])
: authorizationFilter) ?? filter,
authorizationFilter && filterKueryNode
? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
: authorizationFilter,
page: 1,
perPage: 0,
type: 'alert',
Expand Down
Expand Up @@ -18,6 +18,7 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
import { RegistryRuleType } from '../../rule_type_registry';
import { fromKueryExpression, nodeTypes } from '@kbn/es-query';

const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
Expand Down Expand Up @@ -213,14 +214,25 @@ describe('aggregate()', () => {
});

test('supports filters when aggregating', async () => {
const authFilter = fromKueryExpression(
'alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp'
);
authorization.getFindAuthorizationFilter.mockResolvedValue({
filter: authFilter,
ensureRuleTypeIsAuthorized() {},
});

const rulesClient = new RulesClient(rulesClientParams);
await rulesClient.aggregate({ options: { filter: 'someTerm' } });
await rulesClient.aggregate({ options: { filter: 'foo: someTerm' } });

expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toEqual([
{
fields: undefined,
filter: 'someTerm',
filter: nodeTypes.function.buildNode('and', [
fromKueryExpression('foo: someTerm'),
authFilter,
]),
page: 1,
perPage: 0,
type: 'alert',
Expand Down
Expand Up @@ -18,17 +18,19 @@ const MOCK_AGGS = {
ruleTags: MOCK_TAGS,
};

jest.mock('../lib/rule_api', () => ({
loadRuleAggregations: jest.fn(),
jest.mock('../lib/rule_api/aggregate_kuery_filter', () => ({
loadRuleAggregationsWithKueryFilter: jest.fn(),
}));

const { loadRuleAggregations } = jest.requireMock('../lib/rule_api');
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
'../lib/rule_api/aggregate_kuery_filter'
);

const onError = jest.fn();

describe('useLoadRuleAggregations', () => {
beforeEach(() => {
loadRuleAggregations.mockResolvedValue(MOCK_AGGS);
loadRuleAggregationsWithKueryFilter.mockResolvedValue(MOCK_AGGS);
jest.clearAllMocks();
});

Expand All @@ -54,7 +56,7 @@ describe('useLoadRuleAggregations', () => {
await waitForNextUpdate();
});

expect(loadRuleAggregations).toBeCalledWith(expect.objectContaining(params));
expect(loadRuleAggregationsWithKueryFilter).toBeCalledWith(expect.objectContaining(params));
expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus);
});

Expand All @@ -80,12 +82,12 @@ describe('useLoadRuleAggregations', () => {
await waitForNextUpdate();
});

expect(loadRuleAggregations).toBeCalledWith(expect.objectContaining(params));
expect(loadRuleAggregationsWithKueryFilter).toBeCalledWith(expect.objectContaining(params));
expect(result.current.rulesStatusesTotal).toEqual(MOCK_AGGS.ruleExecutionStatus);
});

it('should call onError if API fails', async () => {
loadRuleAggregations.mockRejectedValue('');
loadRuleAggregationsWithKueryFilter.mockRejectedValue('');
const params = {
searchText: '',
typesFilter: [],
Expand Down
Expand Up @@ -8,7 +8,8 @@
import { i18n } from '@kbn/i18n';
import { useState, useCallback, useMemo } from 'react';
import { RuleExecutionStatusValues } from '@kbn/alerting-plugin/common';
import { loadRuleAggregations, LoadRuleAggregationsProps } from '../lib/rule_api';
import type { LoadRuleAggregationsProps } from '../lib/rule_api';
import { loadRuleAggregationsWithKueryFilter } from '../lib/rule_api/aggregate_kuery_filter';
import { useKibana } from '../../common/lib/kibana';

type UseLoadRuleAggregationsProps = Omit<LoadRuleAggregationsProps, 'http'> & {
Expand Down Expand Up @@ -38,7 +39,7 @@ export function useLoadRuleAggregations({

const internalLoadRuleAggregations = useCallback(async () => {
try {
const rulesAggs = await loadRuleAggregations({
const rulesAggs = await loadRuleAggregationsWithKueryFilter({
http,
searchText,
typesFilter,
Expand Down
Expand Up @@ -13,11 +13,11 @@ import {
} from '@kbn/alerting-plugin/common';
import { RuleStatus } from '../../types';

jest.mock('../lib/rule_api', () => ({
loadRules: jest.fn(),
jest.mock('../lib/rule_api/rules_kuery_filter', () => ({
loadRulesWithKueryFilter: jest.fn(),
}));

const { loadRules } = jest.requireMock('../lib/rule_api');
const { loadRulesWithKueryFilter } = jest.requireMock('../lib/rule_api/rules_kuery_filter');

const onError = jest.fn();
const onPage = jest.fn();
Expand Down Expand Up @@ -233,7 +233,7 @@ const MOCK_RULE_DATA = {

describe('useLoadRules', () => {
beforeEach(() => {
loadRules.mockResolvedValue(MOCK_RULE_DATA);
loadRulesWithKueryFilter.mockResolvedValue(MOCK_RULE_DATA);
jest.clearAllMocks();
});

Expand Down Expand Up @@ -273,7 +273,7 @@ describe('useLoadRules', () => {
expect(result.current.rulesState.isLoading).toBeFalsy();

expect(onPage).toBeCalledTimes(0);
expect(loadRules).toBeCalledWith(expect.objectContaining(params));
expect(loadRulesWithKueryFilter).toBeCalledWith(expect.objectContaining(params));
expect(result.current.rulesState.data).toEqual(expect.arrayContaining(MOCK_RULE_DATA.data));
expect(result.current.rulesState.totalItemCount).toEqual(MOCK_RULE_DATA.total);
});
Expand Down Expand Up @@ -305,11 +305,11 @@ describe('useLoadRules', () => {
await waitForNextUpdate();
});

expect(loadRules).toBeCalledWith(expect.objectContaining(params));
expect(loadRulesWithKueryFilter).toBeCalledWith(expect.objectContaining(params));
});

it('should reset the page if the data is fetched while paged', async () => {
loadRules.mockResolvedValue({
loadRulesWithKueryFilter.mockResolvedValue({
...MOCK_RULE_DATA,
data: [],
});
Expand Down Expand Up @@ -347,7 +347,7 @@ describe('useLoadRules', () => {
});

it('should call onError if API fails', async () => {
loadRules.mockRejectedValue('');
loadRulesWithKueryFilter.mockRejectedValue('');
const params = {
page: {
index: 0,
Expand Down Expand Up @@ -378,7 +378,7 @@ describe('useLoadRules', () => {

describe('No data', () => {
it('noData should be true, if there is no Filter and no rules', async () => {
loadRules.mockResolvedValue({ ...MOCK_RULE_DATA, data: [] });
loadRulesWithKueryFilter.mockResolvedValue({ ...MOCK_RULE_DATA, data: [] });
const params = {
page: {
index: 0,
Expand Down Expand Up @@ -411,7 +411,7 @@ describe('useLoadRules', () => {
});

it('noData should be false, if there is rule types filter and no rules', async () => {
loadRules.mockResolvedValue({ ...MOCK_RULE_DATA, data: [] });
loadRulesWithKueryFilter.mockResolvedValue({ ...MOCK_RULE_DATA, data: [] });
const params = {
page: {
index: 0,
Expand Down Expand Up @@ -444,7 +444,7 @@ describe('useLoadRules', () => {
});

it('noData should be true, if there is rule types filter and no rules with hasDefaultRuleTypesFiltersOn = true', async () => {
loadRules.mockResolvedValue({ ...MOCK_RULE_DATA, data: [] });
loadRulesWithKueryFilter.mockResolvedValue({ ...MOCK_RULE_DATA, data: [] });
const params = {
page: {
index: 0,
Expand Down
Expand Up @@ -8,7 +8,8 @@ import { useMemo, useCallback, useReducer } from 'react';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { Rule, Pagination } from '../../types';
import { loadRules, LoadRulesProps } from '../lib/rule_api';
import type { LoadRulesProps } from '../lib/rule_api';
import { loadRulesWithKueryFilter } from '../lib/rule_api/rules_kuery_filter';
import { useKibana } from '../../common/lib/kibana';

interface RuleState {
Expand Down Expand Up @@ -112,7 +113,7 @@ export function useLoadRules({
dispatch({ type: ActionTypes.SET_LOADING, payload: true });

try {
const rulesResponse = await loadRules({
const rulesResponse = await loadRulesWithKueryFilter({
http,
page,
searchText,
Expand Down
Expand Up @@ -5,36 +5,16 @@
* 2.0.
*/
import { HttpSetup } from '@kbn/core/public';
import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common';
import { RuleAggregations, RuleStatus } from '../../../types';
import { AsApiContract } from '@kbn/actions-plugin/common';
import { RuleAggregations } from '../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
import { mapFiltersToKql } from './map_filters_to_kql';

export interface RuleTagsAggregations {
ruleTags: string[];
}

const rewriteBodyRes: RewriteRequestCase<RuleAggregations> = ({
rule_execution_status: ruleExecutionStatus,
rule_enabled_status: ruleEnabledStatus,
rule_muted_status: ruleMutedStatus,
rule_snoozed_status: ruleSnoozedStatus,
rule_tags: ruleTags,
...rest
}: any) => ({
...rest,
ruleExecutionStatus,
ruleEnabledStatus,
ruleMutedStatus,
ruleSnoozedStatus,
ruleTags,
});

const rewriteTagsBodyRes: RewriteRequestCase<RuleTagsAggregations> = ({
rule_tags: ruleTags,
}: any) => ({
ruleTags,
});
import {
LoadRuleAggregationsProps,
rewriteBodyRes,
rewriteTagsBodyRes,
RuleTagsAggregations,
} from './aggregate_helpers';

// TODO: https://github.com/elastic/kibana/issues/131682
export async function loadRuleTags({ http }: { http: HttpSetup }): Promise<RuleTagsAggregations> {
Expand All @@ -44,16 +24,6 @@ export async function loadRuleTags({ http }: { http: HttpSetup }): Promise<RuleT
return rewriteTagsBodyRes(res);
}

export interface LoadRuleAggregationsProps {
http: HttpSetup;
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
tagsFilter?: string[];
}

export async function loadRuleAggregations({
http,
searchText,
Expand Down

0 comments on commit d85438f

Please sign in to comment.