Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ examples/resizable_layout_examples @elastic/kibana-data-discovery
x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution
src/platform/packages/shared/response-ops/alerts-apis @elastic/response-ops
src/platform/packages/shared/response-ops/alerts-fields-browser @elastic/response-ops
src/platform/packages/shared/response-ops/alerts-filters-form @elastic/response-ops
src/platform/packages/shared/response-ops/alerts-table @elastic/response-ops
src/platform/packages/shared/response-ops/rule_form @elastic/response-ops
src/platform/packages/shared/response-ops/rule_params @elastic/response-ops
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@
"@kbn/resolver-test-plugin": "link:x-pack/test/plugin_functional/plugins/resolver_test",
"@kbn/response-ops-alerts-apis": "link:src/platform/packages/shared/response-ops/alerts-apis",
"@kbn/response-ops-alerts-fields-browser": "link:src/platform/packages/shared/response-ops/alerts-fields-browser",
"@kbn/response-ops-alerts-filters-form": "link:src/platform/packages/shared/response-ops/alerts-filters-form",
"@kbn/response-ops-alerts-table": "link:src/platform/packages/shared/response-ops/alerts-table",
"@kbn/response-ops-rule-form": "link:src/platform/packages/shared/response-ops/rule_form",
"@kbn/response-ops-rule-params": "link:src/platform/packages/shared/response-ops/rule_params",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/response-ops-alerts-filters-form

A form to create and edit boolean filter expressions for alert document search queries.
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { useGetRuleTagsQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_rule_tags_query';
import { AlertsFiltersFormContextProvider } from '../contexts/alerts_filters_form_context';
import { AlertsFilterByRuleTags, filterMetadata } from './alerts_filter_by_rule_tags';

const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
jest.mock('@kbn/response-ops-rules-apis/hooks/use_get_rule_tags_query');
const mockUseGetRuleTagsQuery = jest.mocked(useGetRuleTagsQuery);

const ruleTagsBaseQueryResult = {
hasNextPage: false,
fetchNextPage: jest.fn(),
refetch: jest.fn(),
};

describe('AlertsFilterByRuleTags', () => {
it('should show all available tags as options', async () => {
mockUseGetRuleTagsQuery.mockReturnValue({
tags: ['tag1', 'tag2'],
isLoading: false,
isError: false,
...ruleTagsBaseQueryResult,
});
render(
<AlertsFiltersFormContextProvider
value={{ ruleTypeIds: ['.es-query'], services: { http, notifications } }}
>
<AlertsFilterByRuleTags value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
await userEvent.click(screen.getByRole('combobox'));
expect(screen.getByText('tag1')).toBeInTheDocument();
expect(screen.getByText('tag2')).toBeInTheDocument();
});

it('should show the selected tag in the combobox', async () => {
mockUseGetRuleTagsQuery.mockReturnValue({
tags: ['tag1', 'tag2'],
isLoading: false,
isError: false,
...ruleTagsBaseQueryResult,
});
render(
<AlertsFiltersFormContextProvider
value={{ ruleTypeIds: ['.es-query'], services: { http, notifications } }}
>
<AlertsFilterByRuleTags value={['tag1']} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
const comboboxPills = screen.getAllByTestId('euiComboBoxPill');
expect(comboboxPills).toHaveLength(1);
expect(comboboxPills[0]).toHaveTextContent('tag1');
});

it('should set the combobox in loading mode while loading the available tags', async () => {
mockUseGetRuleTagsQuery.mockReturnValue({
tags: [],
isLoading: true,
isError: false,
...ruleTagsBaseQueryResult,
});
render(
<AlertsFiltersFormContextProvider
value={{ ruleTypeIds: ['.es-query'], services: { http, notifications } }}
>
<AlertsFilterByRuleTags value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

it('should disable the combobox when the tags query fails', async () => {
mockUseGetRuleTagsQuery.mockReturnValue({
tags: [],
isLoading: false,
isError: true,
...ruleTagsBaseQueryResult,
});
render(
<AlertsFiltersFormContextProvider
value={{ ruleTypeIds: ['.es-query'], services: { http, notifications } }}
>
<AlertsFilterByRuleTags value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
const comboboxInput = screen.getByTestId('comboBoxSearchInput');
expect(comboboxInput).toHaveAttribute('aria-invalid', 'true');
expect(comboboxInput).toBeDisabled();
});

describe('filterMetadata', () => {
it('should have the correct type id and component', () => {
expect(filterMetadata.id).toEqual('ruleTags');
expect(filterMetadata.component).toEqual(AlertsFilterByRuleTags);
});

describe('isEmpty', () => {
it.each([undefined, null, []])('should return false for %s', (value) => {
expect(
filterMetadata.isEmpty(value as Parameters<typeof filterMetadata.isEmpty>[0])
).toEqual(true);
});

it('should return true for non-empty values', () => {
expect(filterMetadata.isEmpty(['test-tag'])).toEqual(false);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { useGetRuleTagsQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_rule_tags_query';
import React, { useCallback, useMemo } from 'react';
import { EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box';
import {
RULE_TAGS_FILTER_LABEL,
RULE_TAGS_FILTER_NO_OPTIONS_PLACEHOLDER,
RULE_TAGS_FILTER_PLACEHOLDER,
RULE_TAGS_LOAD_ERROR_MESSAGE,
} from '../translations';
import { useAlertsFiltersFormContext } from '../contexts/alerts_filters_form_context';
import { AlertsFilterComponentType, AlertsFilterMetadata } from '../types';

export const AlertsFilterByRuleTags: AlertsFilterComponentType<string[]> = ({
value,
onChange,
isDisabled = false,
}) => {
const {
ruleTypeIds,
services: {
http,
notifications: { toasts },
},
} = useAlertsFiltersFormContext();

const { tags, isLoading, isError } = useGetRuleTagsQuery({
enabled: true,
perPage: 10000,
// Only search tags from allowed rule type ids
ruleTypeIds,
http,
toasts,
});

const options = useMemo<Array<EuiComboBoxOptionOption<string>>>(
() =>
tags.map((tag) => ({
label: tag,
})),
[tags]
);

const selectedOptions = useMemo(
() => options.filter(({ label }) => value?.includes(label)),
[options, value]
);

const onSelectedOptionsChange = useCallback<NonNullable<EuiComboBoxProps<string>['onChange']>>(
(newOptions) => {
onChange?.(newOptions.map(({ label }) => label));
},
[onChange]
);

return (
<EuiFormRow
label={RULE_TAGS_FILTER_LABEL}
isDisabled={isDisabled || isError}
isInvalid={isError}
error={RULE_TAGS_LOAD_ERROR_MESSAGE}
fullWidth
>
<EuiComboBox
isClearable
isLoading={isLoading}
isDisabled={isDisabled || isError || !options.length}
isInvalid={isError}
options={options}
selectedOptions={selectedOptions}
onChange={onSelectedOptionsChange}
placeholder={
!options.length ? RULE_TAGS_FILTER_NO_OPTIONS_PLACEHOLDER : RULE_TAGS_FILTER_PLACEHOLDER
}
fullWidth
/>
</EuiFormRow>
);
};

export const filterMetadata = {
id: 'ruleTags',
displayName: RULE_TAGS_FILTER_LABEL,
component: AlertsFilterByRuleTags,
isEmpty: (value?: string[]) => !Boolean(value?.length),
} as const satisfies AlertsFilterMetadata<string[]>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { useGetInternalRuleTypesQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query';
import { AlertsFiltersFormContextProvider } from '../contexts/alerts_filters_form_context';
import { AlertsFilterByRuleTypes } from './alerts_filter_by_rule_types';
import { InternalRuleType } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types';
import { filterMetadata } from './alerts_filter_by_rule_types';

const http = httpServiceMock.createStartContract();
const notifications = notificationServiceMock.createStartContract();
jest.mock('@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query');
const mockUseGetInternalRuleTypesQuery = useGetInternalRuleTypesQuery as jest.Mock;

const ruleTypeIds = ['.es-query', '.index-threshold'];

describe('AlertsFilterByRuleTypes', () => {
it('should show all available types as options', async () => {
mockUseGetInternalRuleTypesQuery.mockReturnValue({
data: [
{ id: '.es-query', name: 'Elasticsearch Query' },
{ id: '.index-threshold', name: 'Index threshold' },
] as InternalRuleType[],
isLoading: false,
isError: false,
});
render(
<AlertsFiltersFormContextProvider value={{ ruleTypeIds, services: { http, notifications } }}>
<AlertsFilterByRuleTypes value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
await userEvent.click(screen.getByRole('combobox'));
expect(screen.getByText('Elasticsearch Query')).toBeInTheDocument();
expect(screen.getByText('Index threshold')).toBeInTheDocument();
});

it('should show the selected type in the combobox', async () => {
mockUseGetInternalRuleTypesQuery.mockReturnValue({
data: [
{ id: '.es-query', name: 'Elasticsearch Query' },
{ id: '.index-threshold', name: 'Index threshold' },
] as InternalRuleType[],
isLoading: false,
isError: false,
});
render(
<AlertsFiltersFormContextProvider value={{ ruleTypeIds, services: { http, notifications } }}>
<AlertsFilterByRuleTypes value={['.es-query']} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
const comboboxPills = screen.getAllByTestId('euiComboBoxPill');
expect(comboboxPills).toHaveLength(1);
expect(comboboxPills[0]).toHaveTextContent('Elasticsearch Query');
});

it('should filter available types according to the provided ruleTypeIds', async () => {
mockUseGetInternalRuleTypesQuery.mockReturnValue({
data: [
{ id: '.es-query', name: 'Elasticsearch Query' },
{ id: '.index-threshold', name: 'Index threshold' },
] as InternalRuleType[],
isLoading: false,
isError: false,
});
render(
<AlertsFiltersFormContextProvider
value={{ ruleTypeIds: ['.es-query'], services: { http, notifications } }}
>
<AlertsFilterByRuleTypes value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
await userEvent.click(screen.getByRole('combobox'));
expect(screen.getByText('Elasticsearch Query')).toBeInTheDocument();
expect(screen.queryByText('Index threshold')).not.toBeInTheDocument();
});

it('should set the combobox in loading mode while loading the available types', async () => {
mockUseGetInternalRuleTypesQuery.mockReturnValue({
data: [],
isLoading: true,
isError: false,
});
render(
<AlertsFiltersFormContextProvider value={{ ruleTypeIds, services: { http, notifications } }}>
<AlertsFilterByRuleTypes value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

it('should disable the combobox when the types query fails', async () => {
mockUseGetInternalRuleTypesQuery.mockReturnValue({
types: [],
isLoading: false,
isError: true,
});
render(
<AlertsFiltersFormContextProvider value={{ ruleTypeIds, services: { http, notifications } }}>
<AlertsFilterByRuleTypes value={[]} onChange={jest.fn()} />
</AlertsFiltersFormContextProvider>
);
const comboboxInput = screen.getByTestId('comboBoxSearchInput');
expect(comboboxInput).toHaveAttribute('aria-invalid', 'true');
expect(comboboxInput).toHaveAttribute('disabled');
});

describe('filterMetadata', () => {
it('should have the correct type id and component', () => {
expect(filterMetadata.id).toEqual('ruleTypes');
expect(filterMetadata.component).toEqual(AlertsFilterByRuleTypes);
});

describe('isEmpty', () => {
it.each([undefined, null, []])('should return false for %s', (value) => {
expect(
filterMetadata.isEmpty(value as Parameters<typeof filterMetadata.isEmpty>[0])
).toEqual(true);
});

it('should return true for non-empty values', () => {
expect(filterMetadata.isEmpty(['test-type'])).toEqual(false);
});
});
});
});
Loading