mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ResponseOps][Rules] Use rule form instead of rule flyout in observability solution (#206774)
## Summary Resolves https://github.com/elastic/kibana/issues/195574 This PR updates observability solution to use new rule form to `create` and `edit` rules same as `stack management > rules` page. It removes usage of rule flyout form o11y solution. Also updated functional tests. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### How to test - Create a rule in o11y, verify it works as expected - Edit rule in o11y via different options (from rule details page, rule list table, alert details page etc.) verify it works as expected - Verify the same in serverless o11y project ### Release Note Use rule form to create or edit rules in observability. --------- Co-authored-by: Maryam Saeidi <maryam.saeidi@elastic.co>
This commit is contained in:
parent
1ca4d967d9
commit
dd6376d3be
12 changed files with 469 additions and 61 deletions
|
@ -44,6 +44,7 @@ export interface CreateRuleFormProps {
|
|||
shouldUseRuleProducer?: boolean;
|
||||
canShowConsumerSelection?: boolean;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
isServerless?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (ruleId: string) => void;
|
||||
}
|
||||
|
@ -60,6 +61,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
shouldUseRuleProducer = false,
|
||||
canShowConsumerSelection = true,
|
||||
showMustacheAutocompleteSwitch = false,
|
||||
isServerless = false,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
} = props;
|
||||
|
@ -195,6 +197,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
validConsumers,
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
|
||||
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
||||
import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { CreateRuleForm } from './create_rule_form';
|
||||
import { EditRuleForm } from './edit_rule_form';
|
||||
|
@ -27,10 +28,20 @@ export interface RuleFormProps {
|
|||
plugins: RuleFormPlugins;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (ruleId: string) => void;
|
||||
validConsumers?: RuleCreationValidConsumer[];
|
||||
multiConsumerSelection?: RuleCreationValidConsumer | null;
|
||||
isServerless?: boolean;
|
||||
}
|
||||
|
||||
export const RuleForm = (props: RuleFormProps) => {
|
||||
const { plugins: _plugins, onCancel, onSubmit } = props;
|
||||
const {
|
||||
plugins: _plugins,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
validConsumers,
|
||||
multiConsumerSelection,
|
||||
isServerless,
|
||||
} = props;
|
||||
const { id, ruleTypeId } = useParams<{
|
||||
id?: string;
|
||||
ruleTypeId?: string;
|
||||
|
@ -80,6 +91,9 @@ export const RuleForm = (props: RuleFormProps) => {
|
|||
plugins={plugins}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
validConsumers={validConsumers}
|
||||
multiConsumerSelection={multiConsumerSelection}
|
||||
isServerless={isServerless}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -112,6 +126,9 @@ export const RuleForm = (props: RuleFormProps) => {
|
|||
actionTypeRegistry,
|
||||
id,
|
||||
ruleTypeId,
|
||||
validConsumers,
|
||||
multiConsumerSelection,
|
||||
isServerless,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* 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 { RuleTypeWithDescription } from '@kbn/alerts-ui-shared';
|
||||
import { getInitialMultiConsumer } from './get_initial_multi_consumer';
|
||||
|
||||
describe('getInitialMultiConsumer', () => {
|
||||
const ruleType = {
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
category: 'test',
|
||||
} as RuleTypeWithDescription;
|
||||
|
||||
const ruleTypes = [
|
||||
{
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
},
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
},
|
||||
{
|
||||
enabledInLicense: true,
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
actionGroups: [],
|
||||
defaultActionGroupId: 'threshold met',
|
||||
minimumLicenseRequired: 'basic',
|
||||
authorizedConsumers: {
|
||||
stackAlerts: {
|
||||
read: true,
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
id: '.index-threshold',
|
||||
name: 'Index threshold',
|
||||
category: 'management',
|
||||
producer: 'stackAlerts',
|
||||
},
|
||||
] as RuleTypeWithDescription[];
|
||||
|
||||
test('should return null when rule type id does not match', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['logs', 'observability'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
id: 'test',
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
test('should return null when no valid consumers', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: [],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
test('should return same valid consumer when only one valid consumer', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['alerts'],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('alerts');
|
||||
});
|
||||
|
||||
test('should not return observability consumer for non serverless', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['logs', 'infrastructure', 'observability'],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('logs');
|
||||
});
|
||||
|
||||
test('should return observability consumer for serverless', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['logs', 'infrastructure', 'observability'],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: true,
|
||||
});
|
||||
|
||||
expect(res).toBe('observability');
|
||||
});
|
||||
|
||||
test('should return null when there is no authorized consumers', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['alerts', 'infrastructure'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
test('should return null when multiConsumerSelection is null', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {
|
||||
stackAlerts: { read: true, all: true },
|
||||
},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
test('should return valid multi consumer correctly', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: 'logs',
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {
|
||||
stackAlerts: { read: true, all: true },
|
||||
},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('logs');
|
||||
});
|
||||
|
||||
test('should return stackAlerts correctly', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: 'alerts',
|
||||
validConsumers: ['stackAlerts', 'logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('stackAlerts');
|
||||
});
|
||||
|
||||
test('should return null valid consumer correctly', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: 'alerts',
|
||||
validConsumers: ['infrastructure', 'logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {},
|
||||
},
|
||||
ruleTypes: [],
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
});
|
|
@ -35,11 +35,13 @@ export const getInitialMultiConsumer = ({
|
|||
validConsumers,
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless,
|
||||
}: {
|
||||
multiConsumerSelection?: RuleCreationValidConsumer | null;
|
||||
validConsumers: RuleCreationValidConsumer[];
|
||||
ruleType: RuleTypeWithDescription;
|
||||
ruleTypes: RuleTypeWithDescription[];
|
||||
isServerless?: boolean;
|
||||
}): RuleCreationValidConsumer | null => {
|
||||
// If rule type doesn't support multi-consumer or no valid consumers exists,
|
||||
// return nothing
|
||||
|
@ -52,8 +54,8 @@ export const getInitialMultiConsumer = ({
|
|||
return validConsumers[0];
|
||||
}
|
||||
|
||||
// If o11y is in the valid consumers, just use that
|
||||
if (validConsumers.includes(AlertConsumers.OBSERVABILITY)) {
|
||||
// If o11y is in the valid consumers and it is serverless, just use that
|
||||
if (isServerless && validConsumers.includes(AlertConsumers.OBSERVABILITY)) {
|
||||
return AlertConsumers.OBSERVABILITY;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export const EXPLORATORY_VIEW_PATH = '/exploratory-view' as const; // has been m
|
|||
export const RULES_PATH = '/alerts/rules' as const;
|
||||
export const RULES_LOGS_PATH = '/alerts/rules/logs' as const;
|
||||
export const RULE_DETAIL_PATH = '/alerts/rules/:ruleId' as const;
|
||||
export const CREATE_RULE_PATH = '/alerts/rules/create/:ruleTypeId' as const;
|
||||
export const CASES_PATH = '/cases' as const;
|
||||
export const ANNOTATIONS_PATH = '/annotations' as const;
|
||||
export const SETTINGS_PATH = '/slos/settings' as const;
|
||||
|
@ -37,6 +38,8 @@ export const paths = {
|
|||
rules: `${OBSERVABILITY_BASE_PATH}${RULES_PATH}`,
|
||||
ruleDetails: (ruleId: string) =>
|
||||
`${OBSERVABILITY_BASE_PATH}${RULES_PATH}/${encodeURIComponent(ruleId)}`,
|
||||
createRule: (ruleTypeId: string) =>
|
||||
`${OBSERVABILITY_BASE_PATH}${RULES_PATH}/create/${encodeURIComponent(ruleTypeId)}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { RuleForm } from '@kbn/response-ops-rule-form';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import { observabilityRuleCreationValidConsumers } from '../../../common/constants';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
|
||||
export function RulePage() {
|
||||
const {
|
||||
http,
|
||||
docLinks,
|
||||
observabilityAIAssistant,
|
||||
application,
|
||||
notifications,
|
||||
charts,
|
||||
settings,
|
||||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
serverless,
|
||||
actionTypeRegistry,
|
||||
ruleTypeRegistry,
|
||||
chrome,
|
||||
...startServices
|
||||
} = useKibana().services;
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const location = useLocation<{ returnApp?: string; returnPath?: string }>();
|
||||
const { returnApp, returnPath } = location.state || {};
|
||||
|
||||
useBreadcrumbs(
|
||||
[
|
||||
{
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.alertsLinkText', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
href: http.basePath.prepend(paths.observability.alerts),
|
||||
deepLinkId: 'observability-overview:alerts',
|
||||
},
|
||||
{
|
||||
href: http.basePath.prepend(paths.observability.rules),
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.rulesLinkText', {
|
||||
defaultMessage: 'Rules',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.createLinkText', {
|
||||
defaultMessage: 'Create',
|
||||
}),
|
||||
},
|
||||
],
|
||||
{ serverless }
|
||||
);
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate data-test-subj="rulePage">
|
||||
<HeaderMenu />
|
||||
<RuleForm
|
||||
plugins={{
|
||||
http,
|
||||
application,
|
||||
notifications,
|
||||
charts,
|
||||
settings,
|
||||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
docLinks,
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
...startServices,
|
||||
}}
|
||||
validConsumers={observabilityRuleCreationValidConsumers}
|
||||
multiConsumerSelection={AlertConsumers.LOGS}
|
||||
isServerless={!!serverless}
|
||||
onCancel={() => {
|
||||
if (returnApp && returnPath) {
|
||||
application.navigateToApp(returnApp, { path: returnPath });
|
||||
} else {
|
||||
return application.navigateToUrl(http.basePath.prepend(paths.observability.rules));
|
||||
}
|
||||
}}
|
||||
onSubmit={(ruleId) => {
|
||||
return application.navigateToUrl(
|
||||
http.basePath.prepend(paths.observability.ruleDetails(ruleId))
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
|
@ -5,14 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { RuleTypeModalProps } from '@kbn/response-ops-rule-form/src/rule_type_modal/components/rule_type_modal';
|
||||
import * as pluginContext from '../../hooks/use_plugin_context';
|
||||
import { ObservabilityPublicPluginsStart } from '../../plugin';
|
||||
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
|
||||
|
@ -21,6 +23,12 @@ import { RulesPage } from './rules';
|
|||
|
||||
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
|
||||
const mockObservabilityAIAssistant = observabilityAIAssistantPluginMock.createStartContract();
|
||||
const mockApplication = {
|
||||
navigateToApp: jest.fn(),
|
||||
navigateToUrl: jest.fn(),
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
@ -34,6 +42,11 @@ jest.mock('../../utils/kibana_react', () => ({
|
|||
services: {
|
||||
...mockUseKibanaReturnValue.services,
|
||||
observabilityAIAssistant: mockObservabilityAIAssistant,
|
||||
application: {
|
||||
...mockUseKibanaReturnValue.services.application,
|
||||
navigateToApp: mockApplication.navigateToApp,
|
||||
navigateToUrl: mockApplication.navigateToUrl,
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
@ -48,6 +61,15 @@ jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({
|
|||
useLoadRuleTypesQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/response-ops-rule-form/src/rule_type_modal', () => ({
|
||||
RuleTypeModal: ({ onSelectRuleType }: RuleTypeModalProps) => (
|
||||
<div data-test-subj="ruleTypeModal">
|
||||
RuleTypeModal
|
||||
<button onClick={() => onSelectRuleType('1')}>Rule type 1</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const useLocationMock = useLocation as jest.Mock;
|
||||
|
||||
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
||||
|
@ -130,13 +152,17 @@ describe('RulesPage with all capabilities', () => {
|
|||
|
||||
useLoadRuleTypesQuery.mockReturnValue({
|
||||
ruleTypesState: {
|
||||
isLoading: false,
|
||||
isInitialLoading: false,
|
||||
data: ruleTypeIndex,
|
||||
},
|
||||
});
|
||||
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<RulesPage />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RulesPage />
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
@ -155,6 +181,21 @@ describe('RulesPage with all capabilities', () => {
|
|||
const wrapper = await setup();
|
||||
expect(wrapper.getByTestId('createRuleButton')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('navigates to create rule form correctly', async () => {
|
||||
const wrapper = await setup();
|
||||
expect(wrapper.getByTestId('createRuleButton')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(wrapper.getByTestId('createRuleButton'));
|
||||
expect(await wrapper.findByTestId('ruleTypeModal')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(await wrapper.findByText('Rule type 1'));
|
||||
await waitFor(() => {
|
||||
expect(mockApplication.navigateToUrl).toHaveBeenCalledWith(
|
||||
'/app/observability/alerts/rules/create/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RulesPage with show only capability', () => {
|
||||
|
|
|
@ -11,12 +11,10 @@ import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { useLoadRuleTypesQuery } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import React, { lazy, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { observabilityRuleCreationValidConsumers } from '../../../common/constants';
|
||||
import { RULES_LOGS_PATH, RULES_PATH } from '../../../common/locators/paths';
|
||||
import { RULES_LOGS_PATH, RULES_PATH, paths } from '../../../common/locators/paths';
|
||||
import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
@ -37,18 +35,13 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) {
|
|||
docLinks,
|
||||
notifications: { toasts },
|
||||
observabilityAIAssistant,
|
||||
triggersActionsUi: {
|
||||
ruleTypeRegistry,
|
||||
getAddRuleFlyout: AddRuleFlyout,
|
||||
getRulesSettingsLink: RulesSettingsLink,
|
||||
},
|
||||
application,
|
||||
triggersActionsUi: { ruleTypeRegistry, getRulesSettingsLink: RulesSettingsLink },
|
||||
serverless,
|
||||
} = useKibana().services;
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const history = useHistory();
|
||||
const [ruleTypeModalVisibility, setRuleTypeModalVisibility] = useState<boolean>(false);
|
||||
const [ruleTypeIdToCreate, setRuleTypeIdToCreate] = useState<string | undefined>(undefined);
|
||||
const [addRuleFlyoutVisibility, setAddRuleFlyoutVisibility] = useState(false);
|
||||
const [stateRefresh, setRefresh] = useState(new Date());
|
||||
|
||||
useBreadcrumbs(
|
||||
|
@ -188,9 +181,10 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) {
|
|||
<RuleTypeModal
|
||||
onClose={() => setRuleTypeModalVisibility(false)}
|
||||
onSelectRuleType={(ruleTypeId) => {
|
||||
setRuleTypeIdToCreate(ruleTypeId);
|
||||
setRuleTypeModalVisibility(false);
|
||||
setAddRuleFlyoutVisibility(true);
|
||||
return application.navigateToUrl(
|
||||
http.basePath.prepend(paths.observability.createRule(ruleTypeId))
|
||||
);
|
||||
}}
|
||||
http={http}
|
||||
toasts={toasts}
|
||||
|
@ -198,26 +192,6 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) {
|
|||
filteredRuleTypes={filteredRuleTypes}
|
||||
/>
|
||||
)}
|
||||
|
||||
{addRuleFlyoutVisibility && (
|
||||
<AddRuleFlyout
|
||||
ruleTypeId={ruleTypeIdToCreate}
|
||||
canChangeTrigger={false}
|
||||
consumer={ALERTING_FEATURE_ID}
|
||||
filteredRuleTypes={filteredRuleTypes}
|
||||
validConsumers={observabilityRuleCreationValidConsumers}
|
||||
initialSelectedConsumer={AlertConsumers.LOGS}
|
||||
onClose={() => {
|
||||
setAddRuleFlyoutVisibility(false);
|
||||
}}
|
||||
onSave={() => {
|
||||
setRefresh(new Date());
|
||||
return Promise.resolve();
|
||||
}}
|
||||
hideGrouping
|
||||
useRuleProducer
|
||||
/>
|
||||
)}
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { LandingPage } from '../pages/landing/landing';
|
|||
import { OverviewPage } from '../pages/overview/overview';
|
||||
import { RulesPage } from '../pages/rules/rules';
|
||||
import { RuleDetailsPage } from '../pages/rule_details/rule_details';
|
||||
import { RulePage } from '../pages/rules/rule';
|
||||
import {
|
||||
ALERTS_PATH,
|
||||
ALERT_DETAIL_PATH,
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
OLD_SLOS_OUTDATED_DEFINITIONS_PATH,
|
||||
OLD_SLO_DETAIL_PATH,
|
||||
OLD_SLO_EDIT_PATH,
|
||||
CREATE_RULE_PATH,
|
||||
} from '../../common/locators/paths';
|
||||
import { HasDataContextProvider } from '../context/has_data_context/has_data_context';
|
||||
|
||||
|
@ -133,6 +135,13 @@ export const routes = {
|
|||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
[CREATE_RULE_PATH]: {
|
||||
handler: () => {
|
||||
return <RulePage />;
|
||||
},
|
||||
params: {},
|
||||
exact: true,
|
||||
},
|
||||
[ALERT_DETAIL_PATH]: {
|
||||
handler: () => {
|
||||
return <AlertDetails />;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Key } from 'selenium-webdriver';
|
|||
import expect from 'expect';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
@ -17,9 +17,10 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const find = getService('find');
|
||||
const logger = getService('log');
|
||||
const retry = getService('retry');
|
||||
const toasts = getService('toasts');
|
||||
const PageObjects = getPageObjects(['header']);
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/196766
|
||||
describe.skip('Custom threshold rule', function () {
|
||||
describe('Custom threshold rule', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
||||
const observability = getService('observability');
|
||||
|
@ -58,13 +59,16 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
it('shows the custom threshold rule in the observability section', async () => {
|
||||
await observability.alerts.rulesPage.clickCreateRuleButton();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await observability.alerts.rulesPage.clickOnObservabilityCategory();
|
||||
await observability.alerts.rulesPage.clickOnCustomThresholdRule();
|
||||
});
|
||||
|
||||
it('can add name and tags', async () => {
|
||||
await testSubjects.setValue('ruleNameInput', 'test custom threshold rule');
|
||||
await testSubjects.setValue('comboBoxSearchInput', 'tag1');
|
||||
await testSubjects.setValue('ruleDetailsNameInput', 'test custom threshold rule', {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
await testSubjects.setValue('ruleDetailsTagsInput', 'tag1');
|
||||
});
|
||||
|
||||
it('can add data view', async () => {
|
||||
|
@ -204,9 +208,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('can save the rule', async () => {
|
||||
await testSubjects.click('saveRuleButton');
|
||||
await testSubjects.click('rulePageFooterSaveButton');
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
await find.byCssSelector('button[title="test custom threshold rule"]');
|
||||
|
||||
const title = await toasts.getTitleAndDismiss();
|
||||
expect(title).toEqual(`Created rule "test custom threshold rule"`);
|
||||
});
|
||||
|
||||
it('saved the rule correctly', async () => {
|
||||
|
@ -220,6 +226,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
expect.objectContaining({
|
||||
name: 'test custom threshold rule',
|
||||
tags: ['tag1'],
|
||||
consumer: 'logs',
|
||||
params: expect.objectContaining({
|
||||
alertOnGroupDisappear: false,
|
||||
alertOnNoData: false,
|
||||
|
|
|
@ -51,7 +51,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
it('does render the correct error message', async () => {
|
||||
await testSubjects.setValue('ruleNameInput', 'test custom threshold rule');
|
||||
await testSubjects.setValue('ruleDetailsNameInput', 'test custom threshold rule');
|
||||
|
||||
await testSubjects.click('customEquation');
|
||||
const customEquationField = await find.byCssSelector(
|
||||
|
|
|
@ -64,11 +64,11 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
const selectAndFillInEsQueryRule = async (ruleName: string) => {
|
||||
await testSubjects.click(`.es-query-SelectOption`);
|
||||
await retry.waitFor(
|
||||
'Create Rule flyout is visible',
|
||||
async () => await testSubjects.exists('addRuleFlyoutTitle')
|
||||
'Create Rule form is visible',
|
||||
async () => await testSubjects.exists('createRuleForm')
|
||||
);
|
||||
|
||||
await testSubjects.setValue('ruleNameInput', ruleName);
|
||||
await testSubjects.setValue('ruleDetailsNameInput', ruleName);
|
||||
await testSubjects.click('queryFormType_esQuery');
|
||||
await testSubjects.click('selectIndexExpression');
|
||||
const indexComboBox = await find.byCssSelector('#indexSelectSearchBox');
|
||||
|
@ -90,7 +90,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
|
||||
const observability = getService('observability');
|
||||
|
||||
const navigateAndOpenCreateRuleFlyout = async () => {
|
||||
const navigateAndOpenRuleTypeModal = async () => {
|
||||
await observability.alerts.common.navigateToRulesPage();
|
||||
await retry.waitFor(
|
||||
'Create Rule button is visible',
|
||||
|
@ -128,11 +128,11 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
|
||||
describe('Create rule button', () => {
|
||||
it('Show Rule Type Modal when Create Rule button is clicked', async () => {
|
||||
await navigateAndOpenCreateRuleFlyout();
|
||||
await navigateAndOpenRuleTypeModal();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Create rules flyout', () => {
|
||||
describe('Create rules form', () => {
|
||||
const ruleName = 'esQueryRule';
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -151,13 +151,15 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
infrastructure: ['all'],
|
||||
})
|
||||
);
|
||||
await navigateAndOpenCreateRuleFlyout();
|
||||
await navigateAndOpenRuleTypeModal();
|
||||
await selectAndFillInEsQueryRule(ruleName);
|
||||
|
||||
await testSubjects.click('saveRuleButton');
|
||||
await testSubjects.click('rulePageFooterSaveButton');
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await observability.alerts.common.navigateToRulesPage();
|
||||
|
||||
const tableRows = await find.allByCssSelector('.euiTableRow');
|
||||
const rows = await getRulesList(tableRows);
|
||||
expect(rows.length).to.be(1);
|
||||
|
@ -174,13 +176,14 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
logs: ['all'],
|
||||
})
|
||||
);
|
||||
await navigateAndOpenCreateRuleFlyout();
|
||||
await navigateAndOpenRuleTypeModal();
|
||||
await selectAndFillInEsQueryRule(ruleName);
|
||||
|
||||
await testSubjects.click('saveRuleButton');
|
||||
await testSubjects.click('rulePageFooterSaveButton');
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await observability.alerts.common.navigateToRulesPage();
|
||||
const tableRows = await find.allByCssSelector('.euiTableRow');
|
||||
const rows = await getRulesList(tableRows);
|
||||
expect(rows.length).to.be(1);
|
||||
|
@ -196,17 +199,17 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
})
|
||||
);
|
||||
|
||||
await navigateAndOpenCreateRuleFlyout();
|
||||
await navigateAndOpenRuleTypeModal();
|
||||
await selectAndFillInEsQueryRule(ruleName);
|
||||
|
||||
await retry.waitFor('consumer select modal is visible', async () => {
|
||||
return await testSubjects.exists('ruleFormConsumerSelect');
|
||||
return await testSubjects.exists('ruleConsumerSelection');
|
||||
});
|
||||
|
||||
const consumerSelect = await testSubjects.find('ruleFormConsumerSelect');
|
||||
const consumerSelect = await testSubjects.find('ruleConsumerSelection');
|
||||
await consumerSelect.click();
|
||||
const consumerOptionsList = await testSubjects.find(
|
||||
'comboBoxOptionsList ruleFormConsumerSelect-optionsList'
|
||||
'comboBoxOptionsList ruleConsumerSelectionInput-optionsList'
|
||||
);
|
||||
const consumerOptions = await consumerOptionsList.findAllByClassName(
|
||||
'euiComboBoxOption__content'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue