diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 6c591d2145483..67d1a17b69ca6 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
diff --git a/package.json b/package.json
index bbdcb764a3548..470c10ce3eb1d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/README.md b/src/platform/packages/shared/response-ops/alerts-filters-form/README.md
new file mode 100644
index 0000000000000..7f3bb64d5a81d
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/README.md
@@ -0,0 +1,3 @@
+# @kbn/response-ops-alerts-filters-form
+
+A form to create and edit boolean filter expressions for alert document search queries.
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_tags.test.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_tags.test.tsx
new file mode 100644
index 0000000000000..086292d9dd6ac
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_tags.test.tsx
@@ -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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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[0])
+ ).toEqual(true);
+ });
+
+ it('should return true for non-empty values', () => {
+ expect(filterMetadata.isEmpty(['test-tag'])).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_tags.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_tags.tsx
new file mode 100644
index 0000000000000..07d0d7266a8b9
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_tags.tsx
@@ -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 = ({
+ 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>>(
+ () =>
+ tags.map((tag) => ({
+ label: tag,
+ })),
+ [tags]
+ );
+
+ const selectedOptions = useMemo(
+ () => options.filter(({ label }) => value?.includes(label)),
+ [options, value]
+ );
+
+ const onSelectedOptionsChange = useCallback['onChange']>>(
+ (newOptions) => {
+ onChange?.(newOptions.map(({ label }) => label));
+ },
+ [onChange]
+ );
+
+ return (
+
+
+
+ );
+};
+
+export const filterMetadata = {
+ id: 'ruleTags',
+ displayName: RULE_TAGS_FILTER_LABEL,
+ component: AlertsFilterByRuleTags,
+ isEmpty: (value?: string[]) => !Boolean(value?.length),
+} as const satisfies AlertsFilterMetadata;
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_types.test.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_types.test.tsx
new file mode 100644
index 0000000000000..3d9162bfccef5
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_types.test.tsx
@@ -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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ 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(
+
+
+
+ );
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
+ });
+
+ it('should disable the combobox when the types query fails', async () => {
+ mockUseGetInternalRuleTypesQuery.mockReturnValue({
+ types: [],
+ isLoading: false,
+ isError: true,
+ });
+ render(
+
+
+
+ );
+ 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[0])
+ ).toEqual(true);
+ });
+
+ it('should return true for non-empty values', () => {
+ expect(filterMetadata.isEmpty(['test-type'])).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_types.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_types.tsx
new file mode 100644
index 0000000000000..0eac1461f0969
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filter_by_rule_types.tsx
@@ -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 React, { useCallback, useMemo } from 'react';
+import { useGetInternalRuleTypesQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query';
+import { EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box';
+import { SetRequired } from 'type-fest';
+import { AlertsFilterComponentType, AlertsFilterMetadata } from '../types';
+import { useAlertsFiltersFormContext } from '../contexts/alerts_filters_form_context';
+import {
+ RULE_TYPES_FILTER_LABEL,
+ RULE_TYPES_FILTER_NO_OPTIONS_PLACEHOLDER,
+ RULE_TYPES_FILTER_PLACEHOLDER,
+ RULE_TYPES_LOAD_ERROR_MESSAGE,
+} from '../translations';
+
+export const AlertsFilterByRuleTypes: AlertsFilterComponentType = ({
+ value,
+ onChange,
+ isDisabled = false,
+}) => {
+ const {
+ ruleTypeIds: allowedRuleTypeIds,
+ services: { http },
+ } = useAlertsFiltersFormContext();
+
+ const {
+ data: ruleTypes,
+ isLoading,
+ isError,
+ } = useGetInternalRuleTypesQuery({
+ http,
+ });
+
+ const options = useMemo, 'value'>>>(
+ () =>
+ ruleTypes
+ ?.filter((ruleType) => allowedRuleTypeIds.includes(ruleType.id))
+ .map((ruleType) => ({
+ value: ruleType.id,
+ label: ruleType.name,
+ })) ?? [],
+ [allowedRuleTypeIds, ruleTypes]
+ );
+
+ const selectedOptions = useMemo(
+ () => options.filter((option) => value?.includes(option.value)),
+ [options, value]
+ );
+
+ const onSelectedOptionsChange = useCallback['onChange']>>(
+ (newOptions) => {
+ onChange?.(newOptions.map((option) => option.value!));
+ },
+ [onChange]
+ );
+
+ return (
+
+
+
+ );
+};
+
+export const filterMetadata = {
+ id: 'ruleTypes',
+ displayName: RULE_TYPES_FILTER_LABEL,
+ component: AlertsFilterByRuleTypes,
+ isEmpty: (value?: string[]) => !Boolean(value?.length),
+} as const satisfies AlertsFilterMetadata;
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form.test.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form.test.tsx
new file mode 100644
index 0000000000000..7436592225cb6
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form.test.tsx
@@ -0,0 +1,167 @@
+/*
+ * 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, { useState } from 'react';
+import { render, screen } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
+import { AlertsFiltersForm, AlertsFiltersFormProps } from './alerts_filters_form';
+import { AlertsFilter, AlertsFiltersExpression } from '../types';
+import {
+ ADD_OR_OPERATION_BUTTON_SUBJ,
+ DELETE_OPERAND_BUTTON_SUBJ,
+ FORM_ITEM_SUBJ,
+} from '../constants';
+import { httpServiceMock } from '@kbn/core-http-browser-mocks';
+import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
+import {
+ FORM_ITEM_FILTER_BY_LABEL,
+ FORM_ITEM_FILTER_BY_PLACEHOLDER,
+ RULE_TAGS_FILTER_LABEL,
+ RULE_TYPES_FILTER_LABEL,
+} from '../translations';
+import { alertsFiltersMetadata } from '../filters';
+
+const http = httpServiceMock.createStartContract();
+const notifications = notificationServiceMock.createStartContract();
+
+const TAG_1 = 'tag1';
+const TAG_2 = 'tag2';
+const TAG_3 = 'tag3';
+
+jest.mock('@kbn/response-ops-rules-apis/hooks/use_get_rule_tags_query');
+const { useGetRuleTagsQuery: mockUseGetRuleTagsQuery } = jest.requireMock(
+ '@kbn/response-ops-rules-apis/hooks/use_get_rule_tags_query'
+);
+mockUseGetRuleTagsQuery.mockReturnValue({
+ tags: [TAG_1, TAG_2, TAG_3],
+ isLoading: false,
+ isError: false,
+ hasNextPage: false,
+ fetchNextPage: jest.fn(),
+ refetch: jest.fn(),
+});
+
+jest.mock('@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query');
+const { useGetInternalRuleTypesQuery: mockUseGetInternalRuleTypesQuery } = jest.requireMock(
+ '@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query'
+);
+mockUseGetInternalRuleTypesQuery.mockReturnValue({
+ data: [{ id: 'testType', name: 'Test Type', solution: 'stack' }],
+ isLoading: false,
+ isError: false,
+});
+
+const testExpression: AlertsFiltersExpression = [
+ { filter: { type: alertsFiltersMetadata.ruleTags.id, value: [TAG_1] } },
+ { operator: 'and' },
+ { filter: { type: alertsFiltersMetadata.ruleTags.id, value: [TAG_2] } },
+ { operator: 'or' },
+ { filter: { type: alertsFiltersMetadata.ruleTags.id, value: [TAG_3] } },
+];
+
+const mockOnChange = jest.fn();
+
+const TestComponent = (overrides: Partial) => {
+ const [value, setValue] = useState(testExpression);
+
+ mockOnChange.mockImplementation(setValue);
+
+ return (
+
+
+
+ );
+};
+
+describe('AlertsFiltersForm', () => {
+ it('should render boolean expressions', () => {
+ render();
+
+ [TAG_1, TAG_2, TAG_3].forEach((filter) => {
+ expect(screen.getByText(filter)).toBeInTheDocument();
+ });
+ });
+
+ it('should delete the correct operand when clicking on the trash icon', async () => {
+ render();
+
+ [TAG_1, TAG_2, TAG_3].forEach((filter) => {
+ expect(screen.getByText(filter)).toBeInTheDocument();
+ });
+
+ await userEvent.click(screen.getAllByTestId(DELETE_OPERAND_BUTTON_SUBJ)[0]);
+
+ [TAG_1, TAG_3].forEach((filter) => {
+ expect(screen.getByText(filter)).toBeInTheDocument();
+ });
+ expect(screen.queryByText(TAG_2)).not.toBeInTheDocument();
+ expect(mockOnChange).toHaveBeenCalledWith([
+ ...testExpression.slice(0, 1),
+ ...testExpression.slice(3),
+ ]);
+ });
+
+ it('should correctly add a new operand', async () => {
+ render();
+
+ expect(screen.getAllByTestId(FORM_ITEM_SUBJ)).toHaveLength(3);
+
+ await userEvent.click(screen.getByTestId(ADD_OR_OPERATION_BUTTON_SUBJ));
+
+ const formItems = screen.getAllByTestId(FORM_ITEM_SUBJ);
+ expect(formItems).toHaveLength(4);
+ // New operands should be empty
+ expect(formItems[3]).toHaveTextContent(FORM_ITEM_FILTER_BY_PLACEHOLDER);
+ expect(mockOnChange).toHaveBeenCalledWith([
+ ...testExpression,
+ { operator: 'or' },
+ { filter: {} },
+ ]);
+ });
+
+ it('should correctly change filter types', async () => {
+ render();
+
+ const filterTypeSelectors = screen.getAllByRole('button', {
+ name: `${RULE_TAGS_FILTER_LABEL} , ${FORM_ITEM_FILTER_BY_LABEL}`,
+ });
+ await userEvent.click(filterTypeSelectors[0]);
+ await userEvent.click(screen.getByRole('option', { name: RULE_TYPES_FILTER_LABEL }));
+ expect(mockOnChange).toHaveBeenCalledWith([
+ { filter: { type: alertsFiltersMetadata.ruleTypes.id } },
+ ...testExpression.slice(1),
+ ]);
+ });
+
+ it('should correctly change filter values', async () => {
+ render();
+
+ const filterValueDropdownToggles = screen.getAllByRole('button', {
+ name: 'Open list of options',
+ });
+ await userEvent.click(filterValueDropdownToggles[0]);
+ await userEvent.click(screen.getByRole('option', { name: TAG_2 }));
+ expect(mockOnChange).toHaveBeenCalledWith([
+ {
+ filter: {
+ ...(testExpression[0] as { filter: AlertsFilter }).filter,
+ value: [TAG_1, TAG_2],
+ },
+ },
+ ...testExpression.slice(1),
+ ]);
+ });
+});
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form.tsx
new file mode 100644
index 0000000000000..135a16aee0cb6
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form.tsx
@@ -0,0 +1,252 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import {
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ useEuiTheme,
+ EuiPanel,
+ EuiHorizontalRule,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import type { HttpStart } from '@kbn/core-http-browser';
+import type { NotificationsStart } from '@kbn/core-notifications-browser';
+import { AlertsFiltersFormContextProvider } from '../contexts/alerts_filters_form_context';
+import {
+ AlertsFiltersExpression,
+ AlertsFiltersFormItemType,
+ AlertsFilter,
+ AlertsFiltersExpressionOperator,
+} from '../types';
+import {
+ ADD_OPERATION_LABEL,
+ AND_OPERATOR,
+ DELETE_OPERAND_LABEL,
+ OR_OPERATOR,
+} from '../translations';
+import { AlertsFiltersFormItem } from './alerts_filters_form_item';
+import { isFilter } from '../utils';
+import {
+ ADD_AND_OPERATION_BUTTON_SUBJ,
+ ADD_OR_OPERATION_BUTTON_SUBJ,
+ DELETE_OPERAND_BUTTON_SUBJ,
+} from '../constants';
+
+export interface AlertsFiltersFormProps {
+ ruleTypeIds: string[];
+ value?: AlertsFiltersExpression;
+ onChange: (newValue: AlertsFiltersExpression) => void;
+ isDisabled?: boolean;
+ services: {
+ http: HttpStart;
+ notifications: NotificationsStart;
+ };
+}
+
+// This ensures that the form is initialized with an initially empty "Filter by" selector
+const DEFAULT_VALUE: AlertsFiltersExpression = [{ filter: {} }];
+
+/**
+ * A form to build boolean expressions of filters for alerts searches
+ */
+export const AlertsFiltersForm = ({
+ ruleTypeIds,
+ value = DEFAULT_VALUE,
+ onChange,
+ isDisabled = false,
+ services,
+}: AlertsFiltersFormProps) => {
+ const [firstItem, ...otherItems] = value as [
+ {
+ filter: AlertsFilter;
+ },
+ ...AlertsFiltersExpression
+ ];
+
+ const addOperand = useCallback(
+ (operator: AlertsFiltersExpressionOperator) => {
+ onChange([
+ ...value,
+ {
+ operator,
+ },
+ { filter: {} },
+ ]);
+ },
+ [onChange, value]
+ );
+
+ const deleteOperand = useCallback(
+ (atIndex: number) => {
+ // Remove two items: the operator and the following filter
+ const newValue = [...value];
+ newValue.splice(atIndex, 2);
+ onChange(newValue);
+ },
+ [onChange, value]
+ );
+
+ const onFormItemTypeChange = useCallback(
+ (atIndex: number, newType: AlertsFiltersFormItemType) => {
+ const newValue = [...value];
+ const expressionItem = value[atIndex];
+ if (isFilter(expressionItem)) {
+ newValue[atIndex] = {
+ filter: {
+ type: newType,
+ },
+ };
+ onChange(newValue);
+ }
+ },
+ [onChange, value]
+ );
+
+ const onFormItemValueChange = useCallback(
+ (atIndex: number, newItemValue: unknown) => {
+ const newValue = [...value];
+ const expressionItem = newValue[atIndex];
+ if (isFilter(expressionItem)) {
+ newValue[atIndex] = {
+ filter: {
+ ...expressionItem.filter,
+ value: newItemValue,
+ },
+ };
+ onChange(newValue);
+ }
+ },
+ [onChange, value]
+ );
+
+ const contextValue = useMemo(
+ () => ({
+ ruleTypeIds,
+ services,
+ }),
+ [ruleTypeIds, services]
+ );
+
+ return (
+
+
+
+ onFormItemTypeChange(0, newType)}
+ value={firstItem.filter.value}
+ onValueChange={(newValue) => onFormItemValueChange(0, newValue)}
+ isDisabled={isDisabled}
+ />
+
+ {Boolean(otherItems?.length) && (
+
+
+ {otherItems.map((item, offsetIndex) => {
+ // offsetIndex starts from the second item
+ const index = offsetIndex + 1;
+ return (
+
+ {isFilter(item) ? (
+ onFormItemTypeChange(index, newType)}
+ value={item.filter.value}
+ onValueChange={(newValue) => onFormItemValueChange(index, newValue)}
+ isDisabled={isDisabled}
+ />
+ ) : (
+ deleteOperand(index)} />
+ )}
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+
+ addOperand('or')}
+ isDisabled={isDisabled}
+ data-test-subj={ADD_OR_OPERATION_BUTTON_SUBJ}
+ >
+ {OR_OPERATOR}
+
+
+
+ addOperand('and')}
+ isDisabled={isDisabled}
+ data-test-subj={ADD_AND_OPERATION_BUTTON_SUBJ}
+ >
+ {AND_OPERATOR}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+interface OperatorProps {
+ operator: 'and' | 'or';
+ onDelete: () => void;
+}
+
+const Operator = ({ operator, onDelete }: OperatorProps) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+
+
+
+ {operator.toUpperCase()}
+
+
+
+
+
+
+ );
+};
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form_item.test.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form_item.test.tsx
new file mode 100644
index 0000000000000..c5f6dd12ec95d
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form_item.test.tsx
@@ -0,0 +1,98 @@
+/*
+ * 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, { useState } from 'react';
+import { render, screen } from '@testing-library/react';
+import { userEvent } from '@testing-library/user-event';
+import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
+import { alertsFiltersMetadata } from '../filters';
+import { AlertsFiltersFormItem, AlertsFiltersFormItemProps } from './alerts_filters_form_item';
+
+jest.mock('../filters', () => {
+ const original: { alertsFiltersMetadata: typeof alertsFiltersMetadata } =
+ jest.requireActual('../filters');
+ return {
+ alertsFiltersMetadata: Object.fromEntries(
+ Object.entries(original.alertsFiltersMetadata).map(([key, value]) => [
+ key,
+ {
+ ...value,
+ component: jest
+ .fn()
+ .mockImplementation((props) => (
+ {props.value}
+ )),
+ },
+ ])
+ ),
+ };
+});
+
+const mockOnTypeChange = jest.fn();
+const mockOnValueChange = jest.fn();
+
+const TestComponent = (overrides: Partial>) => {
+ const [type, setType] = useState(overrides?.type);
+ const [value, setValue] = useState(overrides?.value);
+
+ mockOnTypeChange.mockImplementation(setType);
+ mockOnValueChange.mockImplementation(setValue);
+
+ return (
+
+
+
+ );
+};
+
+describe('AlertsFiltersFormItem', () => {
+ it('should show all available filter types as options', async () => {
+ render();
+
+ await userEvent.click(screen.getByRole('button'));
+ Object.values(alertsFiltersMetadata).forEach((filterMeta) => {
+ expect(screen.getByText(filterMeta.displayName)).toBeInTheDocument();
+ });
+ });
+
+ it('should render the correct filter component for the selected type', () => {
+ render();
+
+ expect(screen.getByTestId('ruleTagsFilter')).toBeInTheDocument();
+ });
+
+ it('should forward the correct props to the selected filter component', () => {
+ render();
+
+ expect(screen.getByText('tag1')).toBeInTheDocument();
+ expect(alertsFiltersMetadata.ruleTags.component).toHaveBeenCalledWith(
+ {
+ value: ['tag1'],
+ onChange: mockOnValueChange,
+ isDisabled: false,
+ },
+ {}
+ );
+ });
+
+ it('should call onTypeChange when the type is changed', async () => {
+ render();
+
+ await userEvent.click(screen.getByRole('button'));
+ await userEvent.click(screen.getByText(alertsFiltersMetadata.ruleTags.displayName));
+
+ expect(mockOnTypeChange).toHaveBeenCalledWith(alertsFiltersMetadata.ruleTags.id);
+ });
+});
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form_item.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form_item.tsx
new file mode 100644
index 0000000000000..7f3ee77375982
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_filters_form_item.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 { EuiFormRow, EuiSuperSelect, EuiText } from '@elastic/eui';
+import type { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_item';
+import { FORM_ITEM_SUBJ } from '../constants';
+import { alertsFiltersMetadata } from '../filters';
+import { AlertsFilterComponentType, AlertsFiltersFormItemType } from '../types';
+import {
+ FORM_ITEM_FILTER_BY_LABEL,
+ FORM_ITEM_FILTER_BY_PLACEHOLDER,
+ FORM_ITEM_OPTIONAL_CAPTION,
+} from '../translations';
+
+export interface AlertsFiltersFormItemProps {
+ type?: AlertsFiltersFormItemType;
+ onTypeChange: (newFilterType: AlertsFiltersFormItemType) => void;
+ value?: T;
+ onValueChange: (newFilterValue: T) => void;
+ isDisabled?: boolean;
+}
+
+const options: Array> = Object.values(
+ alertsFiltersMetadata
+).map((filterMeta) => ({
+ value: filterMeta.id,
+ dropdownDisplay: filterMeta.displayName,
+ inputDisplay: filterMeta.displayName,
+}));
+
+export const AlertsFiltersFormItem = ({
+ type,
+ onTypeChange,
+ value,
+ onValueChange,
+ isDisabled = false,
+}: AlertsFiltersFormItemProps) => {
+ const FilterComponent = type
+ ? (alertsFiltersMetadata[type].component as AlertsFilterComponentType)
+ : null;
+
+ return (
+ <>
+
+ {FORM_ITEM_OPTIONAL_CAPTION}
+
+ }
+ fullWidth
+ isDisabled={isDisabled}
+ data-test-subj={FORM_ITEM_SUBJ}
+ >
+
+
+ {FilterComponent && (
+
+ )}
+ >
+ );
+};
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_solution_selector.test.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_solution_selector.test.tsx
new file mode 100644
index 0000000000000..ef5cfc996b871
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_solution_selector.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * 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 { httpServiceMock } from '@kbn/core-http-browser-mocks';
+import { useGetInternalRuleTypesQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query';
+import { InternalRuleType } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types';
+import { AlertsSolutionSelector } from './alerts_solution_selector';
+import { SOLUTION_SELECTOR_SUBJ } from '../constants';
+import userEvent from '@testing-library/user-event';
+
+const http = httpServiceMock.createStartContract();
+jest.mock('@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query');
+const mockUseGetInternalRuleTypesQuery = useGetInternalRuleTypesQuery as jest.Mock;
+
+describe('AlertsSolutionSelector', () => {
+ it('should not render when only no solution is available', () => {
+ mockUseGetInternalRuleTypesQuery.mockReturnValue({
+ data: [],
+ isLoading: false,
+ isError: false,
+ });
+ const mockOnSolutionChange = jest.fn();
+ render(
+
+ );
+ expect(screen.queryByTestId(SOLUTION_SELECTOR_SUBJ)).not.toBeInTheDocument();
+ expect(mockOnSolutionChange).not.toHaveBeenCalled();
+ });
+
+ it.each(['stack', 'security', 'observability'])(
+ 'should not render when only one solution (%s) is available and auto-select it',
+ (solution) => {
+ mockUseGetInternalRuleTypesQuery.mockReturnValue({
+ data: [{ id: '.test-rule-type', name: 'Test rule type', solution }],
+ isLoading: false,
+ isError: false,
+ });
+ const mockOnSolutionChange = jest.fn();
+ render(
+
+ );
+ expect(screen.queryByTestId(SOLUTION_SELECTOR_SUBJ)).not.toBeInTheDocument();
+ expect(mockOnSolutionChange).toHaveBeenCalledWith(solution);
+ }
+ );
+
+ it('should not render when only stack and observability are available and auto-select observability', () => {
+ mockUseGetInternalRuleTypesQuery.mockReturnValue({
+ data: [
+ { id: '.es-query', name: 'Elasticsearch Query', solution: 'stack' },
+ { id: '.custom-threshold', name: 'Custom threshold', solution: 'observability' },
+ ] as InternalRuleType[],
+ isLoading: false,
+ isError: false,
+ });
+ const mockOnSolutionChange = jest.fn();
+ render(
+
+ );
+ expect(screen.queryByTestId(SOLUTION_SELECTOR_SUBJ)).not.toBeInTheDocument();
+ expect(mockOnSolutionChange).toHaveBeenCalledWith('observability');
+ });
+
+ it('should render when security and observability/stack are available', async () => {
+ mockUseGetInternalRuleTypesQuery.mockReturnValue({
+ data: [
+ { id: '.es-query', name: 'Elasticsearch Query', solution: 'stack' },
+ { id: '.custom-threshold', name: 'Custom threshold', solution: 'observability' },
+ { id: 'siem.esqlRule', name: 'Security ESQL Rule', solution: 'security' },
+ ] as InternalRuleType[],
+ isLoading: false,
+ isError: false,
+ });
+ render(
+
+ );
+ expect(screen.queryByTestId(SOLUTION_SELECTOR_SUBJ)).toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button'));
+ expect(screen.getAllByRole('option')).toHaveLength(2);
+ expect(screen.getByText('Observability')).toBeInTheDocument();
+ expect(screen.getByText('Security')).toBeInTheDocument();
+ });
+});
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_solution_selector.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_solution_selector.tsx
new file mode 100644
index 0000000000000..cb0d3fbc3d027
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/components/alerts_solution_selector.tsx
@@ -0,0 +1,121 @@
+/*
+ * 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, { useMemo } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiIcon,
+ EuiSuperSelect,
+ EuiSuperSelectOption,
+} from '@elastic/eui';
+import { capitalize } from 'lodash';
+import type { HttpStart } from '@kbn/core-http-browser';
+import { useGetInternalRuleTypesQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query';
+import { RuleTypeSolution } from '@kbn/alerting-types';
+import { InternalRuleType } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types';
+import { SOLUTION_SELECTOR_SUBJ, SUPPORTED_SOLUTIONS } from '../constants';
+import {
+ RULE_TYPES_LOAD_ERROR_MESSAGE,
+ SOLUTION_SELECTOR_LABEL,
+ SOLUTION_SELECTOR_PLACEHOLDER,
+} from '../translations';
+
+export interface AlertsSolutionSelectorProps {
+ solution?: RuleTypeSolution;
+ onSolutionChange: (newSolution: RuleTypeSolution) => void;
+ services: {
+ http: HttpStart;
+ };
+}
+
+const featuresIcons: Record = {
+ stack: 'managementApp',
+ security: 'logoSecurity',
+ observability: 'logoObservability',
+};
+
+const getAvailableSolutions = (ruleTypes: InternalRuleType[]) => {
+ const solutions = new Set();
+
+ for (const ruleType of ruleTypes) {
+ // We want to filter out solutions we do not support in case someone
+ // abuses the solution rule type attribute
+ if (SUPPORTED_SOLUTIONS.includes(ruleType.solution)) {
+ solutions.add(ruleType.solution);
+ }
+ }
+
+ if (solutions.has('stack') && solutions.has('observability')) {
+ solutions.delete('stack');
+ }
+
+ return solutions;
+};
+
+/**
+ * A solution selector for segregated rule types authorization
+ * When only one solution is available, it will be selected by default
+ * and the picker will be hidden.
+ * When Observability/Stack and Security rule types are available
+ * the selector will be shown, hiding Stack under Observability.
+ * Stack is shown only when it's the unique alternative to Security
+ * (i.e. in Security serverless projects).
+ */
+export const AlertsSolutionSelector = ({
+ solution,
+ onSolutionChange,
+ services: { http },
+}: AlertsSolutionSelectorProps) => {
+ const { data: ruleTypes, isLoading, isError } = useGetInternalRuleTypesQuery({ http });
+ const availableSolutions = useMemo(() => getAvailableSolutions(ruleTypes ?? []), [ruleTypes]);
+ const options = useMemo>>(() => {
+ return Array.from(availableSolutions.values()).map((sol) => ({
+ value: sol,
+ inputDisplay: (
+
+
+
+
+ {capitalize(sol)}
+
+ ),
+ }));
+ }, [availableSolutions]);
+
+ if (options.length < 2) {
+ if (options.length === 1) {
+ // Select the only available solution and
+ // don't show the selector
+ onSolutionChange(options[0].value);
+ }
+ return null;
+ }
+
+ return (
+
+ onSolutionChange(newSol)}
+ fullWidth
+ />
+
+ );
+};
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/constants.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/constants.ts
new file mode 100644
index 0000000000000..5c7b859e1f951
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/constants.ts
@@ -0,0 +1,15 @@
+/*
+ * 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".
+ */
+
+export const DELETE_OPERAND_BUTTON_SUBJ = 'deleteOperandButton';
+export const ADD_OR_OPERATION_BUTTON_SUBJ = 'addOrOperationButton';
+export const ADD_AND_OPERATION_BUTTON_SUBJ = 'addAndOperationButton';
+export const SOLUTION_SELECTOR_SUBJ = 'solutionSelector';
+export const FORM_ITEM_SUBJ = 'formItem';
+export const SUPPORTED_SOLUTIONS = ['stack', 'security', 'observability'] as const;
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/contexts/alerts_filters_form_context.tsx b/src/platform/packages/shared/response-ops/alerts-filters-form/contexts/alerts_filters_form_context.tsx
new file mode 100644
index 0000000000000..9854e418676f1
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/contexts/alerts_filters_form_context.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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, { createContext, PropsWithChildren, useContext } from 'react';
+import { AlertsFiltersFormContextValue } from '../types';
+
+export const AlertsFiltersFormContext = createContext(
+ undefined
+);
+
+export const AlertsFiltersFormContextProvider = ({
+ children,
+ value,
+}: PropsWithChildren<{ value: AlertsFiltersFormContextValue }>) => {
+ return (
+ {children}
+ );
+};
+
+export const useAlertsFiltersFormContext = () => {
+ const context = useContext(AlertsFiltersFormContext);
+ if (!context) {
+ throw new Error('Missing AlertsFiltersFormContext');
+ }
+ return context;
+};
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/filters.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/filters.ts
new file mode 100644
index 0000000000000..66e64c84e1501
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/filters.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { filterMetadata as ruleTagsFilterMetadata } from './components/alerts_filter_by_rule_tags';
+import { filterMetadata as ruleTypesFilterMetadata } from './components/alerts_filter_by_rule_types';
+
+export const alertsFiltersMetadata = {
+ [ruleTagsFilterMetadata.id]: ruleTagsFilterMetadata,
+ [ruleTypesFilterMetadata.id]: ruleTypesFilterMetadata,
+} as const;
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/jest.config.js b/src/platform/packages/shared/response-ops/alerts-filters-form/jest.config.js
new file mode 100644
index 0000000000000..33affa5c3bcd1
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/jest.config.js
@@ -0,0 +1,17 @@
+/*
+ * 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".
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../../../..',
+ roots: ['/src/platform/packages/shared/response-ops/alerts-filters-form'],
+ setupFilesAfterEnv: [
+ '/src/platform/packages/shared/response-ops/alerts-filters-form/setup_tests.ts',
+ ],
+};
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/kibana.jsonc b/src/platform/packages/shared/response-ops/alerts-filters-form/kibana.jsonc
new file mode 100644
index 0000000000000..09afc7c089d62
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/kibana.jsonc
@@ -0,0 +1,7 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/response-ops-alerts-filters-form",
+ "owner": "@elastic/response-ops",
+ "group": "platform",
+ "visibility": "shared"
+}
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/package.json b/src/platform/packages/shared/response-ops/alerts-filters-form/package.json
new file mode 100644
index 0000000000000..36b2d9a9529c1
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/response-ops-alerts-filters-form",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/setup_tests.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/setup_tests.ts
new file mode 100644
index 0000000000000..5ebc6d3dac1ca
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/setup_tests.ts
@@ -0,0 +1,11 @@
+/*
+ * 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".
+ */
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+import '@testing-library/jest-dom';
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/translations.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/translations.ts
new file mode 100644
index 0000000000000..e10c334afc911
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/translations.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+
+export const RULE_TAGS_FILTER_LABEL = i18n.translate('alertsFiltersForm.ruleTags.label', {
+ defaultMessage: 'Rule tags',
+});
+
+export const RULE_TAGS_FILTER_PLACEHOLDER = i18n.translate(
+ 'alertsFiltersForm.ruleTags.placeholder',
+ {
+ defaultMessage: 'Select rule tags',
+ }
+);
+
+export const RULE_TAGS_FILTER_NO_OPTIONS_PLACEHOLDER = i18n.translate(
+ 'alertsFiltersForm.ruleTags.noOptionsPlaceholder',
+ {
+ defaultMessage: 'No tags available',
+ }
+);
+
+export const RULE_TAGS_LOAD_ERROR_MESSAGE = i18n.translate(
+ 'alertsFiltersForm.ruleTags.errorMessage',
+ {
+ defaultMessage: 'Cannot load available rule tags',
+ }
+);
+
+export const RULE_TYPES_FILTER_LABEL = i18n.translate('alertsFiltersForm.ruleTypes.label', {
+ defaultMessage: 'Rule types',
+});
+
+export const RULE_TYPES_FILTER_PLACEHOLDER = i18n.translate(
+ 'alertsFiltersForm.ruleTypes.placeholder',
+ {
+ defaultMessage: 'Select rule types',
+ }
+);
+
+export const RULE_TYPES_FILTER_NO_OPTIONS_PLACEHOLDER = i18n.translate(
+ 'alertsFiltersForm.ruleTypes.noOptionsPlaceHolder',
+ {
+ defaultMessage: 'No rule types available',
+ }
+);
+
+export const RULE_TYPES_LOAD_ERROR_MESSAGE = i18n.translate(
+ 'alertsFiltersForm.ruleTypes.errorMessage',
+ {
+ defaultMessage: 'Cannot load available rule types',
+ }
+);
+
+export const DELETE_OPERAND_LABEL = i18n.translate('alertsFiltersForm.deleteOperand', {
+ defaultMessage: 'Delete operand',
+});
+
+export const FORM_ITEM_OPTIONAL_CAPTION = i18n.translate(
+ 'alertsFiltersForm.formItem.optionalCaption',
+ {
+ defaultMessage: 'Optional',
+ }
+);
+
+export const FORM_ITEM_FILTER_BY_LABEL = i18n.translate(
+ 'alertsFiltersForm.formItem.filterByLabel',
+ {
+ defaultMessage: 'Filter by',
+ }
+);
+
+export const FORM_ITEM_FILTER_BY_PLACEHOLDER = i18n.translate(
+ 'alertsFiltersForm.formItem.filterByPlaceholder',
+ {
+ defaultMessage: 'Select filter type',
+ }
+);
+
+export const ADD_OPERATION_LABEL = i18n.translate('alertsFiltersForm.addOperationLabel', {
+ defaultMessage: 'Add boolean operation',
+});
+
+export const OR_OPERATOR = i18n.translate('alertsFiltersForm.orOperator', {
+ defaultMessage: 'OR',
+});
+
+export const AND_OPERATOR = i18n.translate('alertsFiltersForm.andOperator', {
+ defaultMessage: 'AND',
+});
+
+export const SOLUTION_SELECTOR_LABEL = i18n.translate('alertsFiltersForm.solutionSelectorLabel', {
+ defaultMessage: 'Solution',
+});
+
+export const SOLUTION_SELECTOR_PLACEHOLDER = i18n.translate(
+ 'alertsFiltersForm.solutionSelectorPlaceholder',
+ {
+ defaultMessage: 'Select solution',
+ }
+);
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/tsconfig.json b/src/platform/packages/shared/response-ops/alerts-filters-form/tsconfig.json
new file mode 100644
index 0000000000000..ba06446224d0c
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "extends": "../../../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "@emotion/react/types/css-prop",
+ "@kbn/ambient-ui-types"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/response-ops-rules-apis",
+ "@kbn/i18n",
+ "@kbn/i18n-react",
+ "@kbn/core-http-browser-mocks",
+ "@kbn/core-notifications-browser-mocks",
+ "@kbn/core-http-browser",
+ "@kbn/core-notifications-browser",
+ "@kbn/alerting-types",
+ ]
+}
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/types.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/types.ts
new file mode 100644
index 0000000000000..478fdcba8030e
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/types.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 type { HttpStart } from '@kbn/core-http-browser';
+import type { NotificationsStart } from '@kbn/core-notifications-browser';
+import type { ComponentType } from 'react';
+import type { alertsFiltersMetadata } from './filters';
+
+export type AlertsFilterComponentType = ComponentType<{
+ value?: T;
+ onChange: (newValue: T) => void;
+ isDisabled?: boolean;
+}>;
+
+export interface AlertsFilter {
+ type?: AlertsFiltersFormItemType;
+ value?: unknown;
+}
+
+export interface AlertsFilterMetadata {
+ id: string;
+ displayName: string;
+ component: AlertsFilterComponentType;
+ isEmpty: (value?: T) => boolean;
+}
+
+export interface AlertsFiltersFormContextValue {
+ /**
+ * Pre-selected rule type ids for authorization
+ */
+ ruleTypeIds: string[];
+
+ services: {
+ http: HttpStart;
+ notifications: NotificationsStart;
+ };
+}
+
+export type AlertsFiltersFormItemType = keyof typeof alertsFiltersMetadata;
+
+export type AlertsFiltersExpressionOperator = 'and' | 'or';
+
+export type AlertsFiltersExpressionItem =
+ | {
+ operator: AlertsFiltersExpressionOperator;
+ }
+ | {
+ filter: AlertsFilter;
+ };
+
+export type AlertsFiltersExpression = AlertsFiltersExpressionItem[];
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/utils.test.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/utils.test.ts
new file mode 100644
index 0000000000000..962b2c839c12a
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/utils.test.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { getRuleTypeIdsForSolution, isFilter } from './utils';
+import type { InternalRuleType } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types';
+import type { RuleTypeSolution } from '@kbn/alerting-types';
+
+const ruleTypes = [
+ { id: 'stack-rule-type', solution: 'stack' },
+ { id: 'observability-rule-type', solution: 'observability' },
+ { id: 'security-rule-type', solution: 'security' },
+] as InternalRuleType[];
+
+describe('getRuleTypeIdsForSolution', () => {
+ it.each(['stack', 'observability', 'security'] as RuleTypeSolution[])(
+ 'should include %s rule type ids in the returned array',
+ (solution) => {
+ const solutionRuleTypeIds = ruleTypes
+ .filter((ruleType) => ruleType.solution === solution)
+ .map((ruleType) => ruleType.id);
+ const ruleTypeIds = getRuleTypeIdsForSolution(ruleTypes, solution);
+ for (const ruleTypeId of solutionRuleTypeIds) {
+ expect(ruleTypeIds).toContain(ruleTypeId);
+ }
+ }
+ );
+
+ it('should group stack rule type ids under observability', () => {
+ expect(getRuleTypeIdsForSolution(ruleTypes, 'observability')).toEqual([
+ 'stack-rule-type',
+ 'observability-rule-type',
+ ]);
+ });
+
+ it('should always return security rule type ids in isolation', () => {
+ expect(getRuleTypeIdsForSolution(ruleTypes, 'security')).toEqual(['security-rule-type']);
+ expect(getRuleTypeIdsForSolution(ruleTypes, 'security')).toEqual(['security-rule-type']);
+ });
+});
+
+describe('isFilter', () => {
+ it('should return true for items with filter property', () => {
+ expect(isFilter({ filter: {} })).toBeTruthy();
+ });
+
+ it.each([null, undefined])('should return false for %s items', (filter) => {
+ // @ts-expect-error: Testing empty values
+ expect(isFilter(filter)).toBeFalsy();
+ });
+});
diff --git a/src/platform/packages/shared/response-ops/alerts-filters-form/utils.ts b/src/platform/packages/shared/response-ops/alerts-filters-form/utils.ts
new file mode 100644
index 0000000000000..70812f46d72d0
--- /dev/null
+++ b/src/platform/packages/shared/response-ops/alerts-filters-form/utils.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 type { RuleTypeSolution } from '@kbn/alerting-types';
+import type { InternalRuleType } from '@kbn/response-ops-rules-apis/apis/get_internal_rule_types';
+import type { AlertsFilter, AlertsFiltersExpressionItem } from './types';
+
+/**
+ * Filters rule types by solution and returns their ids.
+ * Stack rules are included under Observability.
+ */
+export const getRuleTypeIdsForSolution = (
+ ruleTypes: InternalRuleType[],
+ solution: RuleTypeSolution
+) => {
+ return ruleTypes
+ .filter(
+ (ruleType) =>
+ ruleType.solution === solution ||
+ (solution === 'observability' && ruleType.solution === 'stack')
+ )
+ .map((ruleType) => ruleType.id);
+};
+
+export const isFilter = (item?: AlertsFiltersExpressionItem): item is { filter: AlertsFilter } =>
+ item != null && 'filter' in item;
diff --git a/src/platform/packages/shared/response-ops/rules-apis/hooks/use_get_rule_tags_query.ts b/src/platform/packages/shared/response-ops/rules-apis/hooks/use_get_rule_tags_query.ts
index c751c52be4c53..3ef7ace468293 100644
--- a/src/platform/packages/shared/response-ops/rules-apis/hooks/use_get_rule_tags_query.ts
+++ b/src/platform/packages/shared/response-ops/rules-apis/hooks/use_get_rule_tags_query.ts
@@ -69,21 +69,29 @@ export function useGetRuleTagsQuery({
};
};
- const { refetch, data, fetchNextPage, isLoading, isFetching, hasNextPage, isFetchingNextPage } =
- useInfiniteQuery({
- queryKey: getKey({
- ruleTypeIds,
- search,
- perPage,
- page,
- refresh,
- }),
- queryFn,
- onError: onErrorFn,
- enabled,
- getNextPageParam,
- refetchOnWindowFocus: false,
- });
+ const {
+ refetch,
+ data,
+ fetchNextPage,
+ isLoading,
+ isFetching,
+ hasNextPage,
+ isFetchingNextPage,
+ isError,
+ } = useInfiniteQuery({
+ queryKey: getKey({
+ ruleTypeIds,
+ search,
+ perPage,
+ page,
+ refresh,
+ }),
+ queryFn,
+ onError: onErrorFn,
+ enabled,
+ getNextPageParam,
+ refetchOnWindowFocus: false,
+ });
const tags = useMemo(() => {
return (
@@ -99,5 +107,6 @@ export function useGetRuleTagsQuery({
refetch,
isLoading: isLoading || isFetching || isFetchingNextPage,
fetchNextPage,
+ isError,
};
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 30383ccc87050..8fde9ee3c8bd1 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1514,6 +1514,8 @@
"@kbn/response-ops-alerts-apis/*": ["src/platform/packages/shared/response-ops/alerts-apis/*"],
"@kbn/response-ops-alerts-fields-browser": ["src/platform/packages/shared/response-ops/alerts-fields-browser"],
"@kbn/response-ops-alerts-fields-browser/*": ["src/platform/packages/shared/response-ops/alerts-fields-browser/*"],
+ "@kbn/response-ops-alerts-filters-form": ["src/platform/packages/shared/response-ops/alerts-filters-form"],
+ "@kbn/response-ops-alerts-filters-form/*": ["src/platform/packages/shared/response-ops/alerts-filters-form/*"],
"@kbn/response-ops-alerts-table": ["src/platform/packages/shared/response-ops/alerts-table"],
"@kbn/response-ops-alerts-table/*": ["src/platform/packages/shared/response-ops/alerts-table/*"],
"@kbn/response-ops-rule-form": ["src/platform/packages/shared/response-ops/rule_form"],
diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx
index 65cb06157afa6..b2bdbcc983af7 100644
--- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx
+++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx
@@ -25,6 +25,7 @@ import { CREATE_RULE_ROUTE, EDIT_RULE_ROUTE, RuleForm } from '@kbn/response-ops-
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
+import { AlertsFiltersFormSandbox } from './components/alerts_filters_form_sandbox';
import { TriggersActionsUiExamplePublicStartDeps } from './plugin';
import { Page } from './components/page';
@@ -248,6 +249,20 @@ const TriggersActionsUiExampleApp = ({
)}
/>
+ (
+
+
+
+ )}
+ />
diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/alerts_filters_form_sandbox.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/alerts_filters_form_sandbox.tsx
new file mode 100644
index 0000000000000..7545453641a00
--- /dev/null
+++ b/x-pack/examples/triggers_actions_ui_example/public/components/alerts_filters_form_sandbox.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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, { useMemo, useState } from 'react';
+import { HttpStart } from '@kbn/core-http-browser';
+import { NotificationsStart } from '@kbn/core-notifications-browser';
+import { AlertsFiltersForm } from '@kbn/response-ops-alerts-filters-form/components/alerts_filters_form';
+import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel, EuiText } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { AlertsFiltersExpression } from '@kbn/response-ops-alerts-filters-form/types';
+import { useGetInternalRuleTypesQuery } from '@kbn/response-ops-rules-apis/hooks/use_get_internal_rule_types_query';
+import { RuleTypeSolution } from '@kbn/alerting-types';
+import { AlertsSolutionSelector } from '@kbn/response-ops-alerts-filters-form/components/alerts_solution_selector';
+import { getRuleTypeIdsForSolution } from '@kbn/response-ops-alerts-filters-form/utils';
+
+export const AlertsFiltersFormSandbox = ({
+ services: { http, notifications },
+}: {
+ services: {
+ http: HttpStart;
+ notifications: NotificationsStart;
+ };
+}) => {
+ const [solution, setSolution] = useState();
+ const [filters, setFilters] = useState();
+ const { data: ruleTypes, isLoading: isLoadingRuleTypes } = useGetInternalRuleTypesQuery({ http });
+ const ruleTypeIds = useMemo(
+ () => (!ruleTypes || !solution ? [] : getRuleTypeIdsForSolution(ruleTypes, solution)),
+ [ruleTypes, solution]
+ );
+ const services = useMemo(
+ () => ({
+ http,
+ notifications,
+ }),
+ [http, notifications]
+ );
+
+ return (
+
+
+
+ {
+ if (solution != null && newSolution !== solution) {
+ setFilters(undefined);
+ }
+ setSolution(newSolution);
+ }}
+ />
+
+
+ {isLoadingRuleTypes ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx
index 1d73a88d8ee2f..145cbe2cf2d12 100644
--- a/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx
+++ b/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx
@@ -64,15 +64,20 @@ export const Sidebar = ({ history }: { history: ScopedHistory }) => {
onClick: () => history.push(`/rule_status_filter`),
},
{
- id: 'alerts table',
+ id: 'alerts_table',
name: 'Alert Table',
onClick: () => history.push('/alerts_table'),
},
{
- id: 'rules settings link',
+ id: 'rules_settings_link',
name: 'Rules Settings Link',
onClick: () => history.push('/rules_settings_link'),
},
+ {
+ id: 'alerts_filters_form',
+ name: 'Alerts filters form',
+ onClick: () => history.push('/alerts_filters_form'),
+ },
],
},
{
diff --git a/x-pack/examples/triggers_actions_ui_example/tsconfig.json b/x-pack/examples/triggers_actions_ui_example/tsconfig.json
index 520f827facc61..f02680c32be5a 100644
--- a/x-pack/examples/triggers_actions_ui_example/tsconfig.json
+++ b/x-pack/examples/triggers_actions_ui_example/tsconfig.json
@@ -35,5 +35,10 @@
"@kbn/licensing-plugin",
"@kbn/response-ops-rule-form",
"@kbn/fields-metadata-plugin",
+ "@kbn/response-ops-alerts-filters-form",
+ "@kbn/core-http-browser",
+ "@kbn/core-notifications-browser",
+ "@kbn/response-ops-rules-apis",
+ "@kbn/alerting-types",
]
}
diff --git a/yarn.lock b/yarn.lock
index 62a7c9f9fb464..5cf804a7a6fc0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6662,6 +6662,10 @@
version "0.0.0"
uid ""
+"@kbn/response-ops-alerts-filters-form@link:src/platform/packages/shared/response-ops/alerts-filters-form":
+ version "0.0.0"
+ uid ""
+
"@kbn/response-ops-alerts-table@link:src/platform/packages/shared/response-ops/alerts-table":
version "0.0.0"
uid ""