[8.x] [Response Ops] [Rule Form] Add Rule Form Flyout v2 (#206685) (#213258)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Response Ops] [Rule Form] Add Rule Form Flyout v2
(#206685)](https://github.com/elastic/kibana/pull/206685)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Zacqary Adam
Xeper","email":"Zacqary@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-04T14:52:58Z","message":"[Response
Ops] [Rule Form] Add Rule Form Flyout v2 (#206685)\n\n##
Summary\r\n\r\nPart of #195211\r\n\r\nReplaces the create/edit rule
flyout with the new rule flyout\r\n\r\n<img width=\"1032\"
alt=\"Screenshot 2025-01-14 at 3 12
30 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/9cbcf4f8-1078-4f7e-a55a-aacc2d877a14\"\r\n/>\r\n<img
width=\"1383\" alt=\"Screenshot 2025-01-14 at 3 12
52 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/2270d57b-9462-4898-9dd0-41baefcc02d4\"\r\n/>\r\n\r\nRestores
the confirmation prompt before canceling or saving a rule\r\nwithout
actions defined.\r\n\r\nAlso fixes most of the design papercuts in the
Actions step:\r\n\r\n<img width=\"494\" alt=\"Screenshot 2025-01-14 at 3
11
06 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/3cf21d43-88e0-4250-b290-a545e1ebdbcf\"\r\n/>\r\n<img
width=\"494\" alt=\"Screenshot 2025-01-14 at 3 11
01 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/00ef3f95-c91b-4bb7-aead-a3e23c02f7df\"\r\n/>\r\n\r\n\r\n\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"367ff8dbec417eaed30fb5f6fdf491642dcd1ebd","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:ResponseOps","release_note:feature","Feature:Alerting/RulesManagement","ci:cloud-deploy","ci:project-deploy-observability","Team:obs-ux-infra_services","Team:obs-ux-management","backport:version","v9.1.0","v8.19.0"],"title":"[Response
Ops] [Rule Form] Add Rule Form Flyout
v2","number":206685,"url":"https://github.com/elastic/kibana/pull/206685","mergeCommit":{"message":"[Response
Ops] [Rule Form] Add Rule Form Flyout v2 (#206685)\n\n##
Summary\r\n\r\nPart of #195211\r\n\r\nReplaces the create/edit rule
flyout with the new rule flyout\r\n\r\n<img width=\"1032\"
alt=\"Screenshot 2025-01-14 at 3 12
30 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/9cbcf4f8-1078-4f7e-a55a-aacc2d877a14\"\r\n/>\r\n<img
width=\"1383\" alt=\"Screenshot 2025-01-14 at 3 12
52 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/2270d57b-9462-4898-9dd0-41baefcc02d4\"\r\n/>\r\n\r\nRestores
the confirmation prompt before canceling or saving a rule\r\nwithout
actions defined.\r\n\r\nAlso fixes most of the design papercuts in the
Actions step:\r\n\r\n<img width=\"494\" alt=\"Screenshot 2025-01-14 at 3
11
06 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/3cf21d43-88e0-4250-b290-a545e1ebdbcf\"\r\n/>\r\n<img
width=\"494\" alt=\"Screenshot 2025-01-14 at 3 11
01 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/00ef3f95-c91b-4bb7-aead-a3e23c02f7df\"\r\n/>\r\n\r\n\r\n\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"367ff8dbec417eaed30fb5f6fdf491642dcd1ebd"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206685","number":206685,"mergeCommit":{"message":"[Response
Ops] [Rule Form] Add Rule Form Flyout v2 (#206685)\n\n##
Summary\r\n\r\nPart of #195211\r\n\r\nReplaces the create/edit rule
flyout with the new rule flyout\r\n\r\n<img width=\"1032\"
alt=\"Screenshot 2025-01-14 at 3 12
30 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/9cbcf4f8-1078-4f7e-a55a-aacc2d877a14\"\r\n/>\r\n<img
width=\"1383\" alt=\"Screenshot 2025-01-14 at 3 12
52 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/2270d57b-9462-4898-9dd0-41baefcc02d4\"\r\n/>\r\n\r\nRestores
the confirmation prompt before canceling or saving a rule\r\nwithout
actions defined.\r\n\r\nAlso fixes most of the design papercuts in the
Actions step:\r\n\r\n<img width=\"494\" alt=\"Screenshot 2025-01-14 at 3
11
06 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/3cf21d43-88e0-4250-b290-a545e1ebdbcf\"\r\n/>\r\n<img
width=\"494\" alt=\"Screenshot 2025-01-14 at 3 11
01 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/00ef3f95-c91b-4bb7-aead-a3e23c02f7df\"\r\n/>\r\n\r\n\r\n\r\n\r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by:
Elastic Machine
<elasticmachine@users.noreply.github.com>","sha":"367ff8dbec417eaed30fb5f6fdf491642dcd1ebd"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2025-03-05 19:32:40 -06:00 committed by GitHub
parent c6ad409763
commit edc74bab07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
153 changed files with 1764 additions and 1008 deletions

View file

@ -32,6 +32,7 @@ export type RuleTypeWithDescription = RuleType<string, string> & { description?:
export type RuleTypeIndexWithDescriptions = Map<string, RuleTypeWithDescription>;
export type RuleTypeParams = Record<string, unknown>;
export type RuleTypeMetaData = Record<string, unknown>;
export interface RuleFormBaseErrors {
name?: string[];
@ -104,7 +105,7 @@ export interface RuleTypeParamsExpressionProps<
metadata?: MetaData;
charts: ChartsPluginSetup;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViews?: DataViewsPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
}

View file

@ -0,0 +1,5 @@
This export path is only for lazy loading the flyout. Importing `@kbn/response-ops-rule-form` directly generally increases a plugin's bundle size unnecessarily.
Flyout UI is handled at the root of this component to avoid UI glitches. We want to render loading states and the fully loaded flyout body within the same <EuiFlyout> component, otherwise the user will see multiple flyouts and overlay masks flickering in and out.
This should be the ONLY export path for contexts that use the rule form as a flyout and not as the full page.

View file

@ -0,0 +1,10 @@
/*
* 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 * from './rule_form_flyout';

View file

@ -0,0 +1,72 @@
/*
* 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 { EuiFlyoutResizable, EuiLoadingElastic } from '@elastic/eui';
import React, { Suspense, lazy, useCallback } from 'react';
import type { RuleFormProps } from '../src/rule_form';
import type { RuleTypeMetaData } from '../src/types';
import {
RuleFlyoutUIContextProvider,
useRuleFlyoutUIContext,
RuleFormErrorPromptWrapper,
} from '../lib';
const RuleForm: React.LazyExoticComponent<React.FC<RuleFormProps<any>>> = lazy(() =>
import('../src/rule_form').then((module) => ({ default: module.RuleForm }))
);
const RuleFormFlyoutRenderer = <MetaData extends RuleTypeMetaData>(
props: RuleFormProps<MetaData>
) => {
const { onClickClose, hideCloseButton } = useRuleFlyoutUIContext();
const onClose = useCallback(() => {
// If onClickClose has been initialized, call it instead of onCancel. onClickClose should be used to
// determine if the close confirmation modal should be shown. props.onCancel is passed down the component hierarchy
// and will be called 1) by onClickClose, if the confirmation modal doesn't need to be shown, or 2) by the confirm
// button on the confirmation modal
if (onClickClose) {
onClickClose();
} else {
// ONLY call props.onCancel directly from this level of the component hierarcht if onClickClose has not yet been initialized.
// This will only occur if the user tries to close the flyout while the Suspense fallback is still visible
props.onCancel?.();
}
}, [onClickClose, props]);
return (
<EuiFlyoutResizable
ownFocus
onClose={onClose}
aria-labelledby="flyoutTitle"
size={620}
minWidth={500}
className="ruleFormFlyout__container"
hideCloseButton={hideCloseButton}
>
<Suspense
fallback={
<RuleFormErrorPromptWrapper hasBorder={false} hasShadow={false}>
<EuiLoadingElastic size="xl" />
</RuleFormErrorPromptWrapper>
}
>
<RuleForm {...props} isFlyout />
</Suspense>
</EuiFlyoutResizable>
);
};
export const RuleFormFlyout = <MetaData extends RuleTypeMetaData>(
props: RuleFormProps<MetaData>
) => {
return (
<RuleFlyoutUIContextProvider>
<RuleFormFlyoutRenderer {...props} />
</RuleFlyoutUIContextProvider>
);
};

View file

@ -10,7 +10,7 @@
export * from './src/types';
export * from './src/rule_type_modal';
export { RuleForm } from './src/rule_form';
export { RuleForm, type RuleFormProps } from './src/rule_form';
export {
fetchUiConfig,

View file

@ -0,0 +1 @@
This export path is for utility functions that might be necessary both for plugins that import from `@kbn/response-ops-rule-form` or from `@kbn/response-ops-rule-form/flyout` to keep their bundle size down.

View file

@ -0,0 +1,12 @@
/*
* 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 * from './is_valid_rule_form_plugins';
export * from './rule_flyout_ui_context';
export * from './rule_form_error_prompt_wrapper';

View file

@ -0,0 +1,47 @@
/*
* 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 { RuleFormProps } from '../src/rule_form';
const requiredPluginNames = [
'http',
'i18n',
'theme',
'userProfile',
'application',
'notifications',
'charts',
'settings',
'data',
'unifiedSearch',
'docLinks',
'dataViews',
'fieldsMetadata',
];
type RequiredRuleFormPlugins = Omit<
RuleFormProps['plugins'],
'actionTypeRegistry' | 'ruleTypeRegistry'
>;
export const isValidRuleFormPlugins = (input: unknown): input is RequiredRuleFormPlugins => {
if (typeof input !== 'object' || input === null) {
return false;
}
requiredPluginNames.forEach((pluginName) => {
if (!(pluginName in input)) {
// eslint-disable-next-line no-console
console.error(`RuleForm plugins is missing required plugin: ${pluginName}`);
return false;
}
});
return true;
};

View file

@ -0,0 +1,45 @@
/*
* 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, useState } from 'react';
const initialRuleFlyoutUIContext: {
onClickClose: (() => void) | null;
hideCloseButton: boolean;
setOnClickClose: (onClickClose: () => void) => void;
setHideCloseButton: (hideCloseButton: boolean) => void;
} = {
onClickClose: null,
hideCloseButton: false,
setOnClickClose: () => {},
setHideCloseButton: () => {},
};
export const RuleFlyoutUIContext = createContext(initialRuleFlyoutUIContext);
export const RuleFlyoutUIContextProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const [onClickClose, setOnClickClose] = useState<(() => void) | null>(null);
const [hideCloseButton, setHideCloseButton] = useState<boolean>(false);
return (
<RuleFlyoutUIContext.Provider
value={{
onClickClose,
hideCloseButton,
setOnClickClose,
setHideCloseButton,
}}
>
{children}
</RuleFlyoutUIContext.Provider>
);
};
export const useRuleFlyoutUIContext = () => {
return React.useContext(RuleFlyoutUIContext);
};

View file

@ -11,6 +11,7 @@ import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { waitFor, renderHook } from '@testing-library/react';
import { httpServiceMock } from '@kbn/core/public/mocks';
import { EcsFlat } from '@elastic/ecs';
import { useLoadRuleTypeAadTemplateField } from './use_load_rule_type_aad_template_fields';
@ -21,6 +22,14 @@ const wrapper = ({ children }: { children: React.ReactNode }) => (
);
const http = httpServiceMock.createStartContract();
const fieldsMetadataMock = {
useFieldsMetadata: jest.fn(),
getClient: jest.fn(() =>
Promise.resolve({
find: () => Promise.resolve({ fields: EcsFlat }),
})
),
};
describe('useLoadRuleTypeAadTemplateFields', () => {
beforeEach(() => {
@ -43,6 +52,7 @@ describe('useLoadRuleTypeAadTemplateFields', () => {
() =>
useLoadRuleTypeAadTemplateField({
http,
fieldsMetadata: fieldsMetadataMock,
ruleTypeId: 'ruleTypeId',
enabled: true,
}),

View file

@ -7,7 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EcsFlat } from '@elastic/ecs';
import { useRef } from 'react';
import { isEmpty } from 'lodash';
import { ActionVariable } from '@kbn/alerting-types';
import type { HttpStart } from '@kbn/core-http-browser';
import { useQuery } from '@tanstack/react-query';
@ -15,21 +16,33 @@ import {
fetchRuleTypeAadTemplateFields,
getDescription,
} from '@kbn/alerts-ui-shared/src/common/apis';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
export interface UseLoadRuleTypeAadTemplateFieldProps {
http: HttpStart;
ruleTypeId?: string;
enabled: boolean;
cacheTime?: number;
fieldsMetadata?: FieldsMetadataPublicStart;
}
export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplateFieldProps) => {
const { http, ruleTypeId, enabled, cacheTime } = props;
const ecsFlat = useRef<Record<string, any>>({});
const { http, ruleTypeId, enabled, cacheTime, fieldsMetadata } = props;
const queryFn = async () => {
if (!ruleTypeId) {
return;
}
if (isEmpty(ecsFlat.current)) {
const fmClient = await fieldsMetadata?.getClient();
if (fmClient) {
const { fields } = await fmClient.find({});
ecsFlat.current = fields;
}
}
return fetchRuleTypeAadTemplateFields({ http, ruleTypeId });
};
@ -44,7 +57,7 @@ export const useLoadRuleTypeAadTemplateField = (props: UseLoadRuleTypeAadTemplat
select: (dataViewFields) => {
return dataViewFields?.map<ActionVariable>((d) => ({
name: d.name,
description: getDescription(d.name, EcsFlat),
description: getDescription(d.name, ecsFlat.current),
}));
},
cacheTime,

View file

@ -14,19 +14,19 @@ import {
CONFIRM_RULE_SAVE_CONFIRM_BUTTON_TEXT,
CONFIRM_RULE_SAVE_CANCEL_BUTTON_TEXT,
CONFIRM_RULE_SAVE_MESSAGE_TEXT,
} from '../translations';
} from '../../translations';
export interface RulePageConfirmCreateRuleProps {
export interface ConfirmCreateRuleProps {
onCancel: () => void;
onConfirm: () => void;
}
export const RulePageConfirmCreateRule = (props: RulePageConfirmCreateRuleProps) => {
export const ConfirmCreateRule = (props: ConfirmCreateRuleProps) => {
const { onCancel, onConfirm } = props;
return (
<EuiConfirmModal
data-test-subj="rulePageConfirmCreateRule"
data-test-subj="confirmCreateRuleModal"
title={CONFIRMATION_RULE_SAVE_TITLE}
onCancel={onCancel}
onConfirm={onConfirm}

View file

@ -0,0 +1,10 @@
/*
* 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 * from './confirm_create_rule';

View file

@ -0,0 +1,43 @@
/*
* 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 { EuiConfirmModal, EuiText } from '@elastic/eui';
import {
RULE_FORM_CANCEL_MODAL_TITLE,
RULE_FORM_CANCEL_MODAL_CONFIRM,
RULE_FORM_CANCEL_MODAL_CANCEL,
RULE_FORM_CANCEL_MODAL_DESCRIPTION,
} from '../../translations';
export interface ConfirmRuleCloseRuleProps {
onCancel: () => void;
onConfirm: () => void;
}
export const ConfirmRuleClose = (props: ConfirmRuleCloseRuleProps) => {
const { onCancel, onConfirm } = props;
return (
<EuiConfirmModal
onCancel={onCancel}
onConfirm={onConfirm}
data-test-subj="confirmRuleCloseModal"
buttonColor="danger"
defaultFocusedButton="confirm"
title={RULE_FORM_CANCEL_MODAL_TITLE}
confirmButtonText={RULE_FORM_CANCEL_MODAL_CONFIRM}
cancelButtonText={RULE_FORM_CANCEL_MODAL_CANCEL}
>
<EuiText>
<p>{RULE_FORM_CANCEL_MODAL_DESCRIPTION}</p>
</EuiText>
</EuiConfirmModal>
);
};

View file

@ -0,0 +1,10 @@
/*
* 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 * from './confirm_rule_close';

View file

@ -0,0 +1,12 @@
/*
* 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 * from './request_code_block';
export * from './confirm_create_rule';
export * from './confirm_rule_close';

View file

@ -16,11 +16,11 @@ import {
UpdateRuleBody,
transformCreateRuleBody,
transformUpdateRuleBody,
} from '../common/apis';
import { BASE_ALERTING_API_PATH } from '../constants';
import { useRuleFormState } from '../hooks';
import { SHOW_REQUEST_MODAL_ERROR } from '../translations';
import { RuleFormData } from '../types';
} from '../../common/apis';
import { BASE_ALERTING_API_PATH } from '../../constants';
import { useRuleFormState } from '../../hooks';
import { SHOW_REQUEST_MODAL_ERROR } from '../../translations';
import { RuleFormData } from '../../types';
const stringifyBodyRequest = ({
formData,

View file

@ -11,11 +11,12 @@ import React, { useCallback } from 'react';
import { EuiLoadingElastic } from '@elastic/eui';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { type RuleCreationValidConsumer } from '@kbn/rule-data-utils';
import type { RuleFormData, RuleFormPlugins } from './types';
import type { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types';
import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';
import { RuleFormStateProvider } from './rule_form_state';
import { useCreateRule } from './common/hooks';
import { RulePage } from './rule_page';
import { RuleFlyout } from './rule_flyout';
import {
RuleFormCircuitBreakerError,
RuleFormErrorPromptWrapper,
@ -44,9 +45,13 @@ export interface CreateRuleFormProps {
shouldUseRuleProducer?: boolean;
canShowConsumerSelection?: boolean;
showMustacheAutocompleteSwitch?: boolean;
isFlyout?: boolean;
isServerless?: boolean;
onCancel?: () => void;
onSubmit?: (ruleId: string) => void;
onChangeMetaData?: (metadata?: RuleTypeMetaData) => void;
initialValues?: Partial<RuleFormData>;
initialMetadata?: RuleTypeMetaData;
}
export const CreateRuleForm = (props: CreateRuleFormProps) => {
@ -61,12 +66,16 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
shouldUseRuleProducer = false,
canShowConsumerSelection = true,
showMustacheAutocompleteSwitch = false,
isFlyout,
isServerless = false,
onCancel,
onSubmit,
onChangeMetaData,
initialMetadata,
initialValues = {},
} = props;
const { http, docLinks, notifications, ruleTypeRegistry, ...deps } = plugins;
const { http, docLinks, notifications, ruleTypeRegistry, fieldsMetadata, ...deps } = plugins;
const { toasts } = notifications;
const { mutate, isLoading: isSaving } = useCreateRule({
@ -112,6 +121,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
validConsumers,
filteredRuleTypes,
connectorFeatureId,
fieldsMetadata,
});
const onSave = useCallback(
@ -158,11 +168,13 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
);
}
const RuleFormUIComponent = isFlyout ? RuleFlyout : RulePage;
return (
<div data-test-subj="createRuleForm">
<RuleFormStateProvider
initialRuleFormState={{
formData: getDefaultFormData({
<RuleFormStateProvider
initialRuleFormState={{
formData: {
...getDefaultFormData({
ruleTypeId,
name: `${ruleType.name} rule`,
consumer: getInitialConsumer({
@ -176,33 +188,41 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
}),
actions: [],
}),
plugins,
connectors,
connectorTypes,
aadTemplateFields,
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
selectedRuleTypeModel: ruleTypeModel,
selectedRuleType: ruleType,
availableRuleTypes: getAvailableRuleTypes({
consumer,
ruleTypes,
ruleTypeRegistry,
}).map(({ ruleType: rt }) => rt),
...initialValues,
},
metadata: initialMetadata,
plugins,
connectors,
connectorTypes,
aadTemplateFields,
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
selectedRuleTypeModel: ruleTypeModel,
selectedRuleType: ruleType,
availableRuleTypes: getAvailableRuleTypes({
consumer,
ruleTypes,
ruleTypeRegistry,
}).map(({ ruleType: rt }) => rt),
validConsumers,
flappingSettings,
canShowConsumerSelection,
showMustacheAutocompleteSwitch,
multiConsumerSelection: getInitialMultiConsumer({
multiConsumerSelection,
validConsumers,
flappingSettings,
canShowConsumerSelection,
showMustacheAutocompleteSwitch,
multiConsumerSelection: getInitialMultiConsumer({
multiConsumerSelection,
validConsumers,
ruleType,
ruleTypes,
isServerless,
}),
}}
>
<RulePage isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />
</RuleFormStateProvider>
</div>
ruleType,
ruleTypes,
isServerless,
}),
}}
>
<RuleFormUIComponent
isEdit={false}
isSaving={isSaving}
onCancel={onCancel}
onSave={onSave}
onChangeMetaData={onChangeMetaData}
/>
</RuleFormStateProvider>
);
};

View file

@ -10,10 +10,11 @@
import React, { useCallback, useMemo } from 'react';
import { EuiLoadingElastic } from '@elastic/eui';
import { toMountPoint } from '@kbn/react-kibana-mount';
import type { RuleFormData, RuleFormPlugins } from './types';
import type { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types';
import { RuleFormStateProvider } from './rule_form_state';
import { useUpdateRule } from './common/hooks';
import { RulePage } from './rule_page';
import { RuleFlyout } from './rule_flyout';
import { RuleFormHealthCheckError } from './rule_form_errors/rule_form_health_check_error';
import { useLoadDependencies } from './hooks/use_load_dependencies';
import {
@ -32,8 +33,11 @@ export interface EditRuleFormProps {
plugins: RuleFormPlugins;
showMustacheAutocompleteSwitch?: boolean;
connectorFeatureId?: string;
isFlyout?: boolean;
onCancel?: () => void;
onSubmit?: (ruleId: string) => void;
onChangeMetaData?: (metadata?: RuleTypeMetaData) => void;
initialMetadata?: RuleTypeMetaData;
}
export const EditRuleForm = (props: EditRuleFormProps) => {
@ -44,8 +48,12 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
connectorFeatureId = 'alerting',
onCancel,
onSubmit,
isFlyout,
onChangeMetaData,
initialMetadata,
} = props;
const { http, notifications, docLinks, ruleTypeRegistry, application, ...deps } = plugins;
const { http, notifications, docLinks, ruleTypeRegistry, application, fieldsMetadata, ...deps } =
plugins;
const { toasts } = notifications;
const { mutate, isLoading: isSaving } = useUpdateRule({
@ -89,6 +97,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
ruleTypeRegistry,
id,
connectorFeatureId,
fieldsMetadata,
});
const onSave = useCallback(
@ -179,40 +188,47 @@ export const EditRuleForm = (props: EditRuleFormProps) => {
return action;
});
const RuleFormUIComponent = isFlyout ? RuleFlyout : RulePage;
return (
<div data-test-subj="editRuleForm">
<RuleFormStateProvider
initialRuleFormState={{
connectors,
connectorTypes,
aadTemplateFields,
formData: {
...getDefaultFormData({
ruleTypeId: fetchedFormData.ruleTypeId,
name: fetchedFormData.name,
consumer: fetchedFormData.consumer,
actions: fetchedFormData.actions,
}),
...fetchedFormData,
actions: actionsWithFrequency,
},
id,
plugins,
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
selectedRuleType: ruleType,
selectedRuleTypeModel: ruleTypeModel,
availableRuleTypes: getAvailableRuleTypes({
<RuleFormStateProvider
initialRuleFormState={{
connectors,
connectorTypes,
aadTemplateFields,
formData: {
...getDefaultFormData({
ruleTypeId: fetchedFormData.ruleTypeId,
name: fetchedFormData.name,
consumer: fetchedFormData.consumer,
ruleTypes,
ruleTypeRegistry,
}).map(({ ruleType: rt }) => rt),
flappingSettings,
validConsumers: DEFAULT_VALID_CONSUMERS,
showMustacheAutocompleteSwitch,
}}
>
<RulePage isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />
</RuleFormStateProvider>
</div>
actions: fetchedFormData.actions,
}),
...fetchedFormData,
actions: actionsWithFrequency,
},
id,
metadata: initialMetadata,
plugins,
minimumScheduleInterval: uiConfig?.minimumScheduleInterval,
selectedRuleType: ruleType,
selectedRuleTypeModel: ruleTypeModel,
availableRuleTypes: getAvailableRuleTypes({
consumer: fetchedFormData.consumer,
ruleTypes,
ruleTypeRegistry,
}).map(({ ruleType: rt }) => rt),
flappingSettings,
validConsumers: DEFAULT_VALID_CONSUMERS,
showMustacheAutocompleteSwitch,
}}
>
<RuleFormUIComponent
isEdit
isSaving={isSaving}
onSave={onSave}
onCancel={onCancel}
onChangeMetaData={onChangeMetaData}
/>
</RuleFormStateProvider>
);
};

View file

@ -17,6 +17,7 @@ import {
useLoadRuleTypesQuery,
useFetchFlappingSettings,
} from '@kbn/alerts-ui-shared';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import {
useLoadConnectors,
useLoadConnectorTypes,
@ -38,6 +39,7 @@ export interface UseLoadDependencies {
validConsumers?: RuleCreationValidConsumer[];
filteredRuleTypes?: string[];
connectorFeatureId?: string;
fieldsMetadata?: FieldsMetadataPublicStart;
}
export const useLoadDependencies = (props: UseLoadDependencies) => {
@ -50,6 +52,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
capabilities,
filteredRuleTypes = [],
connectorFeatureId,
fieldsMetadata,
} = props;
const canReadConnectors = !!capabilities.actions?.show;
@ -129,6 +132,7 @@ export const useLoadDependencies = (props: UseLoadDependencies) => {
ruleTypeId: computedRuleTypeId,
enabled: !!computedRuleTypeId && canReadConnectors,
cacheTime: 0,
fieldsMetadata,
});
const ruleType = useMemo(() => {

View file

@ -131,6 +131,49 @@ describe('useRuleFormSteps', () => {
});
});
test('renders actions as incomplete if there are 0 defined actions', async () => {
useRuleFormState.mockReturnValue({
...ruleFormStateMock,
formData: {
...formDataMock,
actions: [],
},
});
const TestComponent = () => {
const { steps } = useRuleFormSteps();
return <EuiSteps steps={steps} />;
};
render(<TestComponent />);
expect(await screen.getByText('Step 2 is incomplete')).toBeInTheDocument();
const step2 = screen.getByTestId('ruleFormStep-rule-actions-reportOnBlur');
await fireEvent.blur(step2!);
expect(await screen.queryByText('Step 2 has errors')).not.toBeInTheDocument();
});
test('renders actions as complete if there are more than 0 defined actions', async () => {
useRuleFormState.mockReturnValue({
...ruleFormStateMock,
formData: {
...formDataMock,
actions: [{ id: '1', actionTypeId: 'test', name: 'test' }],
},
});
const TestComponent = () => {
const { steps } = useRuleFormSteps();
return <EuiSteps steps={steps} />;
};
render(<TestComponent />);
expect(await screen.queryByText('Step 2 has errors')).not.toBeInTheDocument();
expect(await screen.queryByText('Step 2 is incomplete')).not.toBeInTheDocument();
});
describe('useRuleFormHorizontalSteps', () => {
afterEach(() => {
jest.clearAllMocks();

View file

@ -47,11 +47,13 @@ const getStepStatus = ({
currentStep,
hasErrors,
touchedSteps,
isIncomplete,
}: {
step: RuleFormStepId;
currentStep?: RuleFormStepId;
hasErrors: boolean;
touchedSteps: Record<RuleFormStepId, boolean>;
isIncomplete?: boolean;
}) => {
// Only apply the current status if currentStep is being tracked
if (currentStep === step) return 'current';
@ -61,6 +63,11 @@ const getStepStatus = ({
// Otherwise just mark it as incomplete
return touchedSteps[step] ? 'danger' : 'incomplete';
}
if (isIncomplete) {
return 'incomplete';
}
// Only mark this step complete or incomplete if the currentStep flag is being used, otherwise set no status
if (currentStep && isStepBefore(step, currentStep)) {
return 'complete';
@ -83,6 +90,7 @@ const useCommonRuleFormSteps = ({
paramsErrors = {},
actionsErrors = {},
actionsParamsErrors = {},
formData: { actions },
} = useRuleFormState();
const canReadConnectors = !!application.capabilities.actions?.show;
@ -121,8 +129,9 @@ const useCommonRuleFormSteps = ({
currentStep,
hasErrors: hasActionErrors,
touchedSteps,
isIncomplete: actions.length === 0,
}),
[hasActionErrors, currentStep, touchedSteps]
[hasActionErrors, currentStep, touchedSteps, actions]
);
const ruleDetailsStatus = useMemo(
@ -144,12 +153,14 @@ const useCommonRuleFormSteps = ({
: RULE_FORM_PAGE_RULE_DEFINITION_TITLE,
status: ruleDefinitionStatus,
children: <RuleDefinition />,
'data-test-subj': 'ruleFormStep-definition',
},
[RuleFormStepId.ACTIONS]: canReadConnectors
? {
title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
status: actionsStatus,
children: <RuleActions />,
'data-test-subj': 'ruleFormStep-actions',
}
: null,
[RuleFormStepId.DETAILS]: {
@ -158,6 +169,7 @@ const useCommonRuleFormSteps = ({
: RULE_FORM_PAGE_RULE_DETAILS_TITLE,
status: ruleDetailsStatus,
children: <RuleDetails />,
'data-test-subj': 'ruleFormStep-details',
},
}),
[ruleDefinitionStatus, canReadConnectors, actionsStatus, ruleDetailsStatus, shortTitles]

View file

@ -30,7 +30,11 @@ import {
EuiSelectableProps,
useCurrentEuiBreakpoint,
} from '@elastic/eui';
import { ActionConnector, checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared';
import {
ActionConnector,
type ActionTypeModel,
checkActionFormActionTypeEnabled,
} from '@kbn/alerts-ui-shared';
import React, { Suspense, useCallback, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { RuleFormParamsErrors } from '../common/types';
@ -348,7 +352,13 @@ export const RuleActionsConnectorsBody = ({
<EuiFlexGroup direction="column">
{filteredConnectors.map((connector) => {
const { id, actionTypeId, name } = connector;
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
let actionTypeModel: ActionTypeModel;
try {
actionTypeModel = actionTypeRegistry.get(actionTypeId);
if (!actionTypeModel) return null;
} catch (e) {
return null;
}
const actionType = connectorTypes.find((item) => item.id === actionTypeId);
if (!actionType) {
@ -370,6 +380,7 @@ export const RuleActionsConnectorsBody = ({
const connectorCard = (
<EuiCard
data-test-subj="ruleActionsConnectorsModalCard"
data-action-type-id={actionTypeId}
hasBorder
isDisabled={isDisabled}
titleSize="xs"

View file

@ -557,7 +557,7 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
}, []);
const accordionIcon = useMemo(() => {
if (!connector) {
if (!connector || !actionType) {
return (
<EuiFlexItem grow={false}>
<EuiToolTip content={ACTION_UNABLE_TO_LOAD_CONNECTOR_TITLE}>
@ -585,35 +585,24 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
</EuiToolTip>
) : (
<Suspense fallback={null}>
<EuiIcon size="l" type={actionTypeModel.iconClass} />
<EuiToolTip content={actionType.name}>
<EuiIcon size="l" type={actionTypeModel.iconClass} />
</EuiToolTip>
</Suspense>
)}
</EuiFlexItem>
);
}, [connector, showActionGroupErrorIcon, actionTypeModel]);
}, [connector, showActionGroupErrorIcon, actionType, actionTypeModel.iconClass]);
const connectorTitle = useMemo(() => {
const title = connector ? ACTION_TITLE(connector) : actionTypeModel.actionTypeTitle;
return (
<EuiFlexItem grow={false}>
<EuiText>{title}</EuiText>
<EuiFlexItem grow={false} className=".eui-textBreakWord">
<EuiText size="s">{title}</EuiText>
</EuiFlexItem>
);
}, [connector, actionTypeModel]);
const actionTypeTitle = useMemo(() => {
if (!connector || !actionType) {
return null;
}
return (
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<strong>{actionType.name}</strong>
</EuiText>
</EuiFlexItem>
);
}, [connector, actionType]);
const runWhenTitle = useMemo(() => {
if (!connector) {
return null;
@ -624,11 +613,15 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
if (selectedActionGroup || action.frequency?.summary) {
return (
<EuiFlexItem grow={false}>
<EuiBadge iconType="clock">
{action.frequency?.summary
? SUMMARY_GROUP_TITLE
: RUN_WHEN_GROUP_TITLE(selectedActionGroup!.name.toLocaleLowerCase())}
</EuiBadge>
<EuiToolTip
content={
action.frequency?.summary
? SUMMARY_GROUP_TITLE
: RUN_WHEN_GROUP_TITLE(selectedActionGroup!.name.toLocaleLowerCase())
}
>
<EuiBadge iconType="clock" />
</EuiToolTip>
</EuiFlexItem>
);
}
@ -644,9 +637,9 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
if (warning) {
return (
<EuiFlexItem grow={false}>
<EuiBadge data-test-subj="warning-badge" iconType="warning" color="warning">
{ACTION_WARNING_TITLE}
</EuiBadge>
<EuiToolTip content={ACTION_WARNING_TITLE}>
<EuiBadge data-test-subj="warning-badge" iconType="warning" color="warning" />
</EuiToolTip>
</EuiFlexItem>
);
}
@ -695,20 +688,23 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
<EuiPanel color="subdued" paddingSize="m">
<EuiFlexGroup alignItems="center" responsive={false}>
{accordionIcon}
{connectorTitle}
{actionTypeTitle}
{runWhenTitle}
{warningIcon}
{actionTypeModel.isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
alignment="middle"
data-test-subj="ruleActionsSystemActionsItemBetaBadge"
label={TECH_PREVIEW_LABEL}
tooltipContent={TECH_PREVIEW_DESCRIPTION}
/>
</EuiFlexItem>
)}
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
{connectorTitle}
{actionTypeModel.isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
alignment="middle"
data-test-subj="ruleActionsSystemActionsItemBetaBadge"
label={TECH_PREVIEW_LABEL}
tooltipContent={TECH_PREVIEW_DESCRIPTION}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs" responsive={false}>
{runWhenTitle}
{warningIcon}
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
}

View file

@ -135,7 +135,6 @@ describe('ruleActionsSystemActionsItem', () => {
expect(screen.getByTestId('ruleActionsSystemActionsItem')).toBeInTheDocument();
expect(screen.getByText('connector-1')).toBeInTheDocument();
expect(screen.getByText('actionType: 1')).toBeInTheDocument();
expect(screen.getByTestId('ruleActionsSystemActionsItemAccordionContent')).toBeVisible();
expect(screen.getByText('RuleActionsMessage')).toBeInTheDocument();

View file

@ -283,35 +283,38 @@ export const RuleActionsSystemActionsItem = (props: RuleActionsSystemActionsItem
</EuiToolTip>
) : (
<Suspense fallback={null}>
<EuiIcon size="l" type={actionTypeModel.iconClass} />
<EuiToolTip content={actionType?.name}>
<EuiIcon size="l" type={actionTypeModel.iconClass} />
</EuiToolTip>
</Suspense>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>{connector.name}</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
<strong>{actionType?.name}</strong>
</EuiText>
</EuiFlexItem>
{warning && !isOpen && (
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<EuiBadge data-test-subj="warning-badge" iconType="warning" color="warning">
{ACTION_WARNING_TITLE}
</EuiBadge>
<EuiText size="s">{connector.name}</EuiText>
</EuiFlexItem>
)}
{actionTypeModel.isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
alignment="middle"
data-test-subj="ruleActionsSystemActionsItemBetaBadge"
label={TECH_PREVIEW_LABEL}
tooltipContent={TECH_PREVIEW_DESCRIPTION}
/>
</EuiFlexItem>
)}
{actionTypeModel.isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
size="s"
alignment="middle"
data-test-subj="ruleActionsSystemActionsItemBetaBadge"
iconType="beaker"
label={TECH_PREVIEW_LABEL}
tooltipContent={TECH_PREVIEW_DESCRIPTION}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs" responsive={false}>
{warning && !isOpen && (
<EuiFlexItem grow={false}>
<EuiToolTip content={ACTION_WARNING_TITLE}>
<EuiBadge data-test-subj="warning-badge" iconType="warning" color="warning" />
</EuiToolTip>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
}

View file

@ -121,7 +121,7 @@ export const RuleSchedule = () => {
isInvalid={hasIntervalError}
error={baseErrors?.interval}
>
<EuiFlexGroup gutterSize="s">
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem grow={2}>
<EuiFieldNumber
fullWidth

View file

@ -128,6 +128,9 @@ describe('ruleFlyout', () => {
fireEvent.click(screen.getByTestId('ruleFlyoutFooterSaveButton'));
expect(await screen.findByTestId('confirmCreateRuleModal')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
expect(onSave).toHaveBeenCalledWith({
...formDataMock,
consumer: 'logs',
@ -140,4 +143,33 @@ describe('ruleFlyout', () => {
fireEvent.click(screen.getByTestId('ruleFlyoutFooterCancelButton'));
expect(onCancel).toHaveBeenCalled();
});
test('should display discard changes modal only if changes are made in the form', () => {
useRuleFormState.mockReturnValue({
plugins: {
application: {
navigateToUrl,
capabilities: {
actions: {
show: true,
save: true,
execute: true,
},
},
},
},
baseErrors: {},
paramsErrors: {},
touched: true,
formData: formDataMock,
connectors: [],
connectorTypes: [],
aadTemplateFields: [],
});
render(<RuleFlyout onCancel={onCancel} onSave={onSave} />);
fireEvent.click(screen.getByTestId('ruleFlyoutFooterCancelButton'));
expect(screen.getByTestId('confirmRuleCloseModal')).toBeInTheDocument();
});
});

View file

@ -7,29 +7,40 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiFlyout, EuiPortal } from '@elastic/eui';
import React, { useState, useCallback, useMemo } from 'react';
import type { RuleFormData } from '../types';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useRuleFlyoutUIContext } from '../../lib';
import type { RuleFormData, RuleTypeMetaData } from '../types';
import { RuleFormStepId } from '../constants';
import { RuleFlyoutBody } from './rule_flyout_body';
import { RuleFlyoutShowRequest } from './rule_flyout_show_request';
import { useRuleFormScreenContext } from '../hooks';
import { useRuleFormScreenContext, useRuleFormState } from '../hooks';
import { RuleFlyoutSelectConnector } from './rule_flyout_select_connector';
import { ConfirmRuleClose } from '../components';
interface RuleFlyoutProps {
isEdit?: boolean;
isSaving?: boolean;
onCancel?: () => void;
onSave: (formData: RuleFormData) => void;
onChangeMetaData?: (metadata?: RuleTypeMetaData) => void;
}
// This component is only responsible for the CONTENT of the EuiFlyout. See `flyout/rule_form_flyout.tsx` for the
// EuiFlyout itself. This separation is necessary so that the flyout code can be lazy-loaded and still present its loading
// state and finished state within the same EuiFlyout.
export const RuleFlyout = ({
onSave,
isEdit = false,
isSaving = false,
onCancel = () => {},
// Input is named onCancel for consistency with RulePage but rename it to onClose for more clarity on its
// function within the flyout. This avoids the compulsion to name a function something like onCancelCancel when
// we're displaying the confirmation modal for closing the flyout.
onCancel: onClose = () => {},
onChangeMetaData = () => {},
}: RuleFlyoutProps) => {
const [initialStep, setInitialStep] = useState<RuleFormStepId | undefined>(undefined);
const [isConfirmCloseModalVisible, setIsConfirmCloseModalVisible] = useState(false);
const {
isConnectorsScreenVisible,
@ -51,37 +62,52 @@ export const RuleFlyout = ({
setIsShowRequestScreenVisible(false);
}, [setIsShowRequestScreenVisible]);
const onCancelClose = useCallback(() => {
setIsConfirmCloseModalVisible(false);
}, []);
const hideCloseButton = useMemo(
() => isShowRequestScreenVisible || isConnectorsScreenVisible,
[isConnectorsScreenVisible, isShowRequestScreenVisible]
);
const { touched, onInteraction } = useRuleFormState();
const { setOnClickClose, setHideCloseButton } = useRuleFlyoutUIContext();
const onClickCloseOrCancelButton = useCallback(() => {
if (touched) {
setIsConfirmCloseModalVisible(true);
} else {
onClose();
}
}, [touched, setIsConfirmCloseModalVisible, onClose]);
useEffect(() => {
setOnClickClose(() => onClickCloseOrCancelButton);
setHideCloseButton(hideCloseButton);
}, [setOnClickClose, setHideCloseButton, onClickCloseOrCancelButton, hideCloseButton]);
return (
<EuiPortal>
<EuiFlyout
ownFocus
onClose={onCancel}
aria-labelledby="flyoutTitle"
size="m"
maxWidth={500}
className="ruleFormFlyout__container"
hideCloseButton={hideCloseButton}
>
{isShowRequestScreenVisible ? (
<RuleFlyoutShowRequest isEdit={isEdit} onClose={onCloseShowRequest} />
) : isConnectorsScreenVisible ? (
<RuleFlyoutSelectConnector onClose={onCloseConnectorsScreen} />
) : (
<RuleFlyoutBody
onSave={onSave}
onCancel={onCancel}
isEdit={isEdit}
isSaving={isSaving}
onShowRequest={onOpenShowRequest}
initialStep={initialStep}
/>
)}
</EuiFlyout>
</EuiPortal>
<>
{isShowRequestScreenVisible ? (
<RuleFlyoutShowRequest isEdit={isEdit} onClose={onCloseShowRequest} />
) : isConnectorsScreenVisible ? (
<RuleFlyoutSelectConnector onClose={onCloseConnectorsScreen} />
) : (
<RuleFlyoutBody
onSave={onSave}
onInteraction={onInteraction}
onCancel={onClickCloseOrCancelButton}
isEdit={isEdit}
isSaving={isSaving}
onShowRequest={onOpenShowRequest}
initialStep={initialStep}
onChangeMetaData={onChangeMetaData}
/>
)}
{isConfirmCloseModalVisible && (
<ConfirmRuleClose onCancel={onCancelClose} onConfirm={onClose} />
)}
</>
);
};

View file

@ -8,34 +8,38 @@
*/
import {
EuiCallOut,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiSpacer,
EuiStepsHorizontal,
EuiTitle,
EuiSpacer,
EuiCallOut,
} from '@elastic/eui';
import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared';
import React, { useCallback, useMemo } from 'react';
import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RuleFormStepId } from '../constants';
import { useRuleFormHorizontalSteps, useRuleFormState } from '../hooks';
import {
DISABLED_ACTIONS_WARNING_TITLE,
RULE_FLYOUT_HEADER_CREATE_TITLE,
RULE_FLYOUT_HEADER_EDIT_TITLE,
DISABLED_ACTIONS_WARNING_TITLE,
} from '../translations';
import type { RuleFormData } from '../types';
import type { RuleFormData, RuleFormState } from '../types';
import { hasRuleErrors } from '../validation';
import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer';
import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer';
import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs';
import { RuleFormStepId } from '../constants';
import { ConfirmCreateRule } from '../components';
interface RuleFlyoutBodyProps {
isEdit?: boolean;
isSaving?: boolean;
onCancel: () => void;
onSave: (formData: RuleFormData) => void;
onInteraction: () => void;
onShowRequest: () => void;
onChangeMetaData?: (metadata?: RuleFormState['metadata']) => void;
initialStep?: RuleFormStepId;
}
@ -45,8 +49,12 @@ export const RuleFlyoutBody = ({
initialStep,
onCancel,
onSave,
onInteraction,
onShowRequest,
onChangeMetaData = () => {},
}: RuleFlyoutBodyProps) => {
const [showCreateConfirmation, setShowCreateConfirmation] = useState<boolean>(false);
const {
formData,
multiConsumerSelection,
@ -56,8 +64,15 @@ export const RuleFlyoutBody = ({
paramsErrors = {},
actionsErrors = {},
actionsParamsErrors = {},
metadata = {},
} = useRuleFormState();
useEffect(() => {
if (!isEmpty(metadata)) {
onChangeMetaData(metadata);
}
}, [metadata, onChangeMetaData]);
const hasErrors = useMemo(() => {
const hasBrokenConnectors = formData.actions.some((action) => {
return !connectors.find((connector) => connector.id === action.id);
@ -85,14 +100,6 @@ export const RuleFlyoutBody = ({
} = useRuleFormHorizontalSteps(initialStep);
const { actions } = formData;
const onSaveInternal = useCallback(() => {
onSave({
...formData,
...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}),
});
}, [onSave, formData, multiConsumerSelection]);
const hasActionsDisabled = useMemo(() => {
const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured);
return actions.some((action) => {
@ -108,17 +115,41 @@ export const RuleFlyoutBody = ({
});
}, [actions, connectors, connectorTypes]);
const onSaveInternal = useCallback(() => {
onSave({
...formData,
...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}),
});
}, [onSave, formData, multiConsumerSelection]);
const onClickSave = useCallback(() => {
if (!hasActionsDisabled && actions.length === 0) {
setShowCreateConfirmation(true);
} else {
onSaveInternal();
}
}, [actions.length, hasActionsDisabled, onSaveInternal]);
const onCreateConfirmClick = useCallback(() => {
setShowCreateConfirmation(false);
onSaveInternal();
}, [onSaveInternal]);
const onCreateCancelClick = useCallback(() => {
setShowCreateConfirmation(false);
}, []);
return (
<>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s" data-test-subj={isEdit ? 'editRuleFlyoutTitle' : 'addRuleFlyoutTitle'}>
<h3 id="flyoutTitle">
<h3 id="flyoutTitle" data-test-subj="ruleFlyoutTitle">
{isEdit ? RULE_FLYOUT_HEADER_EDIT_TITLE : RULE_FLYOUT_HEADER_CREATE_TITLE}
</h3>
</EuiTitle>
{isEdit && <RuleFlyoutEditTabs steps={steps} />}
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlyoutBody onClick={onInteraction} onKeyDown={onInteraction}>
{!isEdit && <EuiStepsHorizontal size="xs" steps={steps} />}
{hasActionsDisabled && (
<>
@ -137,7 +168,7 @@ export const RuleFlyoutBody = ({
{isEdit ? (
<RuleFlyoutEditFooter
onCancel={onCancel}
onSave={onSaveInternal}
onSave={onClickSave}
onShowRequest={onShowRequest}
isSaving={isSaving}
hasErrors={hasErrors}
@ -145,7 +176,7 @@ export const RuleFlyoutBody = ({
) : (
<RuleFlyoutCreateFooter
onCancel={onCancel}
onSave={onSaveInternal}
onSave={onClickSave}
onShowRequest={onShowRequest}
goToNextStep={goToNextStep}
goToPreviousStep={goToPreviousStep}
@ -155,6 +186,9 @@ export const RuleFlyoutBody = ({
hasErrors={hasErrors}
/>
)}
{showCreateConfirmation && (
<ConfirmCreateRule onConfirm={onCreateConfirmClick} onCancel={onCreateCancelClick} />
)}
</>
);
};

View file

@ -39,7 +39,7 @@ export const RuleFlyoutEditFooter = ({
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelSaveRuleButton" onClick={onCancel}>
<EuiButtonEmpty data-test-subj="ruleFlyoutFooterCancelButton" onClick={onCancel}>
{RULE_FLYOUT_FOOTER_CANCEL_TEXT}
</EuiButtonEmpty>
</EuiFlexItem>
@ -60,7 +60,7 @@ export const RuleFlyoutEditFooter = ({
<EuiFlexItem grow={false}>
<EuiButton
fill
data-test-subj="saveRuleButton"
data-test-subj="ruleFlyoutFooterSaveButton"
type="submit"
isDisabled={isSaving || hasErrors}
isLoading={isSaving}

View file

@ -26,7 +26,7 @@ import {
RULE_FLYOUT_FOOTER_BACK_TEXT,
RULE_FLYOUT_HEADER_BACK_TEXT,
} from '../translations';
import { RequestCodeBlock } from '../request_code_block';
import { RequestCodeBlock } from '../components';
interface RuleFlyoutShowRequestProps {
isEdit: boolean;

View file

@ -7,45 +7,68 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useMemo } from 'react';
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
import React, { useMemo } from 'react';
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';
import {
RULE_FORM_ROUTE_PARAMS_ERROR_TITLE,
RULE_FORM_ROUTE_PARAMS_ERROR_TEXT,
} from './translations';
import { RuleFormPlugins } from './types';
import './rule_form.scss';
import { RuleFormScreenContextProvider } from './rule_form_screen_context';
import {
RULE_FORM_ROUTE_PARAMS_ERROR_TEXT,
RULE_FORM_ROUTE_PARAMS_ERROR_TITLE,
} from './translations';
import { RuleFormData, RuleFormPlugins, RuleTypeMetaData } from './types';
const queryClient = new QueryClient();
export interface RuleFormProps {
export interface RuleFormProps<MetaData extends RuleTypeMetaData = RuleTypeMetaData> {
plugins: RuleFormPlugins;
id?: string;
ruleTypeId?: string;
isFlyout?: boolean;
onCancel?: () => void;
onSubmit?: (ruleId: string) => void;
validConsumers?: RuleCreationValidConsumer[];
onChangeMetaData?: (metadata: MetaData) => void;
consumer?: string;
connectorFeatureId?: string;
multiConsumerSelection?: RuleCreationValidConsumer | null;
hideInterval?: boolean;
validConsumers?: RuleCreationValidConsumer[];
filteredRuleTypes?: string[];
shouldUseRuleProducer?: boolean;
canShowConsumerSelection?: boolean;
showMustacheAutocompleteSwitch?: boolean;
initialValues?: Partial<Omit<RuleFormData, 'ruleTypeId'>>;
initialMetadata?: MetaData;
isServerless?: boolean;
}
export const RuleForm = (props: RuleFormProps) => {
export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
props: RuleFormProps<MetaData>
) => {
const {
plugins: _plugins,
onCancel,
onSubmit,
validConsumers,
onChangeMetaData,
id,
ruleTypeId,
isFlyout,
consumer,
connectorFeatureId,
multiConsumerSelection,
hideInterval,
validConsumers,
filteredRuleTypes,
shouldUseRuleProducer,
canShowConsumerSelection,
showMustacheAutocompleteSwitch,
initialValues,
initialMetadata,
isServerless,
} = props;
const { id, ruleTypeId } = useParams<{
id?: string;
ruleTypeId?: string;
}>();
const {
http,
@ -62,6 +85,7 @@ export const RuleForm = (props: RuleFormProps) => {
docLinks,
ruleTypeRegistry,
actionTypeRegistry,
fieldsMetadata,
} = _plugins;
const ruleFormComponent = useMemo(() => {
@ -80,9 +104,27 @@ export const RuleForm = (props: RuleFormProps) => {
docLinks,
ruleTypeRegistry,
actionTypeRegistry,
fieldsMetadata,
};
// Passing the MetaData type all the way down the component hierarchy is unnecessary, this type is
// only used for the benefit of consumers of the RuleForm component. Retype onChangeMetaData to ignore this type.
const retypedOnChangeMetaData = onChangeMetaData as (metadata?: RuleTypeMetaData) => void;
if (id) {
return <EditRuleForm id={id} plugins={plugins} onCancel={onCancel} onSubmit={onSubmit} />;
return (
<EditRuleForm
id={id}
plugins={plugins}
onCancel={onCancel}
onSubmit={onSubmit}
onChangeMetaData={retypedOnChangeMetaData}
isFlyout={isFlyout}
showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch}
connectorFeatureId={connectorFeatureId}
initialMetadata={initialMetadata}
/>
);
}
if (ruleTypeId) {
return (
@ -91,8 +133,19 @@ export const RuleForm = (props: RuleFormProps) => {
plugins={plugins}
onCancel={onCancel}
onSubmit={onSubmit}
validConsumers={validConsumers}
onChangeMetaData={retypedOnChangeMetaData}
isFlyout={isFlyout}
consumer={consumer}
connectorFeatureId={connectorFeatureId}
multiConsumerSelection={multiConsumerSelection}
hideInterval={hideInterval}
validConsumers={validConsumers}
filteredRuleTypes={filteredRuleTypes}
shouldUseRuleProducer={shouldUseRuleProducer}
canShowConsumerSelection={canShowConsumerSelection}
showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch}
initialValues={initialValues}
initialMetadata={initialMetadata}
isServerless={isServerless}
/>
);
@ -124,20 +177,30 @@ export const RuleForm = (props: RuleFormProps) => {
docLinks,
ruleTypeRegistry,
actionTypeRegistry,
isServerless,
id,
ruleTypeId,
validConsumers,
multiConsumerSelection,
isServerless,
onCancel,
onSubmit,
onChangeMetaData,
isFlyout,
showMustacheAutocompleteSwitch,
connectorFeatureId,
initialMetadata,
consumer,
hideInterval,
filteredRuleTypes,
shouldUseRuleProducer,
canShowConsumerSelection,
initialValues,
fieldsMetadata,
]);
return (
<QueryClientProvider client={queryClient}>
<RuleFormScreenContextProvider>
<div className="ruleForm__container">{ruleFormComponent}</div>
</RuleFormScreenContextProvider>
<RuleFormScreenContextProvider>{ruleFormComponent}</RuleFormScreenContextProvider>
</QueryClientProvider>
);
};

View file

@ -11,5 +11,5 @@ export * from './rule_form_health_check_error';
export * from './rule_form_circuit_breaker_error';
export * from './rule_form_resolve_rule_error';
export * from './rule_form_rule_type_error';
export * from './rule_form_error_prompt_wrapper';
export * from './rule_form_action_permission_error';
export { RuleFormErrorPromptWrapper } from '../../lib';

View file

@ -11,7 +11,13 @@ import { createContext } from 'react';
import type { RuleFormState } from '../types';
import type { RuleFormStateReducerAction } from './rule_form_state_reducer';
export const RuleFormStateContext = createContext<RuleFormState>({} as RuleFormState);
type RuleFormStateWithInteractHandler = RuleFormState & {
onInteraction: () => void;
};
export const RuleFormStateContext = createContext<RuleFormStateWithInteractHandler>(
{} as RuleFormStateWithInteractHandler
);
export const RuleFormReducerContext = createContext<React.Dispatch<RuleFormStateReducerAction>>(
() => {}

View file

@ -7,10 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useReducer } from 'react';
import React, { useReducer, useState, useCallback } from 'react';
import { RuleFormState } from '../types';
import { RuleFormStateContext, RuleFormReducerContext } from './rule_form_state_context';
import { ruleFormStateReducer } from './rule_form_state_reducer';
import { RuleFormStateReducerAction, ruleFormStateReducer } from './rule_form_state_reducer';
import { validateRuleBase, validateRuleParams } from '../validation';
export interface RuleFormStateProviderProps {
@ -20,6 +20,12 @@ export interface RuleFormStateProviderProps {
export const RuleFormStateProvider: React.FC<
React.PropsWithChildren<RuleFormStateProviderProps>
> = (props) => {
// Tracking whether the user has changed the form is unreliable if we base it only on the difference
// between initial data and current data, as many rule types will use reducer actions to set their initial data.
// We need to track whether the user has actually physically interacted with the form before the ruleFormStateReducer
// can accurately determine the `touched` state
const [hasUserInteracted, setHasUserInteracted] = useState(false);
const { children, initialRuleFormState } = props;
const {
formData,
@ -27,7 +33,7 @@ export const RuleFormStateProvider: React.FC<
minimumScheduleInterval,
} = initialRuleFormState;
const [ruleFormState, dispatch] = useReducer(ruleFormStateReducer, {
const [ruleFormState, baseDispatch] = useReducer(ruleFormStateReducer, {
...initialRuleFormState,
baseErrors: validateRuleBase({
formData,
@ -38,8 +44,25 @@ export const RuleFormStateProvider: React.FC<
ruleTypeModel,
}),
});
// Prime the dispatch function to set `touched` to true on the next action, but not yet
const onInteraction = useCallback(() => {
if (!hasUserInteracted) setHasUserInteracted(true);
}, [hasUserInteracted]);
const dispatch: React.Dispatch<RuleFormStateReducerAction> = useCallback(
(...args) => {
// If the user has interacted with the form and the `touched` state is false, first update it to be true
// before executing the next action
if (hasUserInteracted && !ruleFormState.touched) {
baseDispatch({ type: 'setTouched' });
}
baseDispatch(...args);
},
[baseDispatch, hasUserInteracted, ruleFormState.touched]
);
return (
<RuleFormStateContext.Provider value={ruleFormState}>
<RuleFormStateContext.Provider value={{ ...ruleFormState, onInteraction }}>
<RuleFormReducerContext.Provider value={dispatch}>{children}</RuleFormReducerContext.Provider>
</RuleFormStateContext.Provider>
);

View file

@ -8,7 +8,7 @@
*/
import { RuleActionParams } from '@kbn/alerting-types';
import { isEmpty, omit, isEqual } from 'lodash';
import { isEmpty, omit } from 'lodash';
import { RuleFormActionsErrors, RuleFormParamsErrors, RuleUiAction } from '../common';
import { RuleFormData, RuleFormState } from '../types';
import { validateRuleBase, validateRuleParams } from '../validation';
@ -109,7 +109,8 @@ export type RuleFormStateReducerAction =
}
| {
type: 'runValidation';
};
}
| { type: 'setTouched' };
const getUpdateWithValidation =
(ruleFormState: RuleFormState) =>
@ -119,7 +120,6 @@ const getUpdateWithValidation =
selectedRuleTypeModel,
multiConsumerSelection,
selectedRuleType,
formData: originalFormData,
} = ruleFormState;
const formData = updater();
@ -150,14 +150,11 @@ const getUpdateWithValidation =
}
}
const touched = !isEqual(originalFormData, formData);
return {
...ruleFormState,
formData,
baseErrors,
paramsErrors,
touched,
};
};
@ -356,6 +353,12 @@ export const ruleFormStateReducer = (
case 'runValidation': {
return updateWithValidation(() => formData);
}
case 'setTouched': {
return {
...ruleFormState,
touched: true,
};
}
default: {
return ruleFormState;
}

View file

@ -10,4 +10,3 @@
export * from './rule_page';
export * from './rule_page_name_input';
export * from './rule_page_footer';
export * from './rule_page_confirm_create_rule';

View file

@ -10,7 +10,6 @@
import {
EuiButtonEmpty,
EuiCallOut,
EuiConfirmModal,
EuiFlexGroup,
EuiFlexItem,
EuiPageTemplate,
@ -21,19 +20,13 @@ import {
import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared';
import React, { useCallback, useMemo, useState } from 'react';
import { useRuleFormScreenContext, useRuleFormState, useRuleFormSteps } from '../hooks';
import {
DISABLED_ACTIONS_WARNING_TITLE,
RULE_FORM_CANCEL_MODAL_CANCEL,
RULE_FORM_CANCEL_MODAL_CONFIRM,
RULE_FORM_CANCEL_MODAL_DESCRIPTION,
RULE_FORM_CANCEL_MODAL_TITLE,
RULE_FORM_RETURN_TITLE,
} from '../translations';
import { DISABLED_ACTIONS_WARNING_TITLE, RULE_FORM_RETURN_TITLE } from '../translations';
import type { RuleFormData } from '../types';
import { RulePageFooter } from './rule_page_footer';
import { RulePageNameInput } from './rule_page_name_input';
import { RuleActionsConnectorsModal } from '../rule_actions/rule_actions_connectors_modal';
import { RulePageShowRequestModal } from './rule_page_show_request_modal';
import { ConfirmRuleClose } from '../components';
export interface RulePageProps {
isEdit?: boolean;
@ -46,7 +39,7 @@ export const RulePage = (props: RulePageProps) => {
const { isEdit = false, isSaving = false, onCancel = () => {}, onSave } = props;
const [isCancelModalOpen, setIsCancelModalOpen] = useState<boolean>(false);
const { formData, multiConsumerSelection, connectorTypes, connectors, touched } =
const { formData, multiConsumerSelection, connectorTypes, connectors, touched, onInteraction } =
useRuleFormState();
const { steps } = useRuleFormSteps();
@ -89,7 +82,16 @@ export const RulePage = (props: RulePageProps) => {
return (
<>
<EuiPageTemplate grow bottomBorder offset={0} css={styles}>
<EuiPageTemplate
grow
bottomBorder
offset={0}
css={styles}
onClick={onInteraction}
onKeyDown={onInteraction}
className="ruleForm__container"
data-test-subj="ruleForm"
>
<EuiPageTemplate.Header>
<EuiFlexGroup
direction="column"
@ -140,18 +142,7 @@ export const RulePage = (props: RulePageProps) => {
</EuiPageTemplate.Section>
</EuiPageTemplate>
{isCancelModalOpen && (
<EuiConfirmModal
onCancel={() => setIsCancelModalOpen(false)}
onConfirm={onCancel}
data-test-subj="confirmRuleCloseModal"
buttonColor="danger"
defaultFocusedButton="confirm"
title={RULE_FORM_CANCEL_MODAL_TITLE}
confirmButtonText={RULE_FORM_CANCEL_MODAL_CONFIRM}
cancelButtonText={RULE_FORM_CANCEL_MODAL_CANCEL}
>
<p>{RULE_FORM_CANCEL_MODAL_DESCRIPTION}</p>
</EuiConfirmModal>
<ConfirmRuleClose onCancel={() => setIsCancelModalOpen(false)} onConfirm={onCancel} />
)}
{isConnectorsScreenVisible && <RuleActionsConnectorsModal />}
{isShowRequestScreenVisible && <RulePageShowRequestModal isEdit={isEdit} />}

View file

@ -1,49 +0,0 @@
/*
* 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 { fireEvent, render, screen } from '@testing-library/react';
import { RulePageConfirmCreateRule } from './rule_page_confirm_create_rule';
import {
CONFIRM_RULE_SAVE_CONFIRM_BUTTON_TEXT,
CONFIRM_RULE_SAVE_CANCEL_BUTTON_TEXT,
CONFIRM_RULE_SAVE_MESSAGE_TEXT,
} from '../translations';
const onConfirmMock = jest.fn();
const onCancelMock = jest.fn();
describe('rulePageConfirmCreateRule', () => {
afterEach(() => {
jest.clearAllMocks();
});
test('renders correctly', () => {
render(<RulePageConfirmCreateRule onConfirm={onConfirmMock} onCancel={onCancelMock} />);
expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument();
expect(screen.getByText(CONFIRM_RULE_SAVE_CONFIRM_BUTTON_TEXT)).toBeInTheDocument();
expect(screen.getByText(CONFIRM_RULE_SAVE_CANCEL_BUTTON_TEXT)).toBeInTheDocument();
expect(screen.getByText(CONFIRM_RULE_SAVE_MESSAGE_TEXT)).toBeInTheDocument();
});
test('can confirm rule creation', () => {
render(<RulePageConfirmCreateRule onConfirm={onConfirmMock} onCancel={onCancelMock} />);
fireEvent.click(screen.getByTestId('confirmModalConfirmButton'));
expect(onConfirmMock).toHaveBeenCalled();
});
test('can cancel rule creation', () => {
render(<RulePageConfirmCreateRule onConfirm={onConfirmMock} onCancel={onCancelMock} />);
fireEvent.click(screen.getByTestId('confirmModalCancelButton'));
expect(onCancelMock).toHaveBeenCalled();
});
});

View file

@ -90,7 +90,7 @@ describe('rulePageFooter', () => {
render(<RulePageFooter onSave={onSave} onCancel={onCancel} />);
fireEvent.click(screen.getByTestId('rulePageFooterSaveButton'));
expect(screen.getByTestId('rulePageConfirmCreateRule')).toBeInTheDocument();
expect(screen.getByTestId('confirmCreateRuleModal')).toBeInTheDocument();
});
test('should not show creat rule confirmation if user cannot read actions', () => {
@ -113,7 +113,7 @@ describe('rulePageFooter', () => {
render(<RulePageFooter onSave={onSave} onCancel={onCancel} />);
fireEvent.click(screen.getByTestId('rulePageFooterSaveButton'));
expect(screen.queryByTestId('rulePageConfirmCreateRule')).not.toBeInTheDocument();
expect(screen.queryByTestId('confirmCreateRuleModal')).not.toBeInTheDocument();
expect(onSave).toHaveBeenCalled();
});

View file

@ -17,7 +17,7 @@ import {
} from '../translations';
import { useRuleFormScreenContext, useRuleFormState } from '../hooks';
import { hasRuleErrors } from '../validation';
import { RulePageConfirmCreateRule } from './rule_page_confirm_create_rule';
import { ConfirmCreateRule } from '../components';
export interface RulePageFooterProps {
isEdit?: boolean;
@ -131,10 +131,7 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
</EuiFlexItem>
</EuiFlexGroup>
{showCreateConfirmation && (
<RulePageConfirmCreateRule
onConfirm={onCreateConfirmClick}
onCancel={onCreateCancelClick}
/>
<ConfirmCreateRule onConfirm={onCreateConfirmClick} onCancel={onCreateCancelClick} />
)}
</>
);

View file

@ -18,7 +18,7 @@ import {
EuiTextColor,
} from '@elastic/eui';
import React, { useCallback } from 'react';
import { RequestCodeBlock } from '../request_code_block';
import { RequestCodeBlock } from '../components';
import { SHOW_REQUEST_MODAL_SUBTITLE, SHOW_REQUEST_MODAL_TITLE } from '../translations';
import { useRuleFormScreenContext } from '../hooks';

View file

@ -638,7 +638,7 @@ export const RULE_FORM_CANCEL_MODAL_CONFIRM = i18n.translate(
export const RULE_FORM_CANCEL_MODAL_CANCEL = i18n.translate(
'responseOpsRuleForm.ruleForm.ruleFormCancelModalCancel',
{
defaultMessage: 'Cancel',
defaultMessage: 'Keep editing',
}
);
@ -793,3 +793,9 @@ export const SHOW_REQUEST_MODAL_ERROR = i18n.translate(
defaultMessage: 'Sorry about that, something went wrong.',
}
);
export const DEFAULT_RULE_NAME = (ruleTypeName: string) =>
i18n.translate('responseOpsRuleForm.ruleForm.defaultRuleName', {
defaultMessage: `{ruleTypeName} rule`,
values: { ruleTypeName },
});

View file

@ -9,6 +9,7 @@
import { ActionType } from '@kbn/actions-types';
import { ActionVariable, RulesSettingsFlapping } from '@kbn/alerting-types';
import type { ActionConnector, ActionTypeRegistryContract } from '@kbn/alerts-ui-shared';
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { ApplicationStart } from '@kbn/core-application-browser';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
@ -16,19 +17,20 @@ import type { HttpStart } from '@kbn/core-http-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import type { NotificationsStart } from '@kbn/core-notifications-browser';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { UserProfileService } from '@kbn/core-user-profile-browser';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { UserProfileService } from '@kbn/core-user-profile-browser';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { ActionConnector, ActionTypeRegistryContract } from '@kbn/alerts-ui-shared';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import {
MinimumScheduleInterval,
Rule,
RuleFormActionsErrors,
RuleFormBaseErrors,
RuleFormParamsErrors,
RuleTypeMetaData,
RuleTypeModel,
RuleTypeParams,
RuleTypeRegistryContract,
@ -67,9 +69,13 @@ export interface RuleFormPlugins {
docLinks: DocLinksStart;
ruleTypeRegistry: RuleTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
fieldsMetadata: FieldsMetadataPublicStart;
}
export interface RuleFormState<Params extends RuleTypeParams = RuleTypeParams> {
export interface RuleFormState<
Params extends RuleTypeParams = RuleTypeParams,
MetaData = RuleTypeMetaData
> {
id?: string;
formData: RuleFormData<Params>;
plugins: RuleFormPlugins;
@ -85,7 +91,7 @@ export interface RuleFormState<Params extends RuleTypeParams = RuleTypeParams> {
selectedRuleTypeModel: RuleTypeModel<Params>;
multiConsumerSelection?: RuleCreationValidConsumer | null;
showMustacheAutocompleteSwitch?: boolean;
metadata?: Record<string, unknown>;
metadata?: MetaData;
minimumScheduleInterval?: MinimumScheduleInterval;
canShowConsumerSelection?: boolean;
validConsumers: RuleCreationValidConsumer[];

View file

@ -30,6 +30,7 @@
"@kbn/kibana-react-plugin",
"@kbn/core-i18n-browser",
"@kbn/core-theme-browser",
"@kbn/core-user-profile-browser"
"@kbn/core-user-profile-browser",
"@kbn/fields-metadata-plugin"
]
}

View file

@ -22,6 +22,8 @@ import {
STACK_ALERTS_FEATURE_ID,
} from '@kbn/rule-data-utils';
import { RuleTypeMetaData } from '@kbn/alerting-plugin/common';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { isValidRuleFormPlugins } from '@kbn/response-ops-rule-form/lib';
import { DiscoverStateContainer } from '../../../state_management/discover_state';
import { AppMenuDiscoverParams } from './types';
import { DiscoverServices } from '../../../../../build_services';
@ -38,6 +40,8 @@ interface EsQueryAlertMetaData extends RuleTypeMetaData {
adHocDataViewList: DataView[];
}
const RuleFormFlyoutWithType = RuleFormFlyout<EsQueryAlertMetaData>;
const CreateAlertFlyout: React.FC<{
discoverParams: AppMenuDiscoverParams;
services: DiscoverServices;
@ -47,7 +51,9 @@ const CreateAlertFlyout: React.FC<{
const query = stateContainer.appState.getState().query;
const { dataView, isEsqlMode, adHocDataViews, onUpdateAdHocDataViews } = discoverParams;
const { triggersActionsUi } = services;
const {
triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry },
} = services;
const timeField = getTimeField(dataView);
/**
@ -79,24 +85,33 @@ const CreateAlertFlyout: React.FC<{
[adHocDataViews]
);
return triggersActionsUi?.getAddRuleFlyout({
metadata: discoverMetadata,
consumer: 'alerts',
onClose: (_, metadata) => {
onUpdateAdHocDataViews(metadata!.adHocDataViewList);
onFinishAction();
},
onSave: async (metadata) => {
onUpdateAdHocDataViews(metadata!.adHocDataViewList);
},
canChangeTrigger: false,
ruleTypeId: ES_QUERY_ID,
initialValues: { params: getParams() },
validConsumers: EsQueryValidConsumer,
useRuleProducer: true,
// Default to the Logs consumer if it's available. This should fall back to Stack Alerts if it's not.
initialSelectedConsumer: AlertConsumers.LOGS,
});
// Some of the rule form's required plugins are from x-pack, so make sure they're defined before
// rendering the flyout. The alerting plugin is also part of x-pack, so this check should probably never
// return false. This is mostly here because Typescript requires us to mark x-pack plugins as optional.
if (!isValidRuleFormPlugins(services)) return null;
return (
<RuleFormFlyoutWithType
plugins={{
...services,
ruleTypeRegistry,
actionTypeRegistry,
}}
initialMetadata={discoverMetadata}
consumer={'alerts'}
onCancel={onFinishAction}
onSubmit={onFinishAction}
onChangeMetaData={(metadata: EsQueryAlertMetaData) =>
onUpdateAdHocDataViews(metadata.adHocDataViewList)
}
ruleTypeId={ES_QUERY_ID}
initialValues={{ params: getParams() }}
validConsumers={EsQueryValidConsumer}
shouldUseRuleProducer
// Default to the Logs consumer if it's available. This should fall back to Stack Alerts if it's not.
multiConsumerSelection={AlertConsumers.LOGS}
/>
);
};
export const getAlertsAppMenuItem = ({

View file

@ -7,11 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { AppMenuActionId, AppMenuActionType, AppMenuRegistry } from '@kbn/discover-utils';
import { DATA_QUALITY_LOCATOR_ID, DataQualityLocatorParams } from '@kbn/deeplinks-observability';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { isOfQueryType } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { isValidRuleFormPlugins } from '@kbn/response-ops-rule-form/lib';
import { AppMenuExtensionParams } from '../../../..';
import type { RootProfileProvider } from '../../../../profiles';
import { ProfileProviderServices } from '../../../profile_provider_services';
@ -74,7 +77,11 @@ const registerDatasetQualityLink = (
const registerCustomThresholdRuleAction = (
registry: AppMenuRegistry,
{ data, triggersActionsUi }: ProfileProviderServices,
{
data,
triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry },
...services
}: ProfileProviderServices,
{ dataView }: AppMenuExtensionParams
) => {
registry.registerCustomActionUnderSubmenu(AppMenuActionId.alerts, {
@ -91,21 +98,34 @@ const registerCustomThresholdRuleAction = (
const index = dataView?.toMinimalSpec();
const { filters, query } = data.query.getState();
return triggersActionsUi.getAddRuleFlyout({
consumer: 'logs',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
canChangeTrigger: false,
initialValues: {
params: {
searchConfiguration: {
index,
query,
filter: filters,
// Some of the rule form's required plugins are from x-pack, so make sure they're defined before
// rendering the flyout. The alerting plugin is also part of x-pack, so this check should probably never
// return false. This is mostly here because Typescript requires us to mark x-pack plugins as optional.
const plugins = { ...services, data };
if (!isValidRuleFormPlugins(plugins)) return null;
return (
<RuleFormFlyout
plugins={{
...plugins,
ruleTypeRegistry,
actionTypeRegistry,
}}
consumer={'logs'}
ruleTypeId={OBSERVABILITY_THRESHOLD_RULE_TYPE_ID}
initialValues={{
params: {
searchConfiguration: {
index,
query,
filter: filters,
},
},
},
},
onClose: onFinishAction,
});
}}
onSubmit={onFinishAction}
onCancel={onFinishAction}
/>
);
},
},
});

View file

@ -100,7 +100,8 @@
"@kbn/esql-ast",
"@kbn/discover-shared-plugin",
"@kbn/embeddable-enhanced-plugin",
"@kbn/ui-theme"
"@kbn/ui-theme",
"@kbn/response-ops-rule-form",
],
"exclude": ["target/**/*"]
}

View file

@ -14,6 +14,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
return {
...functionalConfig.getAll(),
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs'),
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // required for alerts plugin to work
],
},
testFiles: [require.resolve('.')],
};
}

View file

@ -14,10 +14,11 @@
"actions",
"kibanaReact",
"features",
"developerExamples"
"developerExamples",
"unifiedSearch",
"dataViews",
"fieldsMetadata"
],
"requiredBundles": [
"kibanaReact"
]
"requiredBundles": ["kibanaReact"]
}
}

View file

@ -6,17 +6,34 @@
*/
import React, { useState, useCallback } from 'react';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { EuiIcon, EuiFlexItem, EuiCard, EuiFlexGroup } from '@elastic/eui';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { AlertingExampleComponentParams } from '../application';
import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
type KibanaDeps = {
dataViews: DataViewsPublicPluginStart;
charts: ChartsPluginStart;
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
} & CoreStart;
export const CreateAlert = ({
triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout },
triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry },
}: Pick<AlertingExampleComponentParams, 'triggersActionsUi'>) => {
const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(false);
const { services } = useKibana<KibanaDeps>();
const onCloseAlertFlyout = useCallback(
() => setRuleFlyoutVisibility(false),
[setRuleFlyoutVisibility]
@ -34,7 +51,12 @@ export const CreateAlert = ({
</EuiFlexItem>
<EuiFlexItem>
{ruleFlyoutVisible ? (
<AddRuleFlyout consumer={ALERTING_EXAMPLE_APP_ID} onClose={onCloseAlertFlyout} />
<RuleFormFlyout
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
consumer={ALERTING_EXAMPLE_APP_ID}
onCancel={onCloseAlertFlyout}
onSubmit={onCloseAlertFlyout}
/>
) : null}
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -28,5 +28,9 @@
"@kbn/shared-ux-router",
"@kbn/config-schema",
"@kbn/alerts-as-data-utils",
"@kbn/data-views-plugin",
"@kbn/unified-search-plugin",
"@kbn/response-ops-rule-form",
"@kbn/fields-metadata-plugin",
]
}

View file

@ -19,7 +19,8 @@
"dataViewEditor",
"unifiedSearch",
"fieldFormats",
"licensing"
"licensing",
"fieldsMetadata"
],
"optionalPlugins": ["spaces"]
}

View file

@ -24,6 +24,7 @@ import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { CREATE_RULE_ROUTE, EDIT_RULE_ROUTE, RuleForm } from '@kbn/response-ops-rule-form';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { TriggersActionsUiExamplePublicStartDeps } from './plugin';
import { Page } from './components/page';
@ -58,6 +59,7 @@ export interface TriggersActionsUiExampleComponentParams {
unifiedSearch: UnifiedSearchPublicPluginStart;
fieldFormats: FieldFormatsStart;
licensing: LicensingPluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
const TriggersActionsUiExampleApp = ({
@ -284,6 +286,7 @@ export const renderApp = (
unifiedSearch={deps.unifiedSearch}
fieldFormats={deps.fieldFormats}
licensing={deps.licensing}
fieldsMetadata={deps.fieldsMetadata}
{...core}
/>
</IntlProvider>

View file

@ -19,6 +19,7 @@ import {
} from '@kbn/triggers-actions-ui-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { getConnectorType as getSystemLogExampleConnectorType } from './connector_types/system_log_example/system_log_example';
export interface TriggersActionsUiExamplePublicSetupDeps {
@ -37,6 +38,7 @@ export interface TriggersActionsUiExamplePublicStartDeps {
unifiedSearch: UnifiedSearchPublicPluginStart;
fieldFormats: FieldFormatsStart;
licensing: LicensingPluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
export class TriggersActionsUiExamplePlugin

View file

@ -34,5 +34,6 @@
"@kbn/field-formats-plugin",
"@kbn/licensing-plugin",
"@kbn/response-ops-rule-form",
"@kbn/fields-metadata-plugin",
]
}

View file

@ -42,7 +42,11 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) {
ObservabilityAIAssistantMultipaneFlyoutContext,
service: mockService,
},
triggersActionsUi: { getAddRuleFlyout: {}, getAddConnectorFlyout: {} },
triggersActionsUi: {
ruleTypeRegistry: {},
actionTypeRegistry: {},
getAddConnectorFlyout: {},
},
}}
>
<ObservabilityAIAssistantChatServiceContext.Provider value={mockChatService}>

View file

@ -1,18 +1,14 @@
{
"type": "plugin",
"id": "@kbn/monitoring-plugin",
"owner": [
"@elastic/stack-monitoring"
],
"owner": ["@elastic/stack-monitoring"],
"group": "platform",
"visibility": "private",
"plugin": {
"id": "monitoring",
"browser": true,
"server": true,
"configPath": [
"monitoring"
],
"configPath": ["monitoring"],
"requiredPlugins": [
"licensing",
"features",
@ -20,23 +16,21 @@
"navigation",
"dataViews",
"unifiedSearch",
"share"
"share",
"fieldsMetadata"
],
"optionalPlugins": [
"usageCollection",
"home",
"cloud",
"triggersActionsUi",
"charts",
"alerting",
"actions",
"encryptedSavedObjects",
"dashboard",
"fleet"
],
"requiredBundles": [
"kibanaUtils",
"alerting",
"kibanaReact",
]
"requiredBundles": ["kibanaUtils", "alerting", "kibanaReact"]
}
}

View file

@ -5,19 +5,37 @@
* 2.0.
*/
import React, { Fragment, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui';
import { BASE_ALERTING_API_PATH } from '@kbn/alerting-plugin/common';
import { CommonAlert } from '../../common/types/alerts';
import { Legacy } from '../legacy_shims';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { isValidRuleFormPlugins } from '@kbn/response-ops-rule-form/lib';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { Fragment, useCallback, useMemo } from 'react';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { hideBottomBar, showBottomBar } from '../lib/setup_mode';
import { Legacy } from '../legacy_shims';
import { CommonAlert } from '../../common/types/alerts';
interface Props {
alert: CommonAlert;
compressed?: boolean;
}
type KibanaDeps = {
dataViews: DataViewsPublicPluginStart;
charts?: ChartsPluginStart;
data: DataPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
} & CoreStart;
export const AlertConfiguration: React.FC<Props> = (props: Props) => {
const { alert, compressed } = props;
const [showFlyout, setShowFlyout] = React.useState(false);
@ -25,6 +43,8 @@ export const AlertConfiguration: React.FC<Props> = (props: Props) => {
const [isMuted, setIsMuted] = React.useState(alert.muteAll);
const [isSaving, setIsSaving] = React.useState(false);
const { services } = useKibana<KibanaDeps>();
async function disableAlert() {
setIsSaving(true);
try {
@ -82,19 +102,25 @@ export const AlertConfiguration: React.FC<Props> = (props: Props) => {
setIsSaving(false);
}
const onClose = useCallback(() => {
setShowFlyout(false);
showBottomBar();
}, []);
const {
triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry },
} = Legacy.shims;
const flyoutUi = useMemo(
() =>
showFlyout &&
Legacy.shims.triggersActionsUi.getEditRuleFlyout({
initialRule: {
...alert,
ruleTypeId: alert.alertTypeId,
},
onClose: () => {
setShowFlyout(false);
showBottomBar();
},
}),
isValidRuleFormPlugins(services) && (
<RuleFormFlyout
plugins={{ ruleTypeRegistry, actionTypeRegistry, ...services }}
id={alert.id}
onSubmit={onClose}
onCancel={onClose}
/>
),
// eslint-disable-next-line react-hooks/exhaustive-deps
[showFlyout]
);

View file

@ -111,6 +111,7 @@ export class MonitoringPlugin
usageCollection: plugins.usageCollection,
appMountParameters: params,
dataViews: pluginsStart.dataViews,
fieldsMetadata: pluginsStart.fieldsMetadata,
};
Legacy.init({
@ -126,6 +127,7 @@ export class MonitoringPlugin
appMountParameters: deps.appMountParameters,
dataViews: deps.dataViews,
share: deps.share,
fieldsMetadata: deps.fieldsMetadata,
});
const config = Object.fromEntries(externalConfig);

View file

@ -17,6 +17,7 @@ import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { FleetStart } from '@kbn/fleet-plugin/public';
import { SharePluginStart } from '@kbn/share-plugin/public';
import { ReactNode } from 'react';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
export interface MonitoringStartPluginDependencies {
navigation: NavigationStart;
@ -27,6 +28,7 @@ export interface MonitoringStartPluginDependencies {
dashboard?: DashboardStart;
fleet?: FleetStart;
share: SharePluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
interface LegacyStartDependencies {

View file

@ -47,6 +47,9 @@
"@kbn/core-elasticsearch-server",
"@kbn/share-plugin",
"@kbn/analytics",
"@kbn/response-ops-rule-form",
"@kbn/charts-plugin",
"@kbn/fields-metadata-plugin",
],
"exclude": [
"target/**/*",

View file

@ -30,7 +30,8 @@
"charts",
"savedObjectsFinder",
"savedObjectsManagement",
"contentManagement"
"contentManagement",
"fieldsMetadata"
],
"optionalPlugins": [
"dataViewEditor",

View file

@ -7,6 +7,7 @@
import type { FC } from 'react';
import React, { createContext, useContext, useMemo } from 'react';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { memoize } from 'lodash';
import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
@ -32,44 +33,43 @@ export const TransformAlertFlyout: FC<TransformAlertFlyoutProps> = ({
onCloseFlyout,
onSave,
}) => {
const { triggersActionsUi } = useAppDependencies();
const { triggersActionsUi, ...plugins } = useAppDependencies();
const AlertFlyout = useMemo(() => {
if (!triggersActionsUi) return;
const { ruleTypeRegistry, actionTypeRegistry } = triggersActionsUi;
const commonProps = {
onClose: () => {
plugins: { ...plugins, ruleTypeRegistry, actionTypeRegistry },
onCancel: () => {
onCloseFlyout();
},
onSave: async () => {
onSubmit: async () => {
if (onSave) {
onSave();
}
onCloseFlyout();
},
};
if (initialAlert) {
return triggersActionsUi.getEditRuleFlyout({
...commonProps,
initialRule: {
...initialAlert,
ruleTypeId: initialAlert.alertTypeId,
},
});
return <RuleFormFlyout {...commonProps} id={initialAlert.id} />;
}
return triggersActionsUi.getAddRuleFlyout({
...commonProps,
consumer: 'stackAlerts',
canChangeTrigger: false,
ruleTypeId: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH,
metadata: {},
initialValues: {
params: ruleParams!,
},
});
return (
<RuleFormFlyout
{...commonProps}
consumer={'stackAlerts'}
ruleTypeId={TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH}
initialMetadata={{}}
initialValues={{
params: ruleParams!,
}}
/>
);
// deps on id to avoid re-rendering on auto-refresh
}, [triggersActionsUi, initialAlert, ruleParams, onCloseFlyout, onSave]);
}, [triggersActionsUi, plugins, initialAlert, ruleParams, onCloseFlyout, onSave]);
return <>{AlertFlyout}</>;
};

View file

@ -28,6 +28,7 @@ import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-manag
import { settingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks';
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
@ -103,6 +104,7 @@ const appDependencies: AppDependencies = {
settings: settingsServiceMock.createStartContract(),
savedSearch: savedSearchPluginMock.createStartContract(),
contentManagement: contentManagementMock.createStartContract(),
fieldsMetadata: fieldsMetadataPluginPublicMock.createStartContract(),
};
export const useAppDependencies = () => {

View file

@ -38,6 +38,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
export interface AppDependencies {
analytics: AnalyticsServiceStart;
@ -68,6 +69,7 @@ export interface AppDependencies {
savedObjectsManagement: SavedObjectsManagementPluginStart;
settings: SettingsStart;
contentManagement: ContentManagementPublicStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
export const useAppDependencies = () => {

View file

@ -58,6 +58,7 @@ export async function mountManagementSection(
savedObjectsManagement,
savedSearch,
contentManagement,
fieldsMetadata,
} = plugins;
const { docTitle } = chrome;
@ -95,6 +96,7 @@ export async function mountManagementSection(
savedObjectsManagement,
savedSearch,
contentManagement,
fieldsMetadata,
};
const enabledFeatures: TransformEnabledFeatures = {

View file

@ -24,6 +24,7 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { PluginInitializerContext } from '@kbn/core/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { ConfigSchema } from '../server/config';
import { registerFeature } from './register_feature';
import { getTransformHealthRuleType } from './alerting';
@ -44,6 +45,7 @@ export interface PluginsDependencies {
fieldFormats: FieldFormatsStart;
savedObjectsManagement: SavedObjectsManagementPluginStart;
contentManagement: ContentManagementPublicStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
export class TransformUiPlugin {

View file

@ -1,7 +1,7 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": [
"common/**/*",
@ -9,7 +9,7 @@
"server/**/*",
"../../../../../typings/**/*",
// have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636
"public/**/*.json",
"public/**/*.json"
],
"kbn_references": [
"@kbn/core",
@ -80,9 +80,9 @@
"@kbn/ml-field-stats-flyout",
"@kbn/ml-validators",
"@kbn/core-user-profile-browser-mocks",
"@kbn/response-ops-rule-params"
"@kbn/response-ops-rule-form",
"@kbn/response-ops-rule-params",
"@kbn/fields-metadata-plugin"
],
"exclude": [
"target/**/*",
]
"exclude": ["target/**/*"]
}

View file

@ -1,9 +1,7 @@
{
"type": "plugin",
"id": "@kbn/ml-plugin",
"owner": [
"@elastic/ml-ui"
],
"owner": ["@elastic/ml-ui"],
"group": "platform",
"visibility": "shared",
"description": "This plugin provides access to the machine learning features provided by Elastic.",
@ -11,10 +9,7 @@
"id": "ml",
"browser": true,
"server": true,
"configPath": [
"xpack",
"ml"
],
"configPath": ["xpack", "ml"],
"requiredPlugins": [
"aiops",
"charts",
@ -37,7 +32,8 @@
"savedObjectsManagement",
"savedSearch",
"contentManagement",
"presentationUtil"
"presentationUtil",
"fieldsMetadata"
],
"optionalPlugins": [
"alerting",
@ -50,7 +46,7 @@
"spaces",
"observabilityAIAssistant",
"usageCollection",
"cases"
"cases",
],
"requiredBundles": [
"cases",
@ -66,8 +62,6 @@
"usageCollection",
"alerting"
],
"extraPublicDirs": [
"common"
]
"extraPublicDirs": ["common"]
}
}
}

View file

@ -8,6 +8,7 @@
import type { FC } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import type { JobId } from '../../common/types/anomaly_detection_jobs';
@ -38,47 +39,46 @@ export const MlAnomalyAlertFlyout: FC<MlAnomalyAlertFlyoutProps> = ({
onSave,
}) => {
const {
services: { triggersActionsUi },
services: { triggersActionsUi, ...services },
} = useMlKibana();
const AlertFlyout = useMemo(() => {
if (!triggersActionsUi) return;
const { ruleTypeRegistry, actionTypeRegistry } = triggersActionsUi;
const commonProps = {
onClose: () => {
plugins: { ...services, ruleTypeRegistry, actionTypeRegistry },
onCancel: () => {
onCloseFlyout();
},
onSave: async () => {
onSubmit: async () => {
if (onSave) {
onSave();
}
onCloseFlyout();
},
};
if (initialAlert) {
return triggersActionsUi.getEditRuleFlyout({
...commonProps,
initialRule: {
...initialAlert,
ruleTypeId: initialAlert.ruleTypeId ?? initialAlert.alertTypeId,
},
});
return <RuleFormFlyout {...commonProps} id={initialAlert.id} />;
}
return triggersActionsUi.getAddRuleFlyout({
...commonProps,
consumer: PLUGIN_ID,
canChangeTrigger: false,
ruleTypeId: ML_ALERT_TYPES.ANOMALY_DETECTION,
metadata: {},
initialValues: {
params: {
jobSelection: {
jobIds,
return (
<RuleFormFlyout
{...commonProps}
consumer={PLUGIN_ID}
ruleTypeId={ML_ALERT_TYPES.ANOMALY_DETECTION}
initialMetadata={{}}
initialValues={{
params: {
jobSelection: {
jobIds,
},
},
},
},
});
}}
/>
);
// deps on id to avoid re-rendering on auto-refresh
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [triggersActionsUi, initialAlert?.id, jobIds]);

View file

@ -102,6 +102,7 @@ const App: FC<AppProps> = ({
usageCollection: deps.usageCollection,
mlServices: getMlGlobalServices(coreStart, deps.data.dataViews, deps.usageCollection),
spaces: deps.spaces,
fieldsMetadata: deps.fieldsMetadata,
};
}, [deps, coreStart]);

View file

@ -33,6 +33,7 @@ import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { MlServicesContext } from '../../app';
interface StartPlugins {
@ -61,6 +62,7 @@ interface StartPlugins {
uiActions: UiActionsStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
usageCollection?: UsageCollectionSetup;
fieldsMetadata: FieldsMetadataPublicStart;
}
export type StartServices = CoreStart &
StartPlugins & {

View file

@ -52,6 +52,7 @@ import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import { ENABLE_ESQL } from '@kbn/esql-utils';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { MlSharedServices } from './application/services/get_shared_ml_services';
import { getMlSharedServices } from './application/services/get_shared_ml_services';
import { registerManagementSection } from './application/management';
@ -102,6 +103,7 @@ export interface MlStartDependencies {
uiActions: UiActionsStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
telemetry: ITelemetryClient;
fieldsMetadata: FieldsMetadataPublicStart;
}
export interface MlSetupDependencies {
@ -223,6 +225,7 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> {
usageCollection: pluginsSetup.usageCollection,
spaces: pluginsStart.spaces,
telemetry: telemetryClient,
fieldsMetadata: pluginsStart.fieldsMetadata,
},
params,
this.isServerless,

View file

@ -1,7 +1,7 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": [
"common/**/*",
@ -15,9 +15,7 @@
"public/**/*.json",
"server/**/*.json"
],
"exclude": [
"target/**/*",
],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/core",
{
@ -140,7 +138,9 @@
"@kbn/core-security-server",
"@kbn/response-ops-rule-params",
"@kbn/charts-theme",
"@kbn/response-ops-rule-form",
"@kbn/unsaved-changes-prompt",
"@kbn/core-analytics-browser",
"@kbn/fields-metadata-plugin",
"@kbn/core-analytics-browser"
]
}

View file

@ -60,10 +60,19 @@ export const EsQueryRuleTypeExpression: React.FunctionComponent<
}
);
const errorParam = ALL_EXPRESSION_ERROR_KEYS.find((errorKey) => {
// @ts-expect-error upgrade typescript v5.1.6
return errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined;
});
const errorParam =
ALL_EXPRESSION_ERROR_KEYS.find((errorKey) => {
return (
// @ts-expect-error upgrade typescript v5.1.6
errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined
);
}) ||
// For search source alerts, if the only error is timeField, show this error even if the param is undefined
// timeField is inherently a part of the selectable data view, so if the user selects a data view with no
// timeField, this data view is incompatible with the rule.
(isSearchSource && !!errors.timeField?.length && !errors.searchConfiguration?.length
? 'timeField'
: undefined);
const expressionError = !!errorParam && (
<>

View file

@ -31,7 +31,8 @@
"expressions",
"lens",
"controls",
"embeddable"
"embeddable",
"fieldsMetadata"
],
"optionalPlugins": [
"cloud",

View file

@ -36,6 +36,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
import { ActionTypeRegistryContract, RuleTypeRegistryContract } from '../types';
import {
@ -50,7 +51,6 @@ import { KibanaContextProvider, useKibana } from '../common/lib/kibana';
import { ConnectorProvider } from './context/connector_context';
import { ALERTS_PAGE_ID, CONNECTORS_PLUGIN_ID } from '../common/constants';
import { queryClient } from './query_client';
import { getIsExperimentalFeatureEnabled } from '../common/get_experimental_features';
const TriggersActionsUIHome = lazy(() => import('./home'));
const RuleDetailsRoute = lazy(
@ -85,6 +85,7 @@ export interface TriggersAndActionsUiServices extends CoreStart {
isServerless: boolean;
fieldFormats: FieldFormatsStart;
lens: LensPublicStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
export const renderApp = (deps: TriggersAndActionsUiServices) => {
@ -120,25 +121,19 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
application: { navigateToApp },
} = useKibana().services;
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
return (
<ConnectorProvider value={{ services: { validateEmailAddresses } }}>
<Routes>
{!isUsingRuleCreateFlyout && (
<Route
exact
path={createRuleRoute}
component={suspendedComponentWithProps(CreateRuleRoute, 'xl')}
/>
)}
{!isUsingRuleCreateFlyout && (
<Route
exact
path={editRuleRoute}
component={suspendedComponentWithProps(EditRuleRoute, 'xl')}
/>
)}
<Route
exact
path={createRuleRoute}
component={suspendedComponentWithProps(CreateRuleRoute, 'xl')}
/>
<Route
exact
path={editRuleRoute}
component={suspendedComponentWithProps(EditRuleRoute, 'xl')}
/>
<Route
path={`/:section(${sectionsRegex})`}
component={suspendedComponentWithProps(TriggersActionsUIHome, 'xl')}

View file

@ -23,31 +23,24 @@ import { useLoadRuleTypesQuery } from '../../../hooks/use_load_rule_types_query'
import { RuleDefinitionProps } from '../../../../types';
import { RuleType } from '../../../..';
import { useKibana } from '../../../../common/lib/kibana';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
import {
hasAllPrivilege,
hasExecuteActionsCapability,
hasShowActionsCapability,
} from '../../../lib/capabilities';
import { RuleActions } from './rule_actions';
import { RuleEdit } from '../../rule_form';
export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
rule,
actionTypeRegistry,
ruleTypeRegistry,
onEditRule,
hideEditButton = false,
filteredRuleTypes = [],
useNewRuleForm = false,
}) => {
const {
application: { capabilities, navigateToApp },
} = useKibana().services;
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
const [editFlyoutVisible, setEditFlyoutVisible] = useState<boolean>(false);
const [ruleType, setRuleType] = useState<RuleType>();
const hasConditions = !!(rule?.params.criteria as any[])?.length;
@ -110,17 +103,13 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
}, [rule, ruleTypeRegistry]);
const onEditRuleClick = () => {
if (!isUsingRuleCreateFlyout && useNewRuleForm) {
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`,
state: {
returnApp: 'management',
returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
},
});
} else {
setEditFlyoutVisible(true);
}
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`,
state: {
returnApp: 'management',
returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
},
});
};
const ruleDefinitionList = [
@ -239,18 +228,6 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
<EuiSpacer size="m" />
<EuiDescriptionList compressed={true} type="column" listItems={ruleDefinitionList} />
</EuiPanel>
{editFlyoutVisible && (
<RuleEdit
onSave={() => {
setEditFlyoutVisible(false);
return onEditRule();
}}
initialRule={rule}
onClose={() => setEditFlyoutVisible(false)}
ruleTypeRegistry={ruleTypeRegistry}
actionTypeRegistry={actionTypeRegistry}
/>
)}
</EuiFlexItem>
);
};

View file

@ -28,6 +28,7 @@ export const RuleFormRoute = () => {
ruleTypeRegistry,
actionTypeRegistry,
chrome,
isServerless,
setBreadcrumbs,
...startServices
} = useKibana().services;
@ -75,6 +76,9 @@ export const RuleFormRoute = () => {
actionTypeRegistry,
...startServices,
}}
isServerless={isServerless}
id={id}
ruleTypeId={ruleTypeId}
onCancel={() => {
if (returnApp && returnPath) {
application.navigateToApp(returnApp, { path: returnPath });

View file

@ -216,7 +216,8 @@ export const RulesList = ({
const cloneRuleId = useRef<null | string>(null);
const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter');
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
// TODO: Remove this when removing the v1 flyout code
const isUsingRuleCreateFlyout = false; // getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
const [percentileOptions, setPercentileOptions] =
useState<EuiSelectableOption[]>(initialPercentileOptions);

View file

@ -1,28 +0,0 @@
/*
* 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 from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ConnectorProvider } from '../application/context/connector_context';
import { RuleAdd } from '../application/sections/rule_form';
import type { ConnectorServices, RuleAddProps, RuleTypeParams, RuleTypeMetaData } from '../types';
import { queryClient } from '../application/query_client';
export const getAddRuleFlyoutLazy = <
Params extends RuleTypeParams = RuleTypeParams,
MetaData extends RuleTypeMetaData = RuleTypeMetaData
>(
props: RuleAddProps<Params, MetaData> & { connectorServices: ConnectorServices }
) => {
return (
<ConnectorProvider value={{ services: props.connectorServices }}>
<QueryClientProvider client={queryClient}>
<RuleAdd {...props} />
</QueryClientProvider>
</ConnectorProvider>
);
};

View file

@ -1,28 +0,0 @@
/*
* 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 from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ConnectorProvider } from '../application/context/connector_context';
import { RuleEdit } from '../application/sections/rule_form';
import type { ConnectorServices, RuleEditProps, RuleTypeParams, RuleTypeMetaData } from '../types';
import { queryClient } from '../application/query_client';
export const getEditRuleFlyoutLazy = <
Params extends RuleTypeParams = RuleTypeParams,
MetaData extends RuleTypeMetaData = RuleTypeMetaData
>(
props: RuleEditProps<Params, MetaData> & { connectorServices: ConnectorServices }
) => {
return (
<ConnectorProvider value={{ services: props.connectorServices }}>
<QueryClientProvider client={queryClient}>
<RuleEdit {...props} />
</QueryClientProvider>
</ConnectorProvider>
);
};

View file

@ -20,6 +20,7 @@ import { TriggersAndActionsUiServices } from '../../../application/rules_app';
import { RuleTypeRegistryContract, ActionTypeRegistryContract } from '../../../types';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
import { fieldsMetadataPluginPublicMock } from '@kbn/fields-metadata-plugin/public/mocks';
export const createStartServicesMock = (): TriggersAndActionsUiServices => {
const core = coreMock.createStart();
@ -68,6 +69,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => {
isServerless: false,
fieldFormats: fieldFormatsServiceMock.createStartContract(),
lens: lensPluginMock.createStartContract(),
fieldsMetadata: fieldsMetadataPluginPublicMock.createStartContract(),
} as TriggersAndActionsUiServices;
};

View file

@ -13,8 +13,6 @@ import type { TriggersAndActionsUIPublicPluginStart } from './plugin';
import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout';
import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
import { getAddRuleFlyoutLazy } from './common/get_add_rule_flyout';
import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout';
import {
ActionTypeModel,
RuleTypeModel,
@ -75,22 +73,6 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
connectorServices,
});
},
getAddRuleFlyout: (props) => {
return getAddRuleFlyoutLazy({
...props,
actionTypeRegistry,
ruleTypeRegistry,
connectorServices,
});
},
getEditRuleFlyout: (props) => {
return getEditRuleFlyoutLazy({
...props,
actionTypeRegistry,
ruleTypeRegistry,
connectorServices,
});
},
getAlertsSearchBar: (props: AlertsSearchBarProps) => {
return getAlertsSearchBarLazy(props);
},

View file

@ -5,91 +5,87 @@
* 2.0.
*/
import { CoreSetup, CoreStart, Plugin as CorePlugin } from '@kbn/core/public';
import { Plugin as CorePlugin, CoreSetup, CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { ReactElement } from 'react';
import { PluginInitializerContext } from '@kbn/core/public';
import { FeaturesPluginStart } from '@kbn/features-plugin/public';
import { KibanaFeature } from '@kbn/features-plugin/common';
import { ManagementAppMountParams, ManagementSetup } from '@kbn/management-plugin/public';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public';
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
import { RuleAction } from '@kbn/alerting-plugin/common';
import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public';
import { TypeRegistry } from '@kbn/alerts-ui-shared/src/common/type_registry';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import { PluginInitializerContext } from '@kbn/core/public';
import { DashboardStart } from '@kbn/dashboard-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { KibanaFeature } from '@kbn/features-plugin/common';
import { FeaturesPluginStart } from '@kbn/features-plugin/public';
import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { i18n } from '@kbn/i18n';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { ManagementAppMountParams, ManagementSetup } from '@kbn/management-plugin/public';
import { triggersActionsRoute } from '@kbn/rule-data-utils';
import { ServerlessPluginStart } from '@kbn/serverless/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { triggersActionsRoute } from '@kbn/rule-data-utils';
import { DashboardStart } from '@kbn/dashboard-plugin/public';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { ServerlessPluginStart } from '@kbn/serverless/public';
import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { RuleAction } from '@kbn/alerting-plugin/common';
import { TypeRegistry } from '@kbn/alerts-ui-shared/src/common/type_registry';
import { CloudSetup } from '@kbn/cloud-plugin/public';
import type { RuleUiAction } from './types';
import { ReactElement } from 'react';
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { AlertsSearchBarProps } from './application/sections/alerts_search_bar';
import type { RuleUiAction } from './types';
import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout';
import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
import { getAddRuleFlyoutLazy } from './common/get_add_rule_flyout';
import { getEditRuleFlyoutLazy } from './common/get_edit_rule_flyout';
import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown';
import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge';
import { getRulesListLazy } from './common/get_rules_list';
import { getActionFormLazy } from './common/get_action_form';
import { getRuleStatusPanelLazy } from './common/get_rule_status_panel';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import {
ExperimentalFeatures,
parseExperimentalConfigValue,
} from '../common/experimental_features';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import { getActionFormLazy } from './common/get_action_form';
import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout';
import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
import { getRuleEventLogListLazy } from './common/get_rule_event_log_list';
import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleStatusPanelLazy } from './common/get_rule_status_panel';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRulesListLazy } from './common/get_rules_list';
import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge';
import { TriggersActionsUiConfigType } from '../common/types';
import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form';
import { AlertSummaryWidgetProps } from './application/sections/alert_summary_widget';
import { AlertSummaryWidgetDependencies } from './application/sections/alert_summary_widget/types';
import { RuleStatusPanelProps } from './application/sections/rule_details/components/rule_status_panel';
import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal';
import { ALERTS_PAGE_ID, CONNECTORS_PLUGIN_ID, PLUGIN_ID } from './common/constants';
import { getAlertsSearchBarLazy } from './common/get_alerts_search_bar';
import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list';
import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary';
import { getRuleDefinitionLazy } from './common/get_rule_definition';
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
import type {
ActionTypeModel,
RuleAddProps,
RuleEditProps,
RuleTypeModel,
RuleTypeParams,
RuleTypeMetaData,
RuleStatusDropdownProps,
RuleTagFilterProps,
RuleStatusFilterProps,
RuleTagBadgeProps,
RuleTagBadgeOptions,
RuleEventLogListProps,
RuleEventLogListOptions,
GlobalRuleEventLogListProps,
RulesListProps,
RulesListNotifyBadgePropsWithApi,
ConnectorServices,
CreateConnectorFlyoutProps,
EditConnectorFlyoutProps,
ConnectorServices,
GlobalRuleEventLogListProps,
RuleDefinitionProps,
RuleEventLogListOptions,
RuleEventLogListProps,
RuleStatusDropdownProps,
RuleStatusFilterProps,
RuleTagBadgeOptions,
RuleTagBadgeProps,
RuleTagFilterProps,
RuleTypeModel,
RulesListNotifyBadgePropsWithApi,
RulesListProps,
} from './types';
import { TriggersActionsUiConfigType } from '../common/types';
import { PLUGIN_ID, CONNECTORS_PLUGIN_ID, ALERTS_PAGE_ID } from './common/constants';
import { getAlertsSearchBarLazy } from './common/get_alerts_search_bar';
import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form';
import { getRuleDefinitionLazy } from './common/get_rule_definition';
import { RuleStatusPanelProps } from './application/sections/rule_details/components/rule_status_panel';
import { AlertSummaryWidgetProps } from './application/sections/alert_summary_widget';
import { getAlertSummaryWidgetLazy } from './common/get_rule_alerts_summary';
import { RuleSnoozeModalProps } from './application/sections/rules_list/components/rule_snooze_modal';
import { getRuleSnoozeModalLazy } from './common/get_rule_snooze_modal';
import { getRulesSettingsLinkLazy } from './common/get_rules_settings_link';
import { getGlobalRuleEventLogListLazy } from './common/get_global_rule_event_log_list';
import { AlertSummaryWidgetDependencies } from './application/sections/alert_summary_widget/types';
export interface TriggersAndActionsUIPublicPluginSetup {
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
@ -110,18 +106,6 @@ export interface TriggersAndActionsUIPublicPluginStart {
getEditConnectorFlyout: (
props: Omit<EditConnectorFlyoutProps, 'actionTypeRegistry'>
) => ReactElement<EditConnectorFlyoutProps>;
getAddRuleFlyout: <
Params extends RuleTypeParams = RuleTypeParams,
MetaData extends RuleTypeMetaData = RuleTypeMetaData
>(
props: Omit<RuleAddProps<Params, MetaData>, 'actionTypeRegistry' | 'ruleTypeRegistry'>
) => ReactElement<RuleAddProps<Params, MetaData>>;
getEditRuleFlyout: <
Params extends RuleTypeParams = RuleTypeParams,
MetaData extends RuleTypeMetaData = RuleTypeMetaData
>(
props: Omit<RuleEditProps<Params, MetaData>, 'actionTypeRegistry' | 'ruleTypeRegistry'>
) => ReactElement<RuleEditProps<Params, MetaData>>;
getAlertsSearchBar: (props: AlertsSearchBarProps) => ReactElement<AlertsSearchBarProps>;
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement<RuleStatusDropdownProps>;
getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement<RuleTagFilterProps>;
@ -169,6 +153,7 @@ interface PluginsStart {
serverless?: ServerlessPluginStart;
fieldFormats: FieldFormatsRegistry;
lens: LensPublicStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
export class Plugin
@ -306,6 +291,7 @@ export class Plugin
isServerless: !!pluginsStart.serverless,
fieldFormats: pluginsStart.fieldFormats,
lens: pluginsStart.lens,
fieldsMetadata: pluginsStart.fieldsMetadata,
});
},
});
@ -402,6 +388,7 @@ export class Plugin
isServerless: !!pluginsStart.serverless,
fieldFormats: pluginsStart.fieldFormats,
lens: pluginsStart.lens,
fieldsMetadata: pluginsStart.fieldsMetadata,
});
},
});
@ -456,22 +443,6 @@ export class Plugin
connectorServices: this.connectorServices!,
});
},
getAddRuleFlyout: (props) => {
return getAddRuleFlyoutLazy({
...props,
actionTypeRegistry: this.actionTypeRegistry,
ruleTypeRegistry: this.ruleTypeRegistry,
connectorServices: this.connectorServices!,
});
},
getEditRuleFlyout: (props) => {
return getEditRuleFlyoutLazy({
...props,
actionTypeRegistry: this.actionTypeRegistry,
ruleTypeRegistry: this.ruleTypeRegistry,
connectorServices: this.connectorServices!,
});
},
getAlertsSearchBar: (props: AlertsSearchBarProps) => {
return getAlertsSearchBarLazy(props);
},

View file

@ -77,7 +77,8 @@
"@kbn/charts-theme",
"@kbn/rrule",
"@kbn/core-notifications-browser-mocks",
"@kbn/response-ops-rule-params"
"@kbn/response-ops-rule-params",
"@kbn/fields-metadata-plugin"
],
"exclude": ["target/**/*"]
}

View file

@ -60,6 +60,7 @@ describe('Alerts', () => {
describe('when rendered from Service view in APM app', () => {
const ruleName = 'Error count threshold';
const confirmModalButtonSelector = '.euiModal button[data-test-subj=confirmModalConfirmButton]';
const saveButtonSelector = 'button[data-test-subj=ruleFlyoutFooterSaveButton]';
it('alerts table is rendered correctly', () => {
cy.loginAsEditorUser();
@ -73,15 +74,19 @@ describe('Alerts', () => {
// has loaded.
cy.contains('for the last');
cy.contains('Actions');
cy.contains('Save').should('not.be.disabled');
cy.contains('Next').should('not.be.disabled');
// Update "Is above" to "0"
cy.contains('is above').click();
cy.getByTestSubj('apmIsAboveFieldFieldNumber').clear();
cy.contains('is above 0 errors');
// Navigate to Rule Details step
cy.getByTestSubj('ruleFormStep-details').click();
cy.get(saveButtonSelector).should('not.be.disabled');
// Save, with no actions
cy.contains('Save').click();
cy.get(saveButtonSelector).click();
cy.get(confirmModalButtonSelector).click();
cy.contains(`Created rule "${ruleName}`);

View file

@ -60,6 +60,7 @@ describe('Rules', () => {
const ruleName = 'Error count threshold';
const comboBoxInputSelector = '[data-popover-open] [data-test-subj=comboBoxSearchInput]';
const confirmModalButtonSelector = '.euiModal button[data-test-subj=confirmModalConfirmButton]';
const saveButtonSelector = 'button[data-test-subj=ruleFlyoutFooterSaveButton]';
describe('when created from APM', () => {
describe('when created from Service Inventory', () => {
@ -75,10 +76,14 @@ describe('Rules', () => {
// has loaded.
cy.contains('for the last');
cy.contains('Actions');
cy.contains('Save').should('not.be.disabled');
cy.contains('Next').should('not.be.disabled');
// Navigate to Rule Details step
cy.getByTestSubj('ruleFormStep-details').click();
cy.get(saveButtonSelector).should('not.be.disabled');
// Save, with no actions
cy.contains('Save').click();
cy.get(saveButtonSelector).click();
cy.get(confirmModalButtonSelector).click();
cy.contains(`Created rule "${ruleName}`);

View file

@ -39,7 +39,8 @@
"uiActions",
"logsDataAccess",
"savedSearch",
"entityManager"
"entityManager",
"fieldsMetadata"
],
"optionalPlugins": [
"actions",

View file

@ -6,15 +6,16 @@
*/
import React, { useCallback, useMemo } from 'react';
import type { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ApmRuleType } from '@kbn/rule-data-utils';
import type { RuleTypeParams } from '@kbn/alerting-plugin/common';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { isValidRuleFormPlugins } from '@kbn/response-ops-rule-form/lib';
import { APM_SERVER_FEATURE_ID } from '../../../../../common/rules/apm_rule_types';
import { getInitialAlertValues } from '../../utils/get_initial_alert_values';
import type { ApmPluginStartDeps } from '../../../../plugin';
import { useServiceName } from '../../../../hooks/use_service_name';
import { useApmParams } from '../../../../hooks/use_apm_params';
import type { AlertMetadata } from '../../utils/helper';
import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
import { useTimeRange } from '../../../../hooks/use_time_range';
@ -40,7 +41,12 @@ export function AlertingFlyout(props: Props) {
const transactionName = 'transactionName' in query ? query.transactionName : undefined;
const errorGroupingKey = 'groupId' in path ? path.groupId : undefined;
const { services } = useKibana<ApmPluginStartDeps>();
const {
services: {
triggersActionsUi: { ruleTypeRegistry, actionTypeRegistry },
...services
},
} = useKibana<CoreStart & ApmPluginStartDeps>();
const initialValues = getInitialAlertValues(ruleType, serviceName);
const onCloseAddFlyout = useCallback(
@ -51,29 +57,33 @@ export function AlertingFlyout(props: Props) {
const addAlertFlyout = useMemo(
() =>
ruleType &&
services.triggersActionsUi.getAddRuleFlyout<RuleTypeParams, AlertMetadata>({
consumer: APM_SERVER_FEATURE_ID,
onClose: onCloseAddFlyout,
ruleTypeId: ruleType,
canChangeTrigger: false,
initialValues,
metadata: {
environment,
serviceName,
...(ruleType === ApmRuleType.ErrorCount ? {} : { transactionType }),
transactionName,
errorGroupingKey,
start,
end,
},
useRuleProducer: true,
}),
isValidRuleFormPlugins(services) && (
<RuleFormFlyout
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
consumer={APM_SERVER_FEATURE_ID}
onCancel={onCloseAddFlyout}
onSubmit={onCloseAddFlyout}
ruleTypeId={ruleType}
initialValues={initialValues}
initialMetadata={{
environment,
serviceName,
...(ruleType === ApmRuleType.ErrorCount ? {} : { transactionType }),
transactionName,
errorGroupingKey,
start,
end,
}}
shouldUseRuleProducer
/>
),
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[
ruleType,
environment,
onCloseAddFlyout,
services.triggersActionsUi,
ruleTypeRegistry,
actionTypeRegistry,
serviceName,
transactionName,
errorGroupingKey,

View file

@ -72,6 +72,7 @@ import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
import type { SavedSearchPublicPluginStart } from '@kbn/saved-search-plugin/public';
import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
import type { ConfigSchema } from '.';
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import { registerEmbeddables } from './embeddable/register_embeddables';
@ -148,6 +149,7 @@ export interface ApmPluginStartDeps {
logsShared: LogsSharedClientStartExports;
logsDataAccess: LogsDataAccessPluginStart;
savedSearch: SavedSearchPublicPluginStart;
fieldsMetadata: FieldsMetadataPublicStart;
}
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {

View file

@ -135,7 +135,9 @@
"@kbn/entityManager-plugin",
"@kbn/core-http-server-utils",
"@kbn/key-value-metadata-table",
"@kbn/event-stacktrace"
"@kbn/event-stacktrace",
"@kbn/response-ops-rule-form",
"@kbn/fields-metadata-plugin"
],
"exclude": ["target/**/*"]
}

View file

@ -38,7 +38,8 @@
"visTypeTimeseries",
"apmDataAccess",
"logsDataAccess",
"entityManager"
"entityManager",
"fieldsMetadata"
],
"optionalPlugins": [
"spaces",

View file

@ -5,16 +5,21 @@
* 2.0.
*/
import { useContext, useMemo } from 'react';
import type { RuleAddProps } from '@kbn/triggers-actions-ui-plugin/public/types';
import React from 'react';
import type { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import { useContext, useMemo } from 'react';
import type { InfraClientStartDeps } from '../../../types';
import { TriggerActionsContext } from '../../../containers/triggers_actions_context';
interface Props {
onClose: RuleAddProps['onClose'];
onClose: () => void;
}
export function AlertFlyout({ onClose }: Props) {
const { services } = useKibana<CoreStart & InfraClientStartDeps>();
const { triggersActionsUI } = useContext(TriggerActionsContext);
const addAlertFlyout = useMemo(() => {
@ -22,22 +27,27 @@ export function AlertFlyout({ onClose }: Props) {
return null;
}
return triggersActionsUI.getAddRuleFlyout({
consumer: 'infrastructure',
onClose,
canChangeTrigger: false,
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
metadata: {
currentOptions: {
/*
const { ruleTypeRegistry, actionTypeRegistry } = triggersActionsUI;
return (
<RuleFormFlyout
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
consumer={'infrastructure'}
onCancel={onClose}
onSubmit={onClose}
ruleTypeId={OBSERVABILITY_THRESHOLD_RULE_TYPE_ID}
initialMetadata={{
currentOptions: {
/*
Setting the groupBy is currently required in custom threshold
rule for it to populate the rule with additional host context.
*/
groupBy: 'host.name',
},
},
});
}, [onClose, triggersActionsUI]);
groupBy: 'host.name',
},
}}
/>
);
}, [onClose, triggersActionsUI, services]);
return addAlertFlyout;
}

View file

@ -7,10 +7,14 @@
import React, { useCallback, useContext, useMemo } from 'react';
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common';
import { TriggerActionsContext } from '../../../containers/triggers_actions_context';
import type { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { InfraClientStartDeps } from '../../../types';
import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../common/alerting/metrics';
import type { InfraWaffleMapOptions } from '../../../common/inventory/types';
import { TriggerActionsContext } from '../../../containers/triggers_actions_context';
import { useAlertPrefillContext } from '../../use_alert_prefill';
interface Props {
@ -22,29 +26,35 @@ interface Props {
}
export const AlertFlyout = ({ options, nodeType, filter, visible, setVisible }: Props) => {
const { services } = useKibana<CoreStart & InfraClientStartDeps>();
const { triggersActionsUI } = useContext(TriggerActionsContext);
const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
const { inventoryPrefill } = useAlertPrefillContext();
const { customMetrics = [], accountId, region } = inventoryPrefill;
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddRuleFlyout({
consumer: 'infrastructure',
onClose: onCloseFlyout,
canChangeTrigger: false,
ruleTypeId: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
metadata: {
accountId,
options,
nodeType,
filter,
customMetrics,
region,
},
useRuleProducer: true,
}),
() => {
if (!triggersActionsUI) return null;
const { ruleTypeRegistry, actionTypeRegistry } = triggersActionsUI;
return (
<RuleFormFlyout
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
consumer={'infrastructure'}
onCancel={onCloseFlyout}
onSubmit={onCloseFlyout}
ruleTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
initialMetadata={{
accountId,
options,
nodeType,
filter,
customMetrics,
region,
}}
shouldUseRuleProducer
/>
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[triggersActionsUI, visible]
);

View file

@ -5,9 +5,13 @@
* 2.0.
*/
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
import React, { useCallback, useContext, useMemo } from 'react';
import { TriggerActionsContext } from '../../../containers/triggers_actions_context';
import type { CoreStart } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { InfraClientStartDeps } from '../../../types';
import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../../../../common/alerting/logs/log_threshold/types';
import { TriggerActionsContext } from '../../../containers/triggers_actions_context';
interface Props {
visible?: boolean;
@ -15,23 +19,26 @@ interface Props {
}
export const AlertFlyout = (props: Props) => {
const { services } = useKibana<CoreStart & InfraClientStartDeps>();
const { visible, setVisible } = props;
const { triggersActionsUI } = useContext(TriggerActionsContext);
const onCloseFlyout = useCallback(() => setVisible(false), [setVisible]);
const AddAlertFlyout = useMemo(
() =>
triggersActionsUI &&
triggersActionsUI.getAddRuleFlyout({
consumer: 'logs',
onClose: onCloseFlyout,
canChangeTrigger: false,
ruleTypeId: LOG_DOCUMENT_COUNT_RULE_TYPE_ID,
metadata: {
const AddAlertFlyout = useMemo(() => {
if (!triggersActionsUI) return null;
const { ruleTypeRegistry, actionTypeRegistry } = triggersActionsUI;
return (
<RuleFormFlyout
plugins={{ ...services, ruleTypeRegistry, actionTypeRegistry }}
consumer="logs"
onCancel={onCloseFlyout}
onSubmit={onCloseFlyout}
ruleTypeId={LOG_DOCUMENT_COUNT_RULE_TYPE_ID}
initialMetadata={{
isInternal: true,
},
}),
[triggersActionsUI, onCloseFlyout]
);
}}
/>
);
}, [triggersActionsUI, services, onCloseFlyout]);
return <>{visible && AddAlertFlyout}</>;
};

Some files were not shown because too many files have changed in this diff Show more