-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[ResponseOps][Alerts] Implement alerts filters form #214982
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
Changes from 4 commits
9d8983a
854495d
9982fb6
7741bf2
e3d8a54
1d8fca4
048e51d
89856ca
8b1029f
989472f
4cfc7a1
7d77719
0138a34
1dc216f
c0cdc11
6991785
f6f2324
9823b39
6dac42e
8786ed0
b439e12
265436e
b23bf48
1990bab
3c8d622
a2e5746
7aa5569
108db58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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,93 @@ | ||
| /* | ||
| * 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'; | ||
| import { ALERT_RULE_TAGS } from '@kbn/rule-data-utils'; | ||
|
|
||
| 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={{ http, notifications, ruleTypeIds: ['.es-query'] }}> | ||
| <AlertsFilterByRuleTags value={[]} onChange={jest.fn()} /> | ||
| </AlertsFiltersFormContextProvider> | ||
| ); | ||
| await userEvent.click(screen.getByRole('combobox')); | ||
| expect(screen.getByText('tag1')).toBeInTheDocument(); | ||
| expect(screen.getByText('tag2')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| 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={{ http, notifications, ruleTypeIds: ['.es-query'] }}> | ||
| <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={{ http, notifications, ruleTypeIds: ['.es-query'] }}> | ||
| <AlertsFilterByRuleTags value={[]} onChange={jest.fn()} /> | ||
| </AlertsFiltersFormContextProvider> | ||
| ); | ||
| const comboboxInput = screen.getByTestId('comboBoxSearchInput'); | ||
| expect(comboboxInput).toHaveAttribute('aria-invalid', 'true'); | ||
| expect(comboboxInput).toHaveAttribute('disabled'); | ||
|
umbopepato marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| it('should have the correct metadata', () => { | ||
| expect(filterMetadata.id).toEqual('ruleTags'); | ||
|
js-jankisalvi marked this conversation as resolved.
Outdated
|
||
| expect(filterMetadata.component).toEqual(AlertsFilterByRuleTags); | ||
| expect(filterMetadata.isEmpty(undefined)).toEqual(true); | ||
| expect(filterMetadata.isEmpty([])).toEqual(true); | ||
| expect(filterMetadata.isEmpty(['test-tag'])).toEqual(false); | ||
| expect(filterMetadata.toEsQuery(['test-tag'])).toEqual({ | ||
| terms: { | ||
| [ALERT_RULE_TAGS]: ['test-tag'], | ||
| }, | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,99 @@ | ||||||
| /* | ||||||
| * 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 { i18n } from '@kbn/i18n'; | ||||||
| import { EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box'; | ||||||
| import { ALERT_RULE_TAGS } from '@kbn/rule-data-utils'; | ||||||
| import { RULE_TAGS_FILTER_LABEL } from '../translations'; | ||||||
| import { useAlertsFiltersFormContext } from '../contexts/alerts_filters_form_context'; | ||||||
| import { AlertsFilterComponentType, AlertsFilterMetadata } from '../types'; | ||||||
|
|
||||||
| /** | ||||||
| * Filters by one or more rule tags | ||||||
| */ | ||||||
| export const AlertsFilterByRuleTags: AlertsFilterComponentType<string[]> = ({ | ||||||
| value, | ||||||
| onChange, | ||||||
| isDisabled = false, | ||||||
| }) => { | ||||||
| const { | ||||||
| ruleTypeIds, | ||||||
| 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={i18n.translate('alertsFiltersForm.ruleTags.errorDescription', { | ||||||
| defaultMessage: 'Cannot load available rule tags', | ||||||
| })} | ||||||
| fullWidth | ||||||
| > | ||||||
| <EuiComboBox | ||||||
| isClearable | ||||||
| isLoading={isLoading} | ||||||
| isDisabled={isDisabled || isError} | ||||||
| isInvalid={isError} | ||||||
| options={options} | ||||||
| selectedOptions={selectedOptions} | ||||||
| onChange={onSelectedOptionsChange} | ||||||
| fullWidth | ||||||
| /> | ||||||
| </EuiFormRow> | ||||||
| ); | ||||||
| }; | ||||||
|
|
||||||
| export const filterMetadata: AlertsFilterMetadata<string[]> = { | ||||||
| id: 'ruleTags', | ||||||
| displayName: RULE_TAGS_FILTER_LABEL, | ||||||
| component: AlertsFilterByRuleTags, | ||||||
| isEmpty: (value?: string[]) => !value?.length, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes sorry!
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done ✅ |
||||||
| toEsQuery: (value: string[]) => { | ||||||
| return { | ||||||
| terms: { | ||||||
| [ALERT_RULE_TAGS]: value, | ||||||
| }, | ||||||
| }; | ||||||
| }, | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /* | ||
| * 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'; | ||
| import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils'; | ||
|
|
||
| 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={{ http, notifications, ruleTypeIds }}> | ||
| <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 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={{ http, notifications, ruleTypeIds: ['.es-query'] }}> | ||
| <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={{ http, notifications, ruleTypeIds }}> | ||
| <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={{ http, notifications, ruleTypeIds }}> | ||
| <AlertsFilterByRuleTypes value={[]} onChange={jest.fn()} /> | ||
| </AlertsFiltersFormContextProvider> | ||
| ); | ||
| const comboboxInput = screen.getByTestId('comboBoxSearchInput'); | ||
| expect(comboboxInput).toHaveAttribute('aria-invalid', 'true'); | ||
| expect(comboboxInput).toHaveAttribute('disabled'); | ||
| }); | ||
|
|
||
| it('should have the correct metadata', () => { | ||
| expect(filterMetadata.id).toEqual('ruleTypes'); | ||
| expect(filterMetadata.component).toEqual(AlertsFilterByRuleTypes); | ||
| expect(filterMetadata.isEmpty(undefined)).toEqual(true); | ||
| expect(filterMetadata.isEmpty([])).toEqual(true); | ||
| expect(filterMetadata.isEmpty(['test-type'])).toEqual(false); | ||
| expect(filterMetadata.toEsQuery(['test-type'])).toEqual({ | ||
| terms: { | ||
| [ALERT_RULE_TYPE_ID]: ['test-type'], | ||
| }, | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.