[8.x] [Response Ops] [Rule Form] Remove V1 Rule Form Flyout (#209171) (#213748)

# Backport

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

<!--- 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-08T12:26:54Z","message":"[Response
Ops] [Rule Form] Remove V1 Rule Form Flyout (#209171)\n\n##
Summary\n\nCloses #195211 \n\nRemoves all old rule form flyout code,
which should no longer be\nreferenced at all after
https://github.com/elastic/kibana/pull/206685/\n\n### Checklist\n\n- [x]
[Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fffc18cfc424c52a1d635c789927e4c479cbb5e7","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","backport
missing","backport:version","v9.1.0","v8.19.0"],"title":"[Response Ops]
[Rule Form] Remove V1 Rule Form
Flyout","number":209171,"url":"https://github.com/elastic/kibana/pull/209171","mergeCommit":{"message":"[Response
Ops] [Rule Form] Remove V1 Rule Form Flyout (#209171)\n\n##
Summary\n\nCloses #195211 \n\nRemoves all old rule form flyout code,
which should no longer be\nreferenced at all after
https://github.com/elastic/kibana/pull/206685/\n\n### Checklist\n\n- [x]
[Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fffc18cfc424c52a1d635c789927e4c479cbb5e7"}},"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/209171","number":209171,"mergeCommit":{"message":"[Response
Ops] [Rule Form] Remove V1 Rule Form Flyout (#209171)\n\n##
Summary\n\nCloses #195211 \n\nRemoves all old rule form flyout code,
which should no longer be\nreferenced at all after
https://github.com/elastic/kibana/pull/206685/\n\n### Checklist\n\n- [x]
[Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"fffc18cfc424c52a1d635c789927e4c479cbb5e7"}},{"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-13 13:11:07 +00:00 committed by GitHub
parent 5a5519034d
commit 68195d3afb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 49 additions and 8228 deletions

View file

@ -29,4 +29,5 @@ export {
RuleActionsNotifyWhen,
RuleActionsAlertsFilter,
RuleActionsAlertsFilterTimeframe,
NOTIFY_WHEN_OPTIONS,
} from './src/rule_actions';

View file

@ -18,9 +18,6 @@ import {
import { i18n } from '@kbn/i18n';
import { omit, pick } from 'lodash';
import {
ActionGroupWithCondition,
AlertConditions,
AlertConditionsGroup,
RuleTypeModel,
RuleTypeParamsExpressionProps,
} from '@kbn/triggers-actions-ui-plugin/public';
@ -29,6 +26,7 @@ import {
AlwaysFiringActionGroupIds,
DEFAULT_INSTANCES_TO_GENERATE,
} from '../../common/constants';
import { RuleConditions, RuleConditionsGroup, ActionGroupWithCondition } from '../components';
export function getAlertType(): RuleTypeModel {
return {
@ -101,7 +99,7 @@ export const AlwaysFiringExpression: React.FunctionComponent<
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<AlertConditions
<RuleConditions
headline={'Set different thresholds for randomly generated T-Shirt sizes'}
actionGroups={actionGroupsWithConditions}
onInitializeConditionsFor={(actionGroup) => {
@ -111,7 +109,7 @@ export const AlwaysFiringExpression: React.FunctionComponent<
});
}}
>
<AlertConditionsGroup
<RuleConditionsGroup
onResetConditionsFor={(actionGroup) => {
setRuleParams('thresholds', omit(thresholds, actionGroup.id));
}}
@ -124,8 +122,8 @@ export const AlwaysFiringExpression: React.FunctionComponent<
});
}}
/>
</AlertConditionsGroup>
</AlertConditions>
</RuleConditionsGroup>
</RuleConditions>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './rule_conditions';
export * from './rule_conditions_group';

View file

@ -32,5 +32,6 @@
"@kbn/unified-search-plugin",
"@kbn/response-ops-rule-form",
"@kbn/fields-metadata-plugin",
"@kbn/i18n-react",
]
}

View file

@ -10,8 +10,8 @@
*/
import React, { Fragment, lazy } from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { ReactWrapper, mount } from 'enzyme';
import { nextTick } from '@kbn/test-jest-helpers';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { coreMock } from '@kbn/core/public/mocks';
import { actionTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/action_type_registry.mock';
@ -22,12 +22,10 @@ import {
GenericValidationResult,
RuleTypeModel,
} from '@kbn/triggers-actions-ui-plugin/public/types';
import { RuleForm } from '@kbn/triggers-actions-ui-plugin/public/application/sections/rule_form/rule_form';
import ActionForm from '@kbn/triggers-actions-ui-plugin/public/application/sections/action_connector_form/action_form';
import { Legacy } from '../legacy_shims';
import { I18nProvider } from '@kbn/i18n-react';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
jest.mock('@kbn/triggers-actions-ui-plugin/public/application/lib/action_connector_api', () => ({
loadAllActions: jest.fn(),
@ -66,7 +64,6 @@ const initLegacyShims = () => {
const ALERTS_FEATURE_ID = 'alerts';
const validationMethod = (): ValidationResult => ({ errors: {} });
const actionTypeRegistry = actionTypeRegistryMock.create();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
describe('alert_form', () => {
beforeEach(() => {
@ -102,92 +99,6 @@ describe('alert_form', () => {
actionParamsFields: mockedActionParamsFields,
};
describe('alert_form edit alert', () => {
let wrapper: ReactWrapper<any>;
beforeEach(async () => {
const { useLoadRuleTypesQuery } = jest.requireMock(
'@kbn/triggers-actions-ui-plugin/public/application/hooks/use_load_rule_types_query'
);
useLoadRuleTypesQuery.mockReturnValue({
ruleTypesState: {
data: new Map(),
},
});
ruleTypeRegistry.list.mockReturnValue([ruleType]);
ruleTypeRegistry.get.mockReturnValue(ruleType);
ruleTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.list.mockReturnValue([actionType]);
actionTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.get.mockReturnValue(actionType);
const KibanaReactContext = createKibanaReactContext(Legacy.shims.kibanaServices);
const initialAlert = {
name: 'test',
ruleTypeId: ruleType.id,
params: {},
consumer: ALERTS_FEATURE_ID,
schedule: {
interval: '1m',
},
actions: [],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
} as unknown as Rule;
wrapper = mountWithIntl(
<I18nProvider>
<KibanaReactContext.Provider>
<QueryClientProvider
client={
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
}
>
<RuleForm
rule={initialAlert}
config={{
isUsingSecurity: true,
minimumScheduleInterval: { value: '1m', enforce: false },
}}
dispatch={() => {}}
errors={{ name: [], 'schedule.interval': [] }}
operation="create"
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={() => {}}
/>
</QueryClientProvider>
</KibanaReactContext.Provider>
</I18nProvider>
);
await act(async () => {
await nextTick();
wrapper.update();
});
});
it('renders alert name', async () => {
const alertNameField = wrapper.find('[data-test-subj="ruleNameInput"]');
expect(alertNameField.exists()).toBeTruthy();
expect(alertNameField.first().prop('value')).toBe('test');
});
it('renders registered selected alert type', async () => {
const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedRuleTypeTitle"]');
expect(alertTypeSelectOptions.exists()).toBeTruthy();
});
});
describe('alert_form > action_form', () => {
describe('action_form in alert', () => {
async function setup() {

View file

@ -49957,7 +49957,6 @@
"xpack.triggersActionsUI.sections.actionForm.actionSectionsTitle": "Actions",
"xpack.triggersActionsUI.sections.actionForm.addActionButtonLabel": "Ajouter une action",
"xpack.triggersActionsUI.sections.actionForm.getMoreConnectorsTitle": "Obtenir davantage de connecteurs",
"xpack.triggersActionsUI.sections.actionForm.getMoreRuleTypesTitle": "Obtenir davantage de types de règles",
"xpack.triggersActionsUI.sections.actionForm.incidentManagementSystemLabel": "Système de gestion des incidents",
"xpack.triggersActionsUI.sections.actionForm.loadingConnectorsDescription": "Chargement des connecteurs…",
"xpack.triggersActionsUI.sections.actionForm.loadingConnectorTypesDescription": "Chargement des types de connecteurs…",
@ -50022,14 +50021,6 @@
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.confirmConnectorCloseMessage": "Vous ne pouvez pas récupérer de modifications non enregistrées.",
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.discardButtonLabel": "Abandonner les modifications",
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.title": "Abandonner les modifications non enregistrées apportées au connecteur ?",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText": "Annuler",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText": "Abandonner les modifications",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseMessage": "Vous ne pouvez pas récupérer de modifications non enregistrées.",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseTitle": "Abandonner les modifications non enregistrées apportées à la règle ?",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText": "Annuler",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText": "Enregistrer la règle",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveTitle": "Enregistrer la règle ne contenant aucune action ?",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveWithoutActionsMessage": "Vous pouvez ajouter une action à tout moment.",
"xpack.triggersActionsUI.sections.connector.home.unableToLoadActionsMessage": "Impossible de charger les connecteurs",
"xpack.triggersActionsUI.sections.connectorAddInline.accordion.deleteIconAriaLabel": "Supprimer",
"xpack.triggersActionsUI.sections.connectorAddInline.addConnectorButtonLabel": "Créer un connecteur",
@ -50108,14 +50099,7 @@
"xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "Ce connecteur est préconfiguré et ne peut pas être modifié",
"xpack.triggersActionsUI.sections.refineSearchPrompt.backToTop": "Revenir en haut de la page.",
"xpack.triggersActionsUI.sections.refineSearchPrompt.prompt": "Voici les {visibleDocumentSize} premiers documents correspondant à votre recherche. Veuillez l'affiner pour en voir plus.",
"xpack.triggersActionsUI.sections.ruleAdd.flyoutTitle": "Créer une règle",
"xpack.triggersActionsUI.sections.ruleAdd.indexControls.timeFieldOptionLabel": "Choisir un champ",
"xpack.triggersActionsUI.sections.ruleAdd.operationName": "créer",
"xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText": "Impossible de créer une règle.",
"xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText": "Création de la règle \"{ruleName}\" effectuée",
"xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel": "Annuler",
"xpack.triggersActionsUI.sections.ruleAddFooter.saveButtonLabel": "Enregistrer",
"xpack.triggersActionsUI.sections.ruleAddFooter.showRequestButtonLabel": "Afficher la requête API",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.failure": "Impossible de mettre à jour {property} pour {failure, plural, one {# règle} other {# règles}}.",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.filterByErrors": "Filtrer par règles comportant des erreurs",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.property.apiKey": "Clé d'API",
@ -50193,70 +50177,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.updateAPIKeyButtonLabel": "Mettre à jour la clé d'API",
"xpack.triggersActionsUI.sections.ruleDetails.userManagedApikey": "Cette règle est associée à une clé dAPI.",
"xpack.triggersActionsUI.sections.ruleDetails.viewRuleInAppButtonLabel": "Afficher dans l'application",
"xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel": "Annuler",
"xpack.triggersActionsUI.sections.ruleEdit.changeInPrivilegesLabel": "L'enregistrement de cette règle modifiera ses privilèges et peut-être également son comportement.",
"xpack.triggersActionsUI.sections.ruleEdit.disabledActionsWarningTitle": "Cette règle possède des actions qui sont désactivées",
"xpack.triggersActionsUI.sections.ruleEdit.flyoutTitle": "Modifier la règle",
"xpack.triggersActionsUI.sections.ruleEdit.operationName": "modifier",
"xpack.triggersActionsUI.sections.ruleEdit.saveButtonLabel": "Enregistrer",
"xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText": "Impossible de mettre à jour la règle.",
"xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText": "Mise à jour de \"{ruleName}\" effectuée",
"xpack.triggersActionsUI.sections.ruleEdit.showRequestButtonLabel": "Afficher la requête API",
"xpack.triggersActionsUI.sections.ruleForm.advancedOptionsLabel": "Options avancées",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel": "correspondances consécutives",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp": "Une alerte n'est déclenchée que si le nombre spécifié d'exécutions consécutives remplit les conditions de la règle.",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel": "Après une alerte",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayLabel": "Délai d'alerte",
"xpack.triggersActionsUI.sections.ruleForm.changeRuleTypeAriaLabel": "Supprimer",
"xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText": "Des intervalles inférieurs à {minimum} ne sont pas recommandés pour des raisons de performances.",
"xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText": "L'intervalle doit être au minimum de {minimum}.",
"xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel": "Vérifier toutes les",
"xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip": "Définir la fréquence d'évaluation de la condition. Les vérifications sont mises en file d'attente ; elles seront exécutées au plus près de la valeur définie, en fonction de la capacité.",
"xpack.triggersActionsUI.sections.ruleForm.conditions.addConditionLabel": "Ajouter :",
"xpack.triggersActionsUI.sections.ruleForm.conditions.removeConditionLabel": "Retirer",
"xpack.triggersActionsUI.sections.ruleForm.conditions.title": "Conditions :",
"xpack.triggersActionsUI.sections.ruleForm.documentationLabel": "En savoir plus",
"xpack.triggersActionsUI.sections.ruleForm.error.actionThrottleBelowSchedule": "Les intervalles d'action personnalisés ne peuvent pas être plus courts que l'intervalle de vérification de la règle",
"xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText": "L'intervalle doit être au minimum de {minimum}.",
"xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypes": "Afin de {operation} une règle, vous devez avoir reçu les privilèges associés.",
"xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypesTitle": "Vous n'avez été autorisé à {operation} aucun type de règle",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector": "L'action pour le connecteur {actionTypeId} est requis.",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredConsumerText": "La portée est requise.",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText": "L'intervalle de vérification est requis.",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText": "Le nom est requis.",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText": "Le type de règle est requis.",
"xpack.triggersActionsUI.sections.ruleForm.flappingLabel": "Détection de bagotement d'alerte",
"xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypeParamsDescription": "Chargement des paramètres de types de règles…",
"xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription": "Chargement des types de règles…",
"xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel": "Notifier",
"xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip": "Définissez à quelle fréquence les alertes doivent générer des actions.",
"xpack.triggersActionsUI.sections.ruleForm.ruleNameLabel": "Nom",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label": "Chaque",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description": "Actions exécutées si le statut de l'alerte change.",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display": "Lors de changements de statut",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label": "Lors de changements de statut",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description": "Les actions sont exécutées si les conditions de règle sont remplies.",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display": "Selon les intervalles de vérification",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label": "Selon les intervalles de vérification",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description": "Les actions sont exécutées si les conditions de règle sont remplies.",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display": "Selon des intervalles d'action personnalisés",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label": "Selon des intervalles d'action personnalisés",
"xpack.triggersActionsUI.sections.ruleForm.ruleScheduleLabel": "Calendrier des règles",
"xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel": "Sélectionner le type de règle",
"xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle": "Rechercher",
"xpack.triggersActionsUI.sections.ruleForm.solutionFilterLabel": "Filtrer par cas d'utilisation",
"xpack.triggersActionsUI.sections.ruleForm.tagsFieldLabel": "Balises",
"xpack.triggersActionsUI.sections.ruleForm.tagsFieldOptional": "Facultatif",
"xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage": "Impossible de charger les types de règles",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.apm": "APM et expérience utilisateur",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.ariaLabel": "Sélectionner une portée",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.placeholder": "Sélectionner une portée",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.infrastructure": "Indicateurs",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.logs": "Logs",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel": "Visibilité du rôle",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.slo": "SLO",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.stackAlerts": "Règles de la Suite Elastic",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.uptime": "Synthetics et Uptime",
"xpack.triggersActionsUI.sections.rules_list.rules_tag_badge.tagTitle": "Balise",
"xpack.triggersActionsUI.sections.rulesConnectorList.tabText": "Règles",
"xpack.triggersActionsUI.sections.rulesList.ableToRunRuleSoon": "Votre règle est programmée pour s'exécuter",

View file

@ -49917,7 +49917,6 @@
"xpack.triggersActionsUI.sections.actionForm.actionSectionsTitle": "アクション",
"xpack.triggersActionsUI.sections.actionForm.addActionButtonLabel": "アクションの追加",
"xpack.triggersActionsUI.sections.actionForm.getMoreConnectorsTitle": "その他のコネクターを取得",
"xpack.triggersActionsUI.sections.actionForm.getMoreRuleTypesTitle": "その他のルールタイプを取得",
"xpack.triggersActionsUI.sections.actionForm.incidentManagementSystemLabel": "インシデント管理システム",
"xpack.triggersActionsUI.sections.actionForm.loadingConnectorsDescription": "コネクターを読み込んでいます…",
"xpack.triggersActionsUI.sections.actionForm.loadingConnectorTypesDescription": "コネクタータイプを読み込んでいます...",
@ -49982,14 +49981,6 @@
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.confirmConnectorCloseMessage": "保存されていない変更は回復できません。",
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.discardButtonLabel": "変更を破棄",
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.title": "コネクターの保存されていない変更を破棄しますか?",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText": "キャンセル",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText": "変更を破棄",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseMessage": "保存されていない変更は回復できません。",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseTitle": "ルールの保存されていない変更を破棄しますか?",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText": "キャンセル",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText": "ルールを保存",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveTitle": "アクションがないルールを保存しますか?",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveWithoutActionsMessage": "いつでもアクションを追加できます。",
"xpack.triggersActionsUI.sections.connector.home.unableToLoadActionsMessage": "コネクターを読み込めません",
"xpack.triggersActionsUI.sections.connectorAddInline.accordion.deleteIconAriaLabel": "削除",
"xpack.triggersActionsUI.sections.connectorAddInline.addConnectorButtonLabel": "コネクターを作成する",
@ -50068,14 +50059,7 @@
"xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "このコネクターはあらかじめ構成されているため、編集できません。",
"xpack.triggersActionsUI.sections.refineSearchPrompt.backToTop": "最上部へ戻る。",
"xpack.triggersActionsUI.sections.refineSearchPrompt.prompt": "これらは検索条件に一致した初めの{visibleDocumentSize}件のドキュメントです。他の結果を表示するには検索条件を絞ってください。",
"xpack.triggersActionsUI.sections.ruleAdd.flyoutTitle": "ルールを作成",
"xpack.triggersActionsUI.sections.ruleAdd.indexControls.timeFieldOptionLabel": "フィールドを選択",
"xpack.triggersActionsUI.sections.ruleAdd.operationName": "作成",
"xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText": "ルールを作成できません。",
"xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText": "ルール\"{ruleName}\"を作成しました",
"xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel": "キャンセル",
"xpack.triggersActionsUI.sections.ruleAddFooter.saveButtonLabel": "保存",
"xpack.triggersActionsUI.sections.ruleAddFooter.showRequestButtonLabel": "API リクエストを表示",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.failure": "{failure, plural, other {# ルール}}の{property}を更新できませんでした。",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.filterByErrors": "エラーがあるルールでフィルター",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.property.apiKey": "API キー",
@ -50153,70 +50137,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.updateAPIKeyButtonLabel": "APIキーの更新",
"xpack.triggersActionsUI.sections.ruleDetails.userManagedApikey": "このルールはAPIキーに関連付けられています。",
"xpack.triggersActionsUI.sections.ruleDetails.viewRuleInAppButtonLabel": "アプリで表示",
"xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel": "キャンセル",
"xpack.triggersActionsUI.sections.ruleEdit.changeInPrivilegesLabel": "このルールを保存すると権限が変更され、動作が変わる場合があります。",
"xpack.triggersActionsUI.sections.ruleEdit.disabledActionsWarningTitle": "このアラートには無効なアクションがあります",
"xpack.triggersActionsUI.sections.ruleEdit.flyoutTitle": "ルールを編集",
"xpack.triggersActionsUI.sections.ruleEdit.operationName": "編集",
"xpack.triggersActionsUI.sections.ruleEdit.saveButtonLabel": "保存",
"xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText": "ルールを更新できません",
"xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText": "''{ruleName}''を更新しました",
"xpack.triggersActionsUI.sections.ruleEdit.showRequestButtonLabel": "API リクエストを表示",
"xpack.triggersActionsUI.sections.ruleForm.advancedOptionsLabel": "高度なオプション",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel": "連続一致",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp": "指定した数の連続実行がルール条件を満たしたときにのみ、アラートが発生します。",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel": "次の後にアラート",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayLabel": "アラート遅延",
"xpack.triggersActionsUI.sections.ruleForm.changeRuleTypeAriaLabel": "削除",
"xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText": "パフォーマンスの考慮から、{minimum}未満の間隔は推奨されません。",
"xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText": "間隔は{minimum}以上でなければなりません。",
"xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel": "確認間隔",
"xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip": "条件を評価する頻度を定義します。チェックはキューに登録されています。可能なかぎり定義された値に近づくように実行されます。",
"xpack.triggersActionsUI.sections.ruleForm.conditions.addConditionLabel": "追加:",
"xpack.triggersActionsUI.sections.ruleForm.conditions.removeConditionLabel": "削除",
"xpack.triggersActionsUI.sections.ruleForm.conditions.title": "条件:",
"xpack.triggersActionsUI.sections.ruleForm.documentationLabel": "詳細",
"xpack.triggersActionsUI.sections.ruleForm.error.actionThrottleBelowSchedule": "カスタムアクション間隔をルールのチェック間隔よりも短くすることはできません",
"xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText": "間隔は{minimum}以上でなければなりません。",
"xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypes": "ルールを{operation}するには、適切な権限が付与されている必要があります。",
"xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypesTitle": "ルールタイプを{operation}する権限がありません",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector": "{actionTypeId}コネクターのアクションが必要です。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredConsumerText": "範囲が必要です。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText": "確認間隔が必要です。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText": "名前が必要です。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText": "ルールタイプは必須です。",
"xpack.triggersActionsUI.sections.ruleForm.flappingLabel": "アラートフラップ検出",
"xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypeParamsDescription": "ルールタイプパラメーターを読み込んでいます…",
"xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription": "ルールタイプを読み込んでいます…",
"xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel": "通知",
"xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip": "アラートがアクションを生成する頻度を定義します。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNameLabel": "名前",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label": "毎",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description": "アラートステータスが変更される場合にアクションを実行します。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display": "ステータス変更時",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label": "ステータス変更時",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description": "ルール条件が満たされた場合に、アクションが実行されます。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display": "チェック間隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label": "チェック間隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description": "ルール条件が満たされた場合に、アクションが実行されます。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display": "カスタムアクション間隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label": "カスタムアクション間隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleScheduleLabel": "ルールスケジュール",
"xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel": "ルールタイプを選択",
"xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle": "検索",
"xpack.triggersActionsUI.sections.ruleForm.solutionFilterLabel": "ユースケースでフィルタリング",
"xpack.triggersActionsUI.sections.ruleForm.tagsFieldLabel": "タグ",
"xpack.triggersActionsUI.sections.ruleForm.tagsFieldOptional": "オプション",
"xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.apm": "APMおよびユーザーエクスペリエンス",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.ariaLabel": "範囲を選択",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.placeholder": "範囲を選択",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.infrastructure": "メトリック",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.logs": "ログ",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel": "ロールの表示",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.slo": "SLO",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.stackAlerts": "スタックルール",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.uptime": "Syntheticsとアップタイム",
"xpack.triggersActionsUI.sections.rules_list.rules_tag_badge.tagTitle": "タグ",
"xpack.triggersActionsUI.sections.rulesConnectorList.tabText": "ルール",
"xpack.triggersActionsUI.sections.rulesList.ableToRunRuleSoon": "ルールの実行がスケジュールされています",

View file

@ -50000,7 +50000,6 @@
"xpack.triggersActionsUI.sections.actionForm.actionSectionsTitle": "操作",
"xpack.triggersActionsUI.sections.actionForm.addActionButtonLabel": "添加操作",
"xpack.triggersActionsUI.sections.actionForm.getMoreConnectorsTitle": "获取更多连接器",
"xpack.triggersActionsUI.sections.actionForm.getMoreRuleTypesTitle": "获取更多规则类型",
"xpack.triggersActionsUI.sections.actionForm.incidentManagementSystemLabel": "事件管理系统",
"xpack.triggersActionsUI.sections.actionForm.loadingConnectorsDescription": "正在加载连接器……",
"xpack.triggersActionsUI.sections.actionForm.loadingConnectorTypesDescription": "正在加载连接器类型……",
@ -50065,14 +50064,6 @@
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.confirmConnectorCloseMessage": "您无法恢复未保存更改。",
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.discardButtonLabel": "放弃更改",
"xpack.triggersActionsUI.sections.confirmConnectorEditClose.title": "放弃连接器的未保存更改?",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText": "取消",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText": "放弃更改",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseMessage": "您无法恢复未保存更改。",
"xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseTitle": "丢弃规则的未保存更改?",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText": "取消",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText": "保存规则",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveTitle": "保存规则,而不执行任何操作?",
"xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveWithoutActionsMessage": "您可以随时添加操作。",
"xpack.triggersActionsUI.sections.connector.home.unableToLoadActionsMessage": "无法加载连接器",
"xpack.triggersActionsUI.sections.connectorAddInline.accordion.deleteIconAriaLabel": "删除",
"xpack.triggersActionsUI.sections.connectorAddInline.addConnectorButtonLabel": "创建连接器",
@ -50151,14 +50142,7 @@
"xpack.triggersActionsUI.sections.preconfiguredConnectorForm.tooltipContent": "这是预配置连接器,无法编辑",
"xpack.triggersActionsUI.sections.refineSearchPrompt.backToTop": "返回顶部。",
"xpack.triggersActionsUI.sections.refineSearchPrompt.prompt": "下面是与您的搜索匹配的前 {visibleDocumentSize} 个文档,请优化您的搜索以查看其他文档。",
"xpack.triggersActionsUI.sections.ruleAdd.flyoutTitle": "创建规则",
"xpack.triggersActionsUI.sections.ruleAdd.indexControls.timeFieldOptionLabel": "选择字段",
"xpack.triggersActionsUI.sections.ruleAdd.operationName": "创建",
"xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText": "无法创建规则。",
"xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText": "已创建规则“{ruleName}”",
"xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel": "取消",
"xpack.triggersActionsUI.sections.ruleAddFooter.saveButtonLabel": "保存",
"xpack.triggersActionsUI.sections.ruleAddFooter.showRequestButtonLabel": "显示 API 请求",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.failure": "无法更新 {failure, plural, other {# 个规则}}的 {property}。",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.filterByErrors": "按错误规则筛选",
"xpack.triggersActionsUI.sections.ruleApi.bulkEditResponse.property.apiKey": "API 密钥",
@ -50236,70 +50220,6 @@
"xpack.triggersActionsUI.sections.ruleDetails.updateAPIKeyButtonLabel": "更新 API 密钥",
"xpack.triggersActionsUI.sections.ruleDetails.userManagedApikey": "此规则与 API 密钥关联。",
"xpack.triggersActionsUI.sections.ruleDetails.viewRuleInAppButtonLabel": "在应用中查看",
"xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel": "取消",
"xpack.triggersActionsUI.sections.ruleEdit.changeInPrivilegesLabel": "保存此规则将更改其权限,并可能更改其行为。",
"xpack.triggersActionsUI.sections.ruleEdit.disabledActionsWarningTitle": "此规则具有已禁用的操作",
"xpack.triggersActionsUI.sections.ruleEdit.flyoutTitle": "编辑规则",
"xpack.triggersActionsUI.sections.ruleEdit.operationName": "编辑",
"xpack.triggersActionsUI.sections.ruleEdit.saveButtonLabel": "保存",
"xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText": "无法更新规则。",
"xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText": "已更新“{ruleName}”",
"xpack.triggersActionsUI.sections.ruleEdit.showRequestButtonLabel": "显示 API 请求",
"xpack.triggersActionsUI.sections.ruleForm.advancedOptionsLabel": "高级选项",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel": "连续匹配",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp": "仅当指定数目的连续运行满足规则条件时,才会发生告警。",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel": "此后告警",
"xpack.triggersActionsUI.sections.ruleForm.alertDelayLabel": "告警延迟",
"xpack.triggersActionsUI.sections.ruleForm.changeRuleTypeAriaLabel": "删除",
"xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpSuggestionText": "出于性能考虑,不建议时间间隔小于 {minimum}。",
"xpack.triggersActionsUI.sections.ruleForm.checkEveryHelpText": "时间间隔必须至少为 {minimum}。",
"xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel": "检查频率",
"xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip": "定义评估条件的频率。检查已排队;它们的运行接近于容量允许的定义值。",
"xpack.triggersActionsUI.sections.ruleForm.conditions.addConditionLabel": "添加:",
"xpack.triggersActionsUI.sections.ruleForm.conditions.removeConditionLabel": "移除",
"xpack.triggersActionsUI.sections.ruleForm.conditions.title": "条件:",
"xpack.triggersActionsUI.sections.ruleForm.documentationLabel": "了解详情",
"xpack.triggersActionsUI.sections.ruleForm.error.actionThrottleBelowSchedule": "定制操作时间间隔不能短于规则的检查时间间隔",
"xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText": "时间间隔必须至少为 {minimum}。",
"xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypes": "为了{operation}规则,您需要获得相应的权限。",
"xpack.triggersActionsUI.sections.ruleForm.error.noAuthorizedRuleTypesTitle": "您尚无权{operation}任何规则类型",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector": "“{actionTypeId} 连接器的操作”必填。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredConsumerText": "“范围”必填。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText": "“检查时间间隔”必填。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText": "“名称”必填。",
"xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText": "“规则类型”必填。",
"xpack.triggersActionsUI.sections.ruleForm.flappingLabel": "告警摆动检测",
"xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypeParamsDescription": "正在加载规则类型参数……",
"xpack.triggersActionsUI.sections.ruleForm.loadingRuleTypesDescription": "正在加载规则类型……",
"xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel": "通知",
"xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip": "定义告警生成操作的频率。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNameLabel": "名称",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label": "每",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description": "操作在告警状态更改时运行。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display": "在状态更改时",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label": "在状态更改时",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description": "操作在符合规则条件时运行。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display": "按检查时间间隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label": "按检查时间间隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description": "操作在符合规则条件时运行。",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display": "按定制操作时间间隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label": "按定制操作时间间隔",
"xpack.triggersActionsUI.sections.ruleForm.ruleScheduleLabel": "规则计划",
"xpack.triggersActionsUI.sections.ruleForm.ruleTypeSelectLabel": "选择规则类型",
"xpack.triggersActionsUI.sections.ruleForm.searchPlaceholderTitle": "搜索",
"xpack.triggersActionsUI.sections.ruleForm.solutionFilterLabel": "按用例筛选",
"xpack.triggersActionsUI.sections.ruleForm.tagsFieldLabel": "标签",
"xpack.triggersActionsUI.sections.ruleForm.tagsFieldOptional": "可选",
"xpack.triggersActionsUI.sections.ruleForm.unableToLoadRuleTypesMessage": "无法加载规则类型",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.apm": "APM 和用户体验",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.ariaLabel": "选择范围",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.placeholder": "选择范围",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.infrastructure": "指标",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.logs": "日志",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel": "角色可见性",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.slo": "SLO",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.stackAlerts": "Stack 规则",
"xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.uptime": "Synthetics 和 Uptime",
"xpack.triggersActionsUI.sections.rules_list.rules_tag_badge.tagTitle": "标签",
"xpack.triggersActionsUI.sections.rulesConnectorList.tabText": "规则",
"xpack.triggersActionsUI.sections.rulesList.ableToRunRuleSoon": "计划运行您的规则",

View file

@ -71,7 +71,6 @@ export const StorybookContextDecorator: FC<PropsWithChildren<StorybookContextDec
ruleKqlBar: true,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
isUsingRuleCreateFlyout: false,
},
});

View file

@ -21,7 +21,6 @@ export const allowedExperimentalValues = Object.freeze({
ruleKqlBar: false,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
isUsingRuleCreateFlyout: false,
});
const deprecatedExperimentalValues = new Set(['ruleFormV2']);

View file

@ -72,7 +72,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
'xl'
)({
showCreateRuleButtonInPrompt: true,
useNewRuleForm: true,
setHeaderActions,
});
}, []);

View file

@ -6,8 +6,7 @@
*/
import type { SubFeature } from '@kbn/actions-plugin/common';
import type { RuleType } from '../../types';
import type { InitialRule } from '../sections/rule_form/rule_reducer';
import type { RuleType, Rule } from '../../types';
/**
* NOTE: Applications that want to show the alerting UIs will need to add
@ -25,15 +24,12 @@ export const hasExecuteActionsCapability = (capabilities: Capabilities, subFeatu
export const hasDeleteActionsCapability = (capabilities: Capabilities) =>
capabilities?.actions?.delete;
export function hasAllPrivilege(
ruleConsumer: InitialRule['consumer'],
ruleType?: RuleType
): boolean {
export function hasAllPrivilege(ruleConsumer: Rule['consumer'], ruleType?: RuleType): boolean {
return ruleType?.authorizedConsumers[ruleConsumer]?.all ?? false;
}
export function hasAllPrivilegeWithProducerCheck(
ruleConsumer: InitialRule['consumer'],
ruleConsumer: Rule['consumer'],
ruleType?: RuleType
): boolean {
if (ruleConsumer === ruleType?.producer) {
@ -42,9 +38,5 @@ export function hasAllPrivilegeWithProducerCheck(
return hasAllPrivilege(ruleConsumer, ruleType);
}
export function hasReadPrivilege(rule: InitialRule, ruleType?: RuleType): boolean {
return ruleType?.authorizedConsumers[rule.consumer]?.read ?? false;
}
export const hasManageApiKeysCapability = (capabilities: Capabilities) =>
capabilities?.management?.security?.api_keys;

View file

@ -56,8 +56,7 @@ const TriggersActionsUIHome = lazy(() => import('./home'));
const RuleDetailsRoute = lazy(
() => import('./sections/rule_details/components/rule_details_route')
);
const CreateRuleRoute = lazy(() => import('./sections/rule_form/rule_form_route'));
const EditRuleRoute = lazy(() => import('./sections/rule_form/rule_form_route'));
const RuleFormRoute = lazy(() => import('./sections/rule_form/rule_form_route'));
export interface TriggersAndActionsUiServices extends CoreStart {
actions: ActionsPublicPluginSetup;
@ -127,12 +126,12 @@ export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) =
<Route
exact
path={createRuleRoute}
component={suspendedComponentWithProps(CreateRuleRoute, 'xl')}
component={suspendedComponentWithProps(RuleFormRoute, 'xl')}
/>
<Route
exact
path={editRuleRoute}
component={suspendedComponentWithProps(EditRuleRoute, 'xl')}
component={suspendedComponentWithProps(RuleFormRoute, 'xl')}
/>
<Route
path={`/:section(${sectionsRegex})`}

View file

@ -10,17 +10,6 @@ import { suspendedComponentWithProps } from '../lib/suspended_component_with_pro
import type { CreateConnectorFlyoutProps } from './action_connector_form/create_connector_flyout';
import type { EditConnectorFlyoutProps } from './action_connector_form/edit_connector_flyout';
export type {
ActionGroupWithCondition,
RuleConditionsProps as AlertConditionsProps,
} from './rule_form/rule_conditions';
export const AlertConditions = lazy(() => import('./rule_form/rule_conditions'));
export const AlertConditionsGroup = lazy(() => import('./rule_form/rule_conditions_group'));
export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./rule_form/rule_add')));
export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./rule_form/rule_edit')));
export const ConnectorAddFlyout = suspendedComponentWithProps<CreateConnectorFlyoutProps>(
lazy(() => import('./action_connector_form/create_connector_flyout'))
);

View file

@ -223,7 +223,6 @@ export function RuleComponent({
actionTypeRegistry,
ruleTypeRegistry,
hideEditButton: true,
useNewRuleForm: true,
onEditRule: requestRefresh,
})}
</EuiFlexGroup>

View file

@ -16,9 +16,9 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RuleNotifyWhenType } from '@kbn/alerting-plugin/common';
import { NOTIFY_WHEN_OPTIONS } from '@kbn/response-ops-rule-form';
import { ActionTypeRegistryContract, suspendedComponentWithProps } from '../../../..';
import { useFetchRuleActionConnectors } from '../../../hooks/use_fetch_rule_action_connectors';
import { NOTIFY_WHEN_OPTIONS } from '../../rule_form/rule_notify_when';
import { RuleUiAction } from '../../../../types';
export interface RuleActionsProps {
@ -54,15 +54,17 @@ export function RuleActions({
const getNotifyText = (action: RuleUiAction, isSystemAction?: boolean): string | ReactNode => {
if (isSystemAction) {
return NOTIFY_WHEN_OPTIONS[1].inputDisplay;
return NOTIFY_WHEN_OPTIONS[1].value.inputDisplay;
}
if ('frequency' in action) {
const notifyWhen = NOTIFY_WHEN_OPTIONS.find(
(options) => options.value === action.frequency?.notifyWhen
(options) => options.value.value === action.frequency?.notifyWhen
);
return notifyWhen?.inputDisplay ?? action.frequency?.notifyWhen ?? legacyNotifyWhen ?? '';
return (
notifyWhen?.value.inputDisplay ?? action.frequency?.notifyWhen ?? legacyNotifyWhen ?? ''
);
}
return '';

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useState, useEffect, useReducer, useMemo } from 'react';
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import {
EuiPageHeader,
@ -52,14 +52,12 @@ import {
} from '../../common/components/with_bulk_rule_api_operations';
import { RuleRouteWithApi } from './rule_route';
import { ViewInApp } from './view_in_app';
import { RuleEdit } from '../../rule_form';
import { routeToRules } from '../../../constants';
import {
rulesErrorReasonTranslationsMapping,
rulesWarningReasonTranslationsMapping,
} from '../../rules_list/translations';
import { useKibana } from '../../../../common/lib/kibana';
import { getRuleReducer } from '../../rule_form/rule_reducer';
import { loadAllActions as loadConnectors } from '../../../lib/action_connector_api';
import { runRule } from '../../../lib/run_rule';
import {
@ -71,7 +69,6 @@ import {
import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast';
import { RefreshToken } from './types';
import { UntrackAlertsModal } from '../../common/components/untrack_alerts_modal';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
export type RuleDetailsProps = {
rule: Rule;
@ -79,7 +76,6 @@ export type RuleDetailsProps = {
actionTypes: ActionType[];
requestRefresh: () => Promise<void>;
refreshToken?: RefreshToken;
useNewRuleForm?: boolean;
} & Pick<
BulkOperationsComponentOpts,
'bulkDisableRules' | 'bulkEnableRules' | 'bulkDeleteRules' | 'snoozeRule' | 'unsnoozeRule'
@ -102,7 +98,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
const {
application: { capabilities, navigateToApp },
ruleTypeRegistry,
actionTypeRegistry,
setBreadcrumbs,
chrome,
http,
@ -112,14 +107,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
notifications: { toasts },
} = useKibana().services;
const isUsingRuleCreateFlyout = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]);
const [{}, dispatch] = useReducer(ruleReducer, { rule });
const setInitialRule = (value: Rule) => {
dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } });
};
const [rulesToDelete, setRulesToDelete] = useState<string[]>([]);
const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState<string[]>([]);
const [isUntrackAlertsModalOpen, setIsUntrackAlertsModalOpen] = useState<boolean>(false);
@ -178,7 +165,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext
: false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const onRunRule = async (id: string) => {
await runRule(http, toasts, id);
};
@ -240,10 +226,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
hasEditButton,
]);
const setRule = async () => {
history.push(getRuleDetailsRoute(rule.id));
};
const goToRulesList = () => {
history.push(routeToRules);
};
@ -265,17 +247,13 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
};
const onEditRuleClick = () => {
if (!isUsingRuleCreateFlyout) {
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`,
state: {
returnApp: 'management',
returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
},
});
} else {
setEditFlyoutVisibility(true);
}
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(rule.id)}`,
state: {
returnApp: 'management',
returnPath: `insightsAndAlerting/triggersActions/${getRuleDetailsRoute(rule.id)}`,
},
});
};
const editButton = hasEditButton ? (
@ -292,19 +270,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
defaultMessage="Edit"
/>
</EuiButtonEmpty>
{editFlyoutVisible && (
<RuleEdit
initialRule={rule}
onClose={() => {
setInitialRule(rule);
setEditFlyoutVisibility(false);
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
ruleType={ruleType}
onSave={setRule}
/>
)}
</>
) : null;

View file

@ -1,53 +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 { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
interface Props {
onConfirm: () => void;
onCancel: () => void;
}
export const ConfirmRuleClose: React.FC<Props> = ({ onConfirm, onCancel }) => {
return (
<EuiConfirmModal
title={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseTitle',
{
defaultMessage: 'Discard unsaved changes to rule?',
}
)}
onCancel={onCancel}
onConfirm={onConfirm}
buttonColor="danger"
confirmButtonText={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText',
{
defaultMessage: 'Discard changes',
}
)}
cancelButtonText={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseCancelButtonText',
{
defaultMessage: 'Cancel',
}
)}
defaultFocusedButton="confirm"
data-test-subj="confirmRuleCloseModal"
>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseMessage"
defaultMessage="You can't recover unsaved changes."
/>
</p>
</EuiConfirmModal>
);
};

View file

@ -1,52 +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 { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
interface Props {
onConfirm: () => void;
onCancel: () => void;
}
export const ConfirmRuleSave: React.FC<Props> = ({ onConfirm, onCancel }) => {
return (
<EuiConfirmModal
title={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveTitle',
{
defaultMessage: 'Save rule with no actions?',
}
)}
onCancel={onCancel}
onConfirm={onConfirm}
confirmButtonText={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveConfirmButtonText',
{
defaultMessage: 'Save rule',
}
)}
cancelButtonText={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveCancelButtonText',
{
defaultMessage: 'Cancel',
}
)}
defaultFocusedButton="confirm"
data-test-subj="confirmRuleSaveModal"
>
<p>
<FormattedMessage
id="xpack.triggersActionsUI.sections.confirmRuleSave.confirmRuleSaveWithoutActionsMessage"
defaultMessage="You can add an action at anytime."
/>
</p>
</EuiConfirmModal>
);
};

View file

@ -1,23 +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 { getInitialInterval } from './get_initial_interval';
import { DEFAULT_RULE_INTERVAL } from '../../constants';
describe('getInitialInterval', () => {
test('should return DEFAULT_RULE_INTERVAL if minimumScheduleInterval is undefined', () => {
expect(getInitialInterval()).toEqual(DEFAULT_RULE_INTERVAL);
});
test('should return DEFAULT_RULE_INTERVAL if minimumScheduleInterval is smaller than or equal to default', () => {
expect(getInitialInterval('1m')).toEqual(DEFAULT_RULE_INTERVAL);
});
test('should return minimumScheduleInterval if minimumScheduleInterval is greater than default', () => {
expect(getInitialInterval('5m')).toEqual('5m');
});
});

View file

@ -1,19 +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 { parseDuration } from '@kbn/alerting-plugin/common';
import { DEFAULT_RULE_INTERVAL } from '../../constants';
export function getInitialInterval(minimumScheduleInterval?: string) {
if (minimumScheduleInterval) {
// return minimum schedule interval if it is larger than the default
if (parseDuration(minimumScheduleInterval) > parseDuration(DEFAULT_RULE_INTERVAL)) {
return minimumScheduleInterval;
}
}
return DEFAULT_RULE_INTERVAL;
}

View file

@ -1,165 +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 type { InitialRule } from './rule_reducer';
import { hasRuleChanged } from './has_rule_changed';
function createRule(overrides = {}): InitialRule {
return {
params: {},
consumer: 'test',
ruleTypeId: 'test',
schedule: {
interval: '1m',
},
actions: [],
tags: [],
notifyWhen: 'onActionGroupChange',
...overrides,
};
}
test('should return false for same rule', () => {
const a = createRule();
expect(hasRuleChanged(a, a, true)).toEqual(false);
});
test('should return true for different rule', () => {
const a = createRule();
const b = createRule({ ruleTypeId: 'differentTest' });
expect(hasRuleChanged(a, b, true)).toEqual(true);
});
test('should correctly compare name field', () => {
// name field doesn't exist initially
const a = createRule();
// set name to actual value
const b = createRule({ name: 'myRule' });
// set name to different value
const c = createRule({ name: 'anotherRule' });
// set name to various empty/null/undefined states
const d = createRule({ name: '' });
const e = createRule({ name: undefined });
const f = createRule({ name: null });
expect(hasRuleChanged(a, b, true)).toEqual(true);
expect(hasRuleChanged(a, c, true)).toEqual(true);
expect(hasRuleChanged(a, d, true)).toEqual(false);
expect(hasRuleChanged(a, e, true)).toEqual(false);
expect(hasRuleChanged(a, f, true)).toEqual(false);
expect(hasRuleChanged(b, c, true)).toEqual(true);
expect(hasRuleChanged(b, d, true)).toEqual(true);
expect(hasRuleChanged(b, e, true)).toEqual(true);
expect(hasRuleChanged(b, f, true)).toEqual(true);
expect(hasRuleChanged(c, d, true)).toEqual(true);
expect(hasRuleChanged(c, e, true)).toEqual(true);
expect(hasRuleChanged(c, f, true)).toEqual(true);
expect(hasRuleChanged(d, e, true)).toEqual(false);
expect(hasRuleChanged(d, f, true)).toEqual(false);
});
test('should correctly compare ruleTypeId field', () => {
const a = createRule();
// set ruleTypeId to different value
const b = createRule({ ruleTypeId: 'myRuleId' });
// set ruleTypeId to various empty/null/undefined states
const c = createRule({ ruleTypeId: '' });
const d = createRule({ ruleTypeId: undefined });
const e = createRule({ ruleTypeId: null });
expect(hasRuleChanged(a, b, true)).toEqual(true);
expect(hasRuleChanged(a, c, true)).toEqual(true);
expect(hasRuleChanged(a, d, true)).toEqual(true);
expect(hasRuleChanged(a, e, true)).toEqual(true);
expect(hasRuleChanged(b, c, true)).toEqual(true);
expect(hasRuleChanged(b, d, true)).toEqual(true);
expect(hasRuleChanged(b, e, true)).toEqual(true);
expect(hasRuleChanged(c, d, true)).toEqual(false);
expect(hasRuleChanged(c, e, true)).toEqual(false);
expect(hasRuleChanged(d, e, true)).toEqual(false);
});
test('should correctly compare throttle field', () => {
// throttle field doesn't exist initially
const a = createRule();
// set throttle to actual value
const b = createRule({ throttle: '1m' });
// set throttle to different value
const c = createRule({ throttle: '1h' });
// set throttle to various empty/null/undefined states
const d = createRule({ throttle: '' });
const e = createRule({ throttle: undefined });
const f = createRule({ throttle: null });
expect(hasRuleChanged(a, b, true)).toEqual(true);
expect(hasRuleChanged(a, c, true)).toEqual(true);
expect(hasRuleChanged(a, d, true)).toEqual(false);
expect(hasRuleChanged(a, e, true)).toEqual(false);
expect(hasRuleChanged(a, f, true)).toEqual(false);
expect(hasRuleChanged(b, c, true)).toEqual(true);
expect(hasRuleChanged(b, d, true)).toEqual(true);
expect(hasRuleChanged(b, e, true)).toEqual(true);
expect(hasRuleChanged(b, f, true)).toEqual(true);
expect(hasRuleChanged(c, d, true)).toEqual(true);
expect(hasRuleChanged(c, e, true)).toEqual(true);
expect(hasRuleChanged(c, f, true)).toEqual(true);
expect(hasRuleChanged(d, e, true)).toEqual(false);
expect(hasRuleChanged(d, f, true)).toEqual(false);
});
test('should correctly compare tags field', () => {
const a = createRule();
const b = createRule({ tags: ['first'] });
expect(hasRuleChanged(a, b, true)).toEqual(true);
});
test('should correctly compare schedule field', () => {
const a = createRule();
const b = createRule({ schedule: { interval: '3h' } });
expect(hasRuleChanged(a, b, true)).toEqual(true);
});
test('should correctly compare actions field', () => {
const a = createRule();
const b = createRule({
actions: [{ actionTypeId: 'action', group: 'group', id: 'actionId', params: {} }],
});
expect(hasRuleChanged(a, b, true)).toEqual(true);
});
test('should skip comparing params field if compareParams=false', () => {
const a = createRule();
const b = createRule({ params: { newParam: 'value' } });
expect(hasRuleChanged(a, b, false)).toEqual(false);
});
test('should correctly compare params field if compareParams=true', () => {
const a = createRule();
const b = createRule({ params: { newParam: 'value' } });
expect(hasRuleChanged(a, b, true)).toEqual(true);
});
test('should correctly compare notifyWhen field', () => {
const a = createRule();
const b = createRule({ notifyWhen: 'onActiveAlert' });
expect(hasRuleChanged(a, b, true)).toEqual(true);
});

View file

@ -1,42 +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 deepEqual from 'fast-deep-equal';
import { pick } from 'lodash';
import type { RuleTypeParams } from '../../../types';
import type { InitialRule } from './rule_reducer';
const DEEP_COMPARE_FIELDS = ['tags', 'schedule', 'actions'];
function getNonNullCompareFields(rule: InitialRule) {
const { name, ruleTypeId, throttle, notifyWhen } = rule;
return {
...(!!(name && name.length > 0) ? { name } : {}),
...(!!(ruleTypeId && ruleTypeId.length > 0) ? { ruleTypeId } : {}),
...(!!(throttle && throttle.length > 0) ? { throttle } : {}),
...(!!(notifyWhen && notifyWhen.length > 0) ? { notifyWhen } : {}),
};
}
export function hasRuleChanged(a: InitialRule, b: InitialRule, compareParams: boolean) {
// Deep compare these fields
let objectsAreEqual = deepEqual(pick(a, DEEP_COMPARE_FIELDS), pick(b, DEEP_COMPARE_FIELDS));
if (compareParams) {
objectsAreEqual = objectsAreEqual && deepEqual(a.params, b.params);
}
const nonNullCompareFieldsAreEqual = deepEqual(
getNonNullCompareFields(a),
getNonNullCompareFields(b)
);
return !objectsAreEqual || !nonNullCompareFieldsAreEqual;
}
export function haveRuleParamsChanged(a: RuleTypeParams, b: RuleTypeParams) {
return !deepEqual(a, b);
}

View file

@ -1,19 +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 { lazy } from 'react';
import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props';
import type { RuleAddComponent } from './rule_add';
import type { RuleEditComponent } from './rule_edit';
export const RuleAdd = suspendedComponentWithProps(
lazy(() => import('./rule_add'))
) as RuleAddComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component
export const RuleEdit = suspendedComponentWithProps(
lazy(() => import('./rule_edit'))
) as RuleEditComponent; // `React.lazy` is not typed correctly to support generics so casting back to imported component

View file

@ -1,623 +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 { v4 as uuidv4 } from 'uuid';
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { render, screen, within } from '@testing-library/react';
import { EuiFormLabel } from '@elastic/eui';
import { coreMock } from '@kbn/core/public/mocks';
import RuleAdd from './rule_add';
import { createRule } from '@kbn/response-ops-rule-form/src/common/apis/create_rule';
import { fetchAlertingFrameworkHealth as fetchAlertingFrameworkHealth } from '@kbn/alerts-ui-shared/src/common/apis/fetch_alerting_framework_health';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { AlertConsumers, OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import {
Rule,
RuleAddProps,
RuleFlyoutCloseReason,
GenericValidationResult,
ValidationResult,
RuleCreationValidConsumer,
RuleType,
RuleTypeModel,
} from '../../../types';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { useKibana } from '../../../common/lib/kibana';
import { fetchUiConfig } from '@kbn/response-ops-rule-form/src/common/apis/fetch_ui_config';
import { fetchUiHealthStatus } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status';
import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { waitFor } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import userEvent from '@testing-library/user-event';
jest.mock('../../../common/lib/kibana');
jest.mock('../../lib/rule_api/rule_types', () => ({
loadRuleTypes: jest.fn(),
}));
jest.mock('@kbn/response-ops-rule-form/src/common/apis/create_rule', () => ({
createRule: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_alerting_framework_health', () => ({
fetchAlertingFrameworkHealth: jest.fn(() => ({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
})),
}));
jest.mock('@kbn/response-ops-rule-form/src/common/apis/fetch_ui_config', () => ({
fetchUiConfig: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () => ({
fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })),
}));
jest.mock('../../lib/action_connector_api', () => ({
loadActionTypes: jest.fn(),
loadAllActions: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
fetchFlappingSettings: jest.fn().mockResolvedValue({
lookBackWindow: 20,
statusChangeThreshold: 20,
}),
}));
const actionTypeRegistry = actionTypeRegistryMock.create();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
export const TestExpression: FunctionComponent<any> = () => {
return (
<EuiFormLabel>
<FormattedMessage
defaultMessage="Metadata: {val}. Fields: {fields}."
id="xpack.triggersActionsUI.sections.ruleAdd.metadataTest"
values={{ val: 'test', fields: '' }}
/>
</EuiFormLabel>
);
};
describe('rule_add', () => {
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks();
});
async function setup({
initialValues,
onClose = jest.fn(),
defaultScheduleInterval,
ruleTypeId,
actionsShow = false,
validConsumers,
ruleTypesOverwrite,
ruleTypeModelOverwrite,
}: {
initialValues?: Partial<Rule>;
onClose?: RuleAddProps['onClose'];
defaultScheduleInterval?: string;
ruleTypeId?: string;
actionsShow?: boolean;
validConsumers?: RuleCreationValidConsumer[];
ruleTypesOverwrite?: RuleType[];
ruleTypeModelOverwrite?: RuleTypeModel;
}) {
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const mocks = coreMock.createSetup();
const { loadRuleTypes } = jest.requireMock('../../lib/rule_api/rule_types');
const ruleTypes = ruleTypesOverwrite || [
{
id: 'my-rule-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
defaultActionGroupId: 'testActionGroup',
defaultScheduleInterval,
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTING_FEATURE_ID,
authorizedConsumers: {
[ALERTING_FEATURE_ID]: { read: true, all: true },
test: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
},
];
loadRuleTypes.mockResolvedValue(ruleTypes);
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
rulesSettings: {
writeFlappingSettingsUI: true,
},
rules: {
show: true,
save: true,
delete: true,
},
actions: {
show: actionsShow,
},
};
mocks.http.get.mockResolvedValue({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
});
const ruleType = ruleTypeModelOverwrite || {
id: 'my-rule-type',
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
};
const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel);
actionTypeRegistry.has.mockReturnValue(true);
ruleTypeRegistry.list.mockReturnValue([ruleType]);
ruleTypeRegistry.get.mockReturnValue(ruleType);
ruleTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.list.mockReturnValue([actionTypeModel]);
actionTypeRegistry.has.mockReturnValue(true);
return {
consumer: ALERTING_FEATURE_ID,
onClose,
initialValues,
onSave: () => {
return new Promise<void>(() => {});
},
actionTypeRegistry,
ruleTypeRegistry,
metadata: { test: 'some value', fields: ['test'] },
ruleTypeId,
validConsumers,
};
}
it('renders rule add flyout', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const props = await setup({
initialValues: {},
onClose,
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('addRuleFlyoutTitle')).toBeInTheDocument();
expect(await screen.findByTestId('saveRuleButton')).toBeInTheDocument();
expect(await screen.findByTestId('showRequestButton')).toBeInTheDocument();
await userEvent.click(await screen.findByTestId('cancelSaveRuleButton'));
expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.CANCELED, {
fields: ['test'],
test: 'some value',
});
});
it('renders selection of rule types to pick in the modal', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const props = await setup({
initialValues: {},
onClose,
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('my-rule-type-SelectOption')).toBeInTheDocument();
expect(await screen.findByText('Test')).toBeInTheDocument();
expect(await screen.findByText('test')).toBeInTheDocument();
});
it('renders a confirm close modal if the flyout is closed after inputs have changed', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const props = await setup({
initialValues: {},
onClose,
ruleTypeId: 'my-rule-type',
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('ruleNameInput')).toBeInTheDocument();
await userEvent.type(await screen.findByTestId('ruleNameInput'), 'my[Space]rule[Space]type');
expect(await screen.findByTestId('ruleNameInput')).toHaveValue('my rule type');
expect(await screen.findByTestId('comboBoxSearchInput')).toHaveValue('');
expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('m');
await userEvent.click(await screen.findByTestId('cancelSaveRuleButton'));
expect(onClose).not.toHaveBeenCalled();
expect(await screen.findByTestId('confirmRuleCloseModal')).toBeInTheDocument();
});
it('renders rule add flyout with initial values', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const props = await setup({
initialValues: {
name: 'Simple status rule',
tags: ['uptime', 'logs'],
schedule: {
interval: '1h',
},
},
onClose,
ruleTypeId: 'my-rule-type',
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('ruleNameInput')).toHaveValue('Simple status rule');
expect(
await within(await screen.findByTestId('tagsComboBox')).findByText('uptime')
).toBeInTheDocument();
expect(
await within(await screen.findByTestId('tagsComboBox')).findByText('logs')
).toBeInTheDocument();
expect(await screen.findByTestId('intervalInput')).toHaveValue(1);
expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('h');
});
it('renders rule add flyout with DEFAULT_RULE_INTERVAL if no initialValues specified and no minimumScheduleInterval', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({});
const props = await setup({ ruleTypeId: 'my-rule-type' });
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('intervalInput')).toHaveValue(1);
expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('m');
});
it('renders rule add flyout with minimumScheduleInterval if minimumScheduleInterval is greater than DEFAULT_RULE_INTERVAL', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '5m', enforce: false },
});
const props = await setup({ ruleTypeId: 'my-rule-type' });
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('intervalInput')).toHaveValue(5);
expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('m');
});
it('emit an onClose event when the rule is saved', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const rule = mockRule();
(createRule as jest.MockedFunction<typeof createRule>).mockResolvedValue(rule);
const props = await setup({
initialValues: {
name: 'Simple status rule',
ruleTypeId: 'my-rule-type',
tags: ['uptime', 'logs'],
schedule: {
interval: '1h',
},
},
onClose,
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('saveRuleButton')).toBeInTheDocument();
await userEvent.click(await screen.findByTestId('saveRuleButton'));
await waitFor(() => {
return expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED, {
test: 'some value',
fields: ['test'],
});
});
});
it('should set consumer automatically if only 1 authorized consumer exists', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const onClose = jest.fn();
const props = await setup({
initialValues: {
name: 'Simple rule',
consumer: 'alerts',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
tags: ['uptime', 'logs'],
schedule: {
interval: '1h',
},
},
onClose,
ruleTypesOverwrite: [
{
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
name: 'Threshold Rule',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
enabledInLicense: true,
category: 'my-category',
defaultActionGroupId: 'threshold.fired',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTING_FEATURE_ID,
authorizedConsumers: {
logs: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
},
],
ruleTypeModelOverwrite: {
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: TestExpression,
requiresAppContext: false,
},
validConsumers: [AlertConsumers.INFRASTRUCTURE, AlertConsumers.LOGS],
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('saveRuleButton')).toBeInTheDocument();
await waitFor(async () => {
await userEvent.click(await screen.findByTestId('saveRuleButton'));
return expect(createRule).toHaveBeenLastCalledWith(
expect.objectContaining({
rule: expect.objectContaining({
consumer: 'logs',
}),
})
);
});
});
it('should enforce any default interval', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const props = await setup({
initialValues: { ruleTypeId: 'my-rule-type' },
onClose: jest.fn(),
defaultScheduleInterval: '3h',
ruleTypeId: 'my-rule-type',
actionsShow: true,
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('intervalInputUnit')).toHaveValue('h');
expect(await screen.findByTestId('intervalInput')).toHaveValue(3);
});
it('should load connectors and connector types when there is a pre-selected rule type', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
const props = await setup({
initialValues: {},
onClose: jest.fn(),
ruleTypeId: 'my-rule-type',
actionsShow: true,
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
await waitFor(() => {
expect(fetchUiHealthStatus).toHaveBeenCalledTimes(1);
expect(fetchAlertingFrameworkHealth).toHaveBeenCalledTimes(1);
expect(loadActionTypes).toHaveBeenCalledTimes(1);
expect(loadAllActions).toHaveBeenCalledTimes(1);
});
});
it('should not load connectors and connector types when there is not an encryptionKey', async () => {
(fetchUiConfig as jest.Mock).mockResolvedValue({
minimumScheduleInterval: { value: '1m', enforce: false },
});
(fetchAlertingFrameworkHealth as jest.Mock).mockResolvedValue({
isSufficientlySecure: true,
hasPermanentEncryptionKey: false,
});
const props = await setup({
initialValues: {},
onClose: jest.fn(),
ruleTypeId: 'my-rule-type',
actionsShow: true,
});
render(
<IntlProvider locale="en">
<QueryClientProvider client={new QueryClient()}>
<RuleAdd {...props} />
</QueryClientProvider>
</IntlProvider>
);
await waitFor(() => {
expect(fetchUiHealthStatus).toHaveBeenCalledTimes(1);
expect(fetchAlertingFrameworkHealth).toHaveBeenCalledTimes(1);
expect(loadActionTypes).not.toHaveBeenCalled();
expect(loadAllActions).not.toHaveBeenCalled();
});
expect(
await screen.findByText('You must configure an encryption key to use Alerting.', {
collapseWhitespace: false,
})
).toBeInTheDocument();
});
});
function mockRule(overloads: Partial<Rule> = {}): Rule {
return {
id: uuidv4(),
enabled: true,
name: `rule-${uuidv4()}`,
tags: [],
ruleTypeId: '.noop',
consumer: 'consumer',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: null,
notifyWhen: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
revision: 0,
...overloads,
};
}

View file

@ -1,419 +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 { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiPortal, EuiTitle } from '@elastic/eui';
import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common';
import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import {
CreateRuleBody,
createRule,
fetchUiConfig as triggersActionsUiConfig,
} from '@kbn/response-ops-rule-form';
import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { useKibana } from '../../../common/lib/kibana';
import {
IErrorObject,
Rule,
RuleAddProps,
RuleCreationValidConsumer,
RuleFlyoutCloseReason,
RuleTypeIndex,
RuleTypeMetaData,
RuleTypeParams,
RuleUpdates,
TriggersActionsUiConfig,
} from '../../../types';
import { HealthCheck } from '../../components/health_check';
import { ToastWithCircuitBreakerContent } from '../../components/toast_with_circuit_breaker_content';
import { DEFAULT_RULE_INTERVAL, MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants';
import { HealthContextProvider } from '../../context/health_context';
import { hasShowActionsCapability } from '../../lib/capabilities';
import { loadRuleTypes } from '../../lib/rule_api/rule_types';
import { getRuleWithInvalidatedFields } from '../../lib/value_validators';
import { ConfirmRuleClose } from './confirm_rule_close';
import { ConfirmRuleSave } from './confirm_rule_save';
import { getInitialInterval } from './get_initial_interval';
import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed';
import RuleAddFooter from './rule_add_footer';
import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors';
import { RuleForm } from './rule_form';
import { InitialRule, getRuleReducer } from './rule_reducer';
import { ShowRequestModal } from './show_request_modal';
const defaultCreateRuleErrorMessage = i18n.translate(
'xpack.triggersActionsUI.sections.ruleAdd.saveErrorNotificationText',
{
defaultMessage: 'Cannot create rule.',
}
);
export type RuleAddComponent = typeof RuleAdd;
const RuleAdd = <
Params extends RuleTypeParams = RuleTypeParams,
MetaData extends RuleTypeMetaData = RuleTypeMetaData
>({
consumer,
ruleTypeRegistry,
actionTypeRegistry,
onClose,
canChangeTrigger,
ruleTypeId,
initialValues,
reloadRules,
onSave,
hideGrouping,
hideInterval,
metadata: initialMetadata,
filteredRuleTypes,
validConsumers,
useRuleProducer,
initialSelectedConsumer,
...props
}: RuleAddProps<Params, MetaData>) => {
const onSaveHandler = onSave ?? reloadRules;
const [metadata, setMetadata] = useState(initialMetadata);
const onChangeMetaData = useCallback((newMetadata: any) => setMetadata(newMetadata), []);
const initialRule: InitialRule = useMemo(() => {
return {
params: {},
consumer,
ruleTypeId,
schedule: {
interval: DEFAULT_RULE_INTERVAL,
},
actions: [],
tags: [],
...(initialValues ? initialValues : {}),
};
}, [ruleTypeId, consumer, initialValues]);
const ruleReducer = useMemo(() => getRuleReducer(actionTypeRegistry), [actionTypeRegistry]);
const [{ rule }, dispatch] = useReducer(ruleReducer, {
rule: initialRule,
});
const [config, setConfig] = useState<TriggersActionsUiConfig>({ isUsingSecurity: false });
const [initialRuleParams, setInitialRuleParams] = useState<RuleTypeParams>({});
const [isSaving, setIsSaving] = useState<boolean>(false);
const [isConfirmRuleSaveModalOpen, setIsConfirmRuleSaveModalOpen] = useState<boolean>(false);
const [isConfirmRuleCloseModalOpen, setIsConfirmRuleCloseModalOpen] = useState<boolean>(false);
const [isShowRequestModalOpen, setIsShowRequestModalOpen] = useState<boolean>(false);
const [ruleTypeIndex, setRuleTypeIndex] = useState<RuleTypeIndex | undefined>(
props.ruleTypeIndex
);
const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState<boolean>(false);
const [isRuleValid, setIsRuleValid] = useState<boolean>(false);
const selectableConsumer = useMemo(
() => rule.ruleTypeId && MULTI_CONSUMER_RULE_TYPE_IDS.includes(rule.ruleTypeId),
[rule]
);
const [selectedConsumer, setSelectedConsumer] = useState<
RuleCreationValidConsumer | null | undefined
>(selectableConsumer ? initialSelectedConsumer : null);
const setRule = (value: InitialRule) => {
dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } });
};
const setRuleProperty = <Key extends keyof Rule>(key: Key, value: Rule[Key] | null) => {
dispatch({ command: { type: 'setProperty' }, payload: { key, value } });
};
const {
http,
notifications: { toasts },
application: { capabilities },
isServerless,
...startServices
} = useKibana().services;
const canShowActions = hasShowActionsCapability(capabilities);
useEffect(() => {
(async () => {
setConfig(await triggersActionsUiConfig({ http }));
})();
}, [http]);
useEffect(() => {
if (ruleTypeId) {
setRuleProperty('ruleTypeId', ruleTypeId);
}
}, [ruleTypeId]);
useEffect(() => {
if (!props.ruleTypeIndex) {
(async () => {
const ruleTypes = await loadRuleTypes({ http });
const index: RuleTypeIndex = new Map();
for (const ruleType of ruleTypes) {
index.set(ruleType.id, ruleType);
}
setRuleTypeIndex(index);
})();
}
}, [props.ruleTypeIndex, http]);
useEffect(() => {
if (isEmpty(rule.params) && !isEmpty(initialRuleParams)) {
// rule params are explicitly cleared when the rule type is cleared.
// clear the "initial" params in order to capture the
// default when a new rule type is selected
setInitialRuleParams({});
} else if (isEmpty(initialRuleParams)) {
// captures the first change to the rule params,
// when consumers set a default value for the rule params
setInitialRuleParams(rule.params);
}
}, [rule.params, initialRuleParams]);
const [ruleActionsErrors, setRuleActionsErrors] = useState<IErrorObject[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
(async () => {
setIsLoading(true);
const res = await getRuleActionErrors(rule.actions, actionTypeRegistry);
setIsLoading(false);
setRuleActionsErrors([...res]);
})();
}, [rule.actions, actionTypeRegistry]);
useEffect(() => {
if (config.minimumScheduleInterval && !initialValues?.schedule?.interval) {
setRuleProperty('schedule', {
interval: getInitialInterval(config.minimumScheduleInterval.value),
});
}
}, [config.minimumScheduleInterval, initialValues]);
useEffect(() => {
if (rule.ruleTypeId && ruleTypeIndex) {
const type = ruleTypeIndex.get(rule.ruleTypeId);
if (type?.defaultScheduleInterval && !changedFromDefaultInterval) {
setRuleProperty('schedule', { interval: type.defaultScheduleInterval });
}
}
}, [rule.ruleTypeId, ruleTypeIndex, rule.schedule.interval, changedFromDefaultInterval]);
useEffect(() => {
if (rule.schedule.interval !== DEFAULT_RULE_INTERVAL && !changedFromDefaultInterval) {
setChangedFromDefaultInterval(true);
}
}, [rule.schedule.interval, changedFromDefaultInterval]);
const checkForChangesAndCloseFlyout = () => {
if (
hasRuleChanged(rule, initialRule, false) ||
haveRuleParamsChanged(rule.params, initialRuleParams)
) {
setIsConfirmRuleCloseModalOpen(true);
} else {
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
}
};
const saveRuleAndCloseFlyout = async () => {
const savedRule = await onSaveRule();
setIsSaving(false);
if (savedRule) {
onClose(RuleFlyoutCloseReason.SAVED, metadata);
if (onSaveHandler) {
onSaveHandler(metadata);
}
}
};
const ruleType = rule.ruleTypeId ? ruleTypeRegistry.get(rule.ruleTypeId) : null;
const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = useMemo(
() =>
getRuleErrors(
{
...rule,
...(selectableConsumer && selectedConsumer !== undefined
? { consumer: selectedConsumer }
: {}),
} as Rule,
ruleType,
config,
actionTypeRegistry,
isServerless
),
[rule, selectableConsumer, selectedConsumer, ruleType, config, actionTypeRegistry, isServerless]
);
// Confirm before saving if user is able to add actions but hasn't added any to this rule
const shouldConfirmSave = canShowActions && rule.actions?.length === 0;
async function onSaveRule(): Promise<Rule | undefined> {
try {
const { flapping, ...restRule } = rule;
const newRule = await createRule({
http,
rule: {
...restRule,
...(selectableConsumer && selectedConsumer ? { consumer: selectedConsumer } : {}),
...(IS_RULE_SPECIFIC_FLAPPING_ENABLED ? { flapping } : {}),
} as CreateRuleBody,
});
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.ruleAdd.saveSuccessNotificationText', {
defaultMessage: 'Created rule "{ruleName}"',
values: {
ruleName: newRule.name,
},
})
);
return newRule;
} catch (errorRes) {
const message = parseRuleCircuitBreakerErrorMessage(
errorRes.body?.message || defaultCreateRuleErrorMessage
);
toasts.addDanger({
title: message.summary,
...(message.details && {
text: toMountPoint(
<ToastWithCircuitBreakerContent>{message.details}</ToastWithCircuitBreakerContent>,
startServices
),
}),
});
}
}
useEffect(() => {
setIsRuleValid(isValidRule(rule, ruleErrors, ruleActionsErrors));
}, [rule, ruleErrors, ruleActionsErrors]);
return (
<EuiPortal>
<EuiFlyout
onClose={checkForChangesAndCloseFlyout}
aria-labelledby="flyoutRuleAddTitle"
size="m"
maxWidth={620}
ownFocus
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s" data-test-subj="addRuleFlyoutTitle">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Create rule"
id="xpack.triggersActionsUI.sections.ruleAdd.flyoutTitle"
/>
</h3>
</EuiTitle>
</EuiFlyoutHeader>
<HealthContextProvider>
<HealthCheck inFlyout={true} waitForCheck={true}>
<EuiFlyoutBody>
<RuleForm
canShowConsumerSelection
rule={rule}
config={config}
dispatch={dispatch}
errors={ruleErrors}
canChangeTrigger={canChangeTrigger}
operation={i18n.translate(
'xpack.triggersActionsUI.sections.ruleAdd.operationName',
{
defaultMessage: 'create',
}
)}
validConsumers={validConsumers}
selectedConsumer={selectedConsumer}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
metadata={metadata}
filteredRuleTypes={filteredRuleTypes}
hideGrouping={hideGrouping}
hideInterval={hideInterval}
onChangeMetaData={onChangeMetaData}
setConsumer={setSelectedConsumer}
useRuleProducer={useRuleProducer}
initialSelectedConsumer={initialSelectedConsumer}
/>
</EuiFlyoutBody>
<RuleAddFooter
isSaving={isSaving}
isFormLoading={isLoading}
isRuleValid={isRuleValid}
onSave={async () => {
setIsSaving(true);
if (isLoading || !isValidRule(rule, ruleErrors, ruleActionsErrors)) {
setRule(
getRuleWithInvalidatedFields(
rule as Rule,
ruleParamsErrors,
ruleBaseErrors,
ruleActionsErrors
)
);
setIsSaving(false);
return;
}
if (shouldConfirmSave) {
setIsConfirmRuleSaveModalOpen(true);
} else {
await saveRuleAndCloseFlyout();
}
}}
onCancel={checkForChangesAndCloseFlyout}
onShowRequest={() => {
setIsShowRequestModalOpen(true);
}}
/>
</HealthCheck>
</HealthContextProvider>
{isConfirmRuleSaveModalOpen && (
<ConfirmRuleSave
onConfirm={async () => {
setIsConfirmRuleSaveModalOpen(false);
await saveRuleAndCloseFlyout();
}}
onCancel={() => {
setIsSaving(false);
setIsConfirmRuleSaveModalOpen(false);
}}
/>
)}
{isConfirmRuleCloseModalOpen && (
<ConfirmRuleClose
onConfirm={() => {
setIsConfirmRuleCloseModalOpen(false);
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
}}
onCancel={() => {
setIsConfirmRuleCloseModalOpen(false);
}}
/>
)}
{isShowRequestModalOpen && (
<ShowRequestModal
onClose={() => {
setIsShowRequestModalOpen(false);
}}
rule={
{
...rule,
...(selectableConsumer && selectedConsumer ? { consumer: selectedConsumer } : {}),
} as RuleUpdates
}
/>
)}
</EuiFlyout>
</EuiPortal>
);
};
// eslint-disable-next-line import/no-default-export
export { RuleAdd as default };

View file

@ -1,100 +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 { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
EuiLoadingSpinner,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useHealthContext } from '../../context/health_context';
interface RuleAddFooterProps {
isSaving: boolean;
isFormLoading: boolean;
isRuleValid: boolean;
onSave: () => void;
onCancel: () => void;
onShowRequest: () => void;
}
export const RuleAddFooter = ({
isSaving,
onSave,
onCancel,
onShowRequest,
isFormLoading,
isRuleValid,
}: RuleAddFooterProps) => {
const { loadingHealthCheck } = useHealthContext();
return (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelSaveRuleButton" onClick={onCancel}>
{i18n.translate('xpack.triggersActionsUI.sections.ruleAddFooter.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
{isFormLoading ? (
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<EuiLoadingSpinner size="l" />
</EuiFlexItem>
) : (
<></>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton
fill
color="primary"
data-test-subj="showRequestButton"
isDisabled={loadingHealthCheck || !isRuleValid}
onClick={onShowRequest}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleAddFooter.showRequestButtonLabel"
defaultMessage="Show API request"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="success"
data-test-subj="saveRuleButton"
type="submit"
iconType="check"
isDisabled={loadingHealthCheck}
isLoading={isSaving}
onClick={onSave}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleAddFooter.saveButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
);
};
// eslint-disable-next-line import/no-default-export
export { RuleAddFooter as default };

View file

@ -1,263 +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 * as React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
import { RuleConditions, ActionGroupWithCondition } from './rule_conditions';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiTitle,
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiButtonEmpty,
} from '@elastic/eui';
describe('rule_conditions', () => {
async function setup(element: React.ReactElement): Promise<ReactWrapper<unknown>> {
const wrapper = mountWithIntl(element);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
return wrapper;
}
it('renders with custom headline', async () => {
const wrapper = await setup(
<RuleConditions
headline={'Set different threshold with their own status'}
actionGroups={[]}
/>
);
expect(wrapper.find(EuiTitle).find(FormattedMessage).prop('id')).toMatchInlineSnapshot(
`"xpack.triggersActionsUI.sections.ruleForm.conditions.title"`
);
expect(
wrapper.find(EuiTitle).find(FormattedMessage).prop('defaultMessage')
).toMatchInlineSnapshot(`"Conditions:"`);
expect(wrapper.find('[data-test-subj="ruleConditionsHeadline"]').get(0)).toMatchInlineSnapshot(`
<EuiText
color="subdued"
data-test-subj="ruleConditionsHeadline"
size="s"
>
Set different threshold with their own status
</EuiText>
`);
});
it('renders any action group with conditions on it', async () => {
const ConditionForm = ({
actionGroup,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>;
}) => {
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>Name</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.name}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>SomeProp</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{actionGroup?.conditions?.someProp}
</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
const wrapper = await setup(
<RuleConditions
actionGroups={[
{ id: 'default', name: 'Default', conditions: { someProp: 'my prop value' } },
]}
>
<ConditionForm />
</RuleConditions>
);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
default
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
Default
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(2))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
my prop value
</EuiDescriptionListDescription>
`);
});
it('doesnt render action group without conditions', async () => {
const ConditionForm = ({
actionGroup,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>;
}) => {
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
const wrapper = await setup(
<RuleConditions
actionGroups={[
{ id: 'default', name: 'Default', conditions: { someProp: 'default on a prop' } },
{
id: 'shouldRender',
name: 'Should Render',
conditions: { someProp: 'shouldRender on a prop' },
},
{
id: 'shouldntRender',
name: 'Should Not Render',
},
]}
>
<ConditionForm />
</RuleConditions>
);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(0))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
default
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).get(1))
.toMatchInlineSnapshot(`
<EuiDescriptionListDescription>
shouldRender
</EuiDescriptionListDescription>
`);
expect(wrapper.find(EuiDescriptionList).find(EuiDescriptionListDescription).length).toEqual(2);
});
it('render add buttons for action group without conditions', async () => {
const onInitializeConditionsFor = jest.fn();
const ConditionForm = ({
actionGroup,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>;
}) => {
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
const wrapper = await setup(
<RuleConditions
actionGroups={[
{
id: 'shouldntRenderLink',
name: 'Should Not Render Link',
conditions: { someProp: 'shouldRender on a prop' },
},
{
id: 'shouldRenderLink',
name: 'Should Render A Link',
},
]}
onInitializeConditionsFor={onInitializeConditionsFor}
>
<ConditionForm />
</RuleConditions>
);
expect(wrapper.find(EuiButtonEmpty).get(0)).toMatchInlineSnapshot(`
<EuiButtonEmpty
flush="left"
onClick={[Function]}
size="s"
>
Should Render A Link
</EuiButtonEmpty>
`);
wrapper.find(EuiButtonEmpty).simulate('click');
expect(onInitializeConditionsFor).toHaveBeenCalledWith({
id: 'shouldRenderLink',
name: 'Should Render A Link',
});
});
it('passes in any additional props the container passes in', async () => {
const callbackProp = jest.fn();
const ConditionForm = ({
actionGroup,
someCallbackProp,
}: {
actionGroup?: ActionGroupWithCondition<{ someProp: string }, string>;
someCallbackProp: (
actionGroup: ActionGroupWithCondition<{ someProp: string }, string>
) => void;
}) => {
if (!actionGroup) {
return <div />;
}
// call callback when the actionGroup is available
someCallbackProp(actionGroup);
return (
<EuiDescriptionList>
<EuiDescriptionListTitle>ID</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.id}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>Name</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{actionGroup?.name}</EuiDescriptionListDescription>
<EuiDescriptionListTitle>SomeProp</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{actionGroup?.conditions?.someProp}
</EuiDescriptionListDescription>
</EuiDescriptionList>
);
};
await setup(
<RuleConditions
actionGroups={[
{ id: 'default', name: 'Default', conditions: { someProp: 'my prop value' } },
]}
>
<ConditionForm someCallbackProp={callbackProp} />
</RuleConditions>
);
expect(callbackProp).toHaveBeenCalledWith({
id: 'default',
name: 'Default',
conditions: { someProp: 'my prop value' },
});
});
});

View file

@ -1,100 +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 * as React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { ReactWrapper } from 'enzyme';
import { RuleConditionsGroup } from './rule_conditions_group';
import { EuiFormRow, EuiButtonIcon } from '@elastic/eui';
describe('rule_conditions_group', () => {
async function setup(element: React.ReactElement): Promise<ReactWrapper<unknown>> {
const wrapper = mountWithIntl(element);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
return wrapper;
}
it('renders with actionGroup name as label', async () => {
const InnerComponent = () => <div>{'inner component'}</div>;
const wrapper = await setup(
<RuleConditionsGroup
actionGroup={{
id: 'myGroup',
name: 'My Group',
}}
>
<InnerComponent />
</RuleConditionsGroup>
);
expect(wrapper.find(EuiFormRow).prop('label')).toMatchInlineSnapshot(`
<EuiTitle
size="s"
>
<strong>
My Group
</strong>
</EuiTitle>
`);
expect(wrapper.find(InnerComponent).prop('actionGroup')).toMatchInlineSnapshot(`
Object {
"id": "myGroup",
"name": "My Group",
}
`);
});
it('renders a reset button when onResetConditionsFor is specified', async () => {
const onResetConditionsFor = jest.fn();
const wrapper = await setup(
<RuleConditionsGroup
actionGroup={{
id: 'myGroup',
name: 'My Group',
}}
onResetConditionsFor={onResetConditionsFor}
>
<div>{'inner component'}</div>
</RuleConditionsGroup>
);
expect(wrapper.find(EuiButtonIcon).prop('aria-label')).toMatchInlineSnapshot(`"Remove"`);
wrapper.find(EuiButtonIcon).simulate('click');
expect(onResetConditionsFor).toHaveBeenCalledWith({
id: 'myGroup',
name: 'My Group',
});
});
it('shouldnt render a reset button when isRequired is true', async () => {
const onResetConditionsFor = jest.fn();
const wrapper = await setup(
<RuleConditionsGroup
actionGroup={{
id: 'myGroup',
name: 'My Group',
conditions: true,
isRequired: true,
}}
onResetConditionsFor={onResetConditionsFor}
>
<div>{'inner component'}</div>
</RuleConditionsGroup>
);
expect(wrapper.find(EuiButtonIcon).length).toEqual(0);
});
});

View file

@ -1,270 +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 * as React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from 'react-dom/test-utils';
import { coreMock } from '@kbn/core/public/mocks';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ValidationResult, Rule, GenericValidationResult } from '../../../types';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { ReactWrapper } from 'enzyme';
import RuleEdit from './rule_edit';
import { useKibana } from '../../../common/lib/kibana';
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
jest.mock('../../../common/lib/kibana');
const actionTypeRegistry = actionTypeRegistryMock.create();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('../../lib/rule_api/rule_types', () => ({
loadRuleTypes: jest.fn(),
}));
jest.mock('@kbn/response-ops-rule-form/src/common/apis/update_rule', () => ({
updateRule: jest.fn().mockRejectedValue({ body: { message: 'Fail message' } }),
}));
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_alerting_framework_health', () => ({
fetchAlertingFrameworkHealth: jest.fn(() => ({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
})),
}));
jest.mock('@kbn/response-ops-rule-form/src/common/apis/fetch_ui_config', () => ({
fetchUiConfig: jest.fn().mockResolvedValue({
isUsingSecurity: true,
minimumScheduleInterval: { value: '1m', enforce: false },
}),
}));
jest.mock('./rule_errors', () => ({
getRuleActionErrors: jest.fn().mockImplementation(() => {
return [];
}),
getRuleErrors: jest.fn().mockImplementation(() => ({
ruleParamsErrors: {},
ruleBaseErrors: {},
ruleErrors: {
name: new Array<string>(),
'schedule.interval': new Array<string>(),
ruleTypeId: new Array<string>(),
actionConnectors: new Array<string>(),
},
})),
isValidRule: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_ui_health_status', () => ({
fetchUiHealthStatus: jest.fn(() => ({ isRulesAvailable: true })),
}));
jest.mock('@kbn/alerts-ui-shared/src/common/apis/fetch_flapping_settings', () => ({
fetchFlappingSettings: jest.fn().mockResolvedValue({
lookBackWindow: 20,
statusChangeThreshold: 20,
}),
}));
describe('rule_edit', () => {
let wrapper: ReactWrapper<any>;
let mockedCoreSetup: ReturnType<typeof coreMock.createSetup>;
beforeEach(() => {
mockedCoreSetup = coreMock.createSetup();
});
async function setup(initialRuleFields = {}) {
const [
{
application: { capabilities },
},
] = await mockedCoreSetup.getStartServices();
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
rulesSettings: {
writeFlappingSettingsUI: true,
},
rules: {
show: true,
save: true,
delete: true,
execute: true,
},
};
const { loadRuleTypes } = jest.requireMock('../../lib/rule_api/rule_types');
const ruleTypes = [
{
id: 'my-rule-type',
name: 'Test',
actionGroups: [
{
id: 'testActionGroup',
name: 'Test Action Group',
},
],
defaultActionGroupId: 'testActionGroup',
minimumLicenseRequired: 'basic',
recoveryActionGroup: { id: 'recovered', name: 'Recovered' },
producer: ALERTING_FEATURE_ID,
authorizedConsumers: {
[ALERTING_FEATURE_ID]: { read: true, all: true },
test: { read: true, all: true },
},
actionVariables: {
context: [],
state: [],
params: [],
},
},
];
const ruleType = {
id: 'my-rule-type',
iconClass: 'test',
description: 'test',
documentationUrl: null,
validate: (): ValidationResult => {
return { errors: {} };
},
ruleParamsExpression: () => <></>,
requiresAppContext: false,
};
const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
loadRuleTypes.mockResolvedValue(ruleTypes);
const rule: Rule = {
id: 'ab5661e0-197e-45ee-b477-302d89193b5e',
params: {
aggType: 'average',
threshold: [1000, 5000],
index: 'kibana_sample_data_flights',
timeField: 'timestamp',
aggField: 'DistanceMiles',
window: '1s',
comparator: 'between',
},
consumer: 'rules',
ruleTypeId: 'my-rule-type',
enabled: false,
schedule: { interval: '1m' },
actions: [
{
actionTypeId: 'my-action-type',
group: 'threshold met',
params: { message: 'Rule [{{ctx.metadata.name}}] has exceeded the threshold' },
id: '917f5d41-fbc4-4056-a8ad-ac592f7dcee2',
},
],
tags: [],
name: 'test rule',
throttle: null,
notifyWhen: null,
apiKeyOwner: null,
createdBy: 'elastic',
updatedBy: 'elastic',
createdAt: new Date(),
muteAll: false,
mutedInstanceIds: [],
updatedAt: new Date(),
executionStatus: {
status: 'unknown',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
revision: 0,
...initialRuleFields,
};
actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel);
actionTypeRegistry.has.mockReturnValue(true);
ruleTypeRegistry.list.mockReturnValue([ruleType]);
ruleTypeRegistry.get.mockReturnValue(ruleType);
ruleTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.list.mockReturnValue([actionTypeModel]);
actionTypeRegistry.has.mockReturnValue(true);
wrapper = mountWithIntl(
<QueryClientProvider client={new QueryClient()}>
<RuleEdit
onClose={() => {}}
initialRule={rule}
onSave={() => {
return new Promise<void>(() => {});
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
/>
</QueryClientProvider>
);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
}
it('renders rule edit flyout', async () => {
await setup();
expect(wrapper.find('[data-test-subj="editRuleFlyoutTitle"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="saveEditedRuleButton"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="showEditedRequestButton"]').exists()).toBeTruthy();
});
it('displays a toast message on save for server errors', async () => {
const { isValidRule } = jest.requireMock('./rule_errors');
(isValidRule as jest.Mock).mockImplementation(() => {
return true;
});
await setup({ name: undefined });
await act(async () => {
wrapper.find('[data-test-subj="saveEditedRuleButton"]').last().simulate('click');
});
expect(useKibanaMock().services.notifications.toasts.addDanger).toHaveBeenCalledWith({
title: 'Fail message',
});
});
it('should pass in the config into `getRuleErrors`', async () => {
const { getRuleErrors } = jest.requireMock('./rule_errors');
await setup();
const lastCall = getRuleErrors.mock.calls[getRuleErrors.mock.calls.length - 1];
expect(lastCall[2]).toBeDefined();
expect(lastCall[2]).toEqual({
isUsingSecurity: true,
minimumScheduleInterval: { value: '1m', enforce: false },
});
});
it('should render an alert icon next to save button stating the potential change in permissions', async () => {
// Use fake timers so we don't have to wait for the EuiToolTip timeout
jest.useFakeTimers({ legacyFakeTimers: true });
await setup();
expect(wrapper.find('[data-test-subj="changeInPrivilegesTip"]').exists()).toBeTruthy();
await act(async () => {
wrapper.find('[data-test-subj="changeInPrivilegesTip"]').first().simulate('mouseover');
});
// Run the timers so the EuiTooltip will be visible
jest.runOnlyPendingTimers();
wrapper.update();
expect(wrapper.find('.euiToolTipPopover').last().text()).toBe(
'Saving this rule will change its privileges and might change its behavior.'
);
});
});

View file

@ -1,413 +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 {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiIconTip,
EuiLoadingSpinner,
EuiPortal,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { RuleNotifyWhen, parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common';
import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '@kbn/alerts-ui-shared/src/common/constants/rule_flapping';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { fetchUiConfig as triggersActionsUiConfig, updateRule } from '@kbn/response-ops-rule-form';
import { cloneDeep, omit } from 'lodash';
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { useKibana } from '../../../common/lib/kibana';
import {
IErrorObject,
Rule,
RuleAction,
RuleEditProps,
RuleFlyoutCloseReason,
RuleNotifyWhenType,
RuleType,
RuleTypeMetaData,
RuleTypeParams,
RuleUiAction,
TriggersActionsUiConfig,
} from '../../../types';
import { HealthCheck } from '../../components/health_check';
import { ToastWithCircuitBreakerContent } from '../../components/toast_with_circuit_breaker_content';
import { HealthContextProvider } from '../../context/health_context';
import { loadRuleTypes } from '../../lib/rule_api/rule_types';
import { getRuleWithInvalidatedFields } from '../../lib/value_validators';
import { ConfirmRuleClose } from './confirm_rule_close';
import { hasRuleChanged } from './has_rule_changed';
import { getRuleActionErrors, getRuleErrors, isValidRule } from './rule_errors';
import { RuleForm } from './rule_form';
import { getRuleReducer } from './rule_reducer';
import { ShowRequestModal } from './show_request_modal';
const defaultUpdateRuleErrorMessage = i18n.translate(
'xpack.triggersActionsUI.sections.ruleEdit.saveErrorNotificationText',
{
defaultMessage: 'Cannot update rule.',
}
);
// Separate function for determining if an untyped action has a group property or not, which helps determine if
// it is a default action or a system action. Consolidated here to deal with type definition complexity
const actionHasDefinedGroup = (action: RuleUiAction): action is RuleAction => {
if (!('group' in action)) return false;
// If the group property is present, ensure that it isn't null or undefined
return Boolean(action.group);
};
const cloneAndMigrateRule = (initialRule: Rule) => {
const clonedRule = cloneDeep(omit(initialRule, 'notifyWhen', 'throttle'));
const hasRuleLevelNotifyWhen = Boolean(initialRule.notifyWhen);
const hasRuleLevelThrottle = Boolean(initialRule.throttle);
if (hasRuleLevelNotifyWhen || hasRuleLevelThrottle) {
const frequency = hasRuleLevelNotifyWhen
? {
summary: false,
notifyWhen: initialRule.notifyWhen as RuleNotifyWhenType,
throttle:
initialRule.notifyWhen === RuleNotifyWhen.THROTTLE ? initialRule.throttle! : null,
}
: { summary: false, notifyWhen: RuleNotifyWhen.THROTTLE, throttle: initialRule.throttle! };
clonedRule.actions = clonedRule.actions.map((action: RuleUiAction) => {
if (actionHasDefinedGroup(action)) {
return {
...action,
frequency,
};
}
return action;
});
}
return clonedRule;
};
export type RuleEditComponent = typeof RuleEdit;
export const RuleEdit = <
Params extends RuleTypeParams = RuleTypeParams,
MetaData extends RuleTypeMetaData = RuleTypeMetaData
>({
initialRule,
onClose,
reloadRules,
onSave,
hideInterval,
ruleTypeRegistry,
actionTypeRegistry,
metadata: initialMetadata,
...props
}: RuleEditProps<Params, MetaData>) => {
const onSaveHandler = onSave ?? reloadRules;
const ruleReducer = useMemo(() => getRuleReducer<Rule>(actionTypeRegistry), [actionTypeRegistry]);
const [{ rule }, dispatch] = useReducer(ruleReducer, {
rule: cloneAndMigrateRule(initialRule),
});
const [isSaving, setIsSaving] = useState<boolean>(false);
const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false);
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] =
useState<boolean>(false);
const [isConfirmRuleCloseModalOpen, setIsConfirmRuleCloseModalOpen] = useState<boolean>(false);
const [isShowRequestModalOpen, setIsShowRequestModalOpen] = useState<boolean>(false);
const [isRuleValid, setIsRuleValid] = useState<boolean>(false);
const [ruleActionsErrors, setRuleActionsErrors] = useState<IErrorObject[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [serverRuleType, setServerRuleType] = useState<RuleType<string, string> | undefined>(
props.ruleType
);
const [config, setConfig] = useState<TriggersActionsUiConfig>({ isUsingSecurity: false });
const [metadata, setMetadata] = useState(initialMetadata);
const onChangeMetaData = useCallback((newMetadata: any) => setMetadata(newMetadata), []);
const {
http,
notifications: { toasts },
isServerless,
...startServices
} = useKibana().services;
const setRule = (value: Rule) => {
dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } });
};
const ruleType = ruleTypeRegistry.get(rule.ruleTypeId);
useEffect(() => {
(async () => {
setConfig(await triggersActionsUiConfig({ http }));
})();
}, [http]);
useEffect(() => {
(async () => {
setIsLoading(true);
const res = await getRuleActionErrors(rule.actions, actionTypeRegistry);
setRuleActionsErrors([...res]);
setIsLoading(false);
})();
}, [rule.actions, actionTypeRegistry]);
useEffect(() => {
if (!props.ruleType && !serverRuleType) {
(async () => {
const serverRuleTypes = await loadRuleTypes({ http });
for (const _serverRuleType of serverRuleTypes) {
if (ruleType.id === _serverRuleType.id) {
setServerRuleType(_serverRuleType);
}
}
})();
}
}, [props.ruleType, ruleType.id, serverRuleType, http]);
const { ruleBaseErrors, ruleErrors, ruleParamsErrors } = getRuleErrors(
rule as Rule,
ruleType,
config,
actionTypeRegistry,
isServerless
);
const checkForChangesAndCloseFlyout = () => {
if (hasRuleChanged(rule, initialRule, true)) {
setIsConfirmRuleCloseModalOpen(true);
} else {
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
}
};
async function onSaveRule(): Promise<void> {
setIsSaving(true);
try {
if (
!isLoading &&
isValidRule(rule, ruleErrors, ruleActionsErrors) &&
!hasActionsWithBrokenConnector
) {
const { flapping, ...restRule } = rule;
const newRule = await updateRule({
http,
rule: {
...restRule,
...(IS_RULE_SPECIFIC_FLAPPING_ENABLED ? { flapping } : {}),
},
id: rule.id,
});
toasts.addSuccess(
i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText', {
defaultMessage: "Updated ''{ruleName}''",
values: {
ruleName: newRule.name,
},
})
);
onClose(RuleFlyoutCloseReason.SAVED, metadata);
if (onSaveHandler) {
onSaveHandler(metadata);
}
} else {
setRule(
getRuleWithInvalidatedFields(rule, ruleParamsErrors, ruleBaseErrors, ruleActionsErrors)
);
}
} catch (errorRes) {
const message = parseRuleCircuitBreakerErrorMessage(
errorRes.body?.message || defaultUpdateRuleErrorMessage
);
toasts.addDanger({
title: message.summary,
...(message.details && {
text: toMountPoint(
<ToastWithCircuitBreakerContent>{message.details}</ToastWithCircuitBreakerContent>,
startServices
),
}),
});
}
setIsSaving(false);
}
useEffect(() => {
setIsRuleValid(isValidRule(rule, ruleErrors, ruleActionsErrors));
}, [rule, ruleErrors, ruleActionsErrors]);
return (
<EuiPortal>
<EuiFlyout
onClose={checkForChangesAndCloseFlyout}
aria-labelledby="flyoutRuleEditTitle"
size="m"
maxWidth={620}
ownFocus
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s" data-test-subj="editRuleFlyoutTitle">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Edit rule"
id="xpack.triggersActionsUI.sections.ruleEdit.flyoutTitle"
/>
</h3>
</EuiTitle>
</EuiFlyoutHeader>
<HealthContextProvider>
<HealthCheck inFlyout={true} waitForCheck={true}>
<EuiFlyoutBody>
{hasActionsDisabled && (
<>
<EuiCallOut
size="s"
color="danger"
iconType="error"
data-test-subj="hasActionsDisabled"
title={i18n.translate(
'xpack.triggersActionsUI.sections.ruleEdit.disabledActionsWarningTitle',
{ defaultMessage: 'This rule has actions that are disabled' }
)}
/>
<EuiSpacer />
</>
)}
<RuleForm
rule={rule}
config={config}
dispatch={dispatch}
errors={ruleErrors}
actionTypeRegistry={actionTypeRegistry}
hideInterval={hideInterval}
ruleTypeRegistry={ruleTypeRegistry}
canChangeTrigger={false}
setHasActionsDisabled={setHasActionsDisabled}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
operation={i18n.translate(
'xpack.triggersActionsUI.sections.ruleEdit.operationName',
{
defaultMessage: 'edit',
}
)}
metadata={metadata}
onChangeMetaData={onChangeMetaData}
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="cancelSaveEditedRuleButton"
onClick={() => checkForChangesAndCloseFlyout()}
>
{i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
</EuiFlexItem>
{isLoading ? (
<EuiFlexItem grow={false}>
<EuiSpacer size="s" />
<EuiLoadingSpinner size="l" />
</EuiFlexItem>
) : (
<></>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton
fill
color="primary"
data-test-subj="showEditedRequestButton"
isDisabled={!isRuleValid}
onClick={() => {
setIsShowRequestModalOpen(true);
}}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleEdit.showRequestButtonLabel"
defaultMessage="Show API request"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
color="success"
data-test-subj="saveEditedRuleButton"
type="submit"
iconType="check"
isLoading={isSaving}
onClick={async () => await onSaveRule()}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleEdit.saveButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
{config.isUsingSecurity && (
<EuiFlexItem grow={false}>
<EuiIconTip
type="warning"
position="top"
data-test-subj="changeInPrivilegesTip"
content={i18n.translate(
'xpack.triggersActionsUI.sections.ruleEdit.changeInPrivilegesLabel',
{
defaultMessage:
'Saving this rule will change its privileges and might change its behavior.',
}
)}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</HealthCheck>
</HealthContextProvider>
{isConfirmRuleCloseModalOpen && (
<ConfirmRuleClose
onConfirm={() => {
setIsConfirmRuleCloseModalOpen(false);
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
}}
onCancel={() => {
setIsConfirmRuleCloseModalOpen(false);
}}
/>
)}
{isShowRequestModalOpen && (
<ShowRequestModal
onClose={() => {
setIsShowRequestModalOpen(false);
}}
rule={rule}
ruleId={rule.id}
edit={true}
/>
)}
</EuiFlyout>
</EuiPortal>
);
};
// eslint-disable-next-line import/no-default-export
export { RuleEdit as default };

View file

@ -1,347 +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 { v4 as uuidv4 } from 'uuid';
import React, { Fragment } from 'react';
import {
validateBaseProperties,
getRuleErrors,
getRuleActionErrors,
hasObjectErrors,
isValidRule,
} from './rule_errors';
import { Rule, RuleTypeModel } from '../../../types';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
import { ActionTypeModel } from '../../..';
const actionTypeRegistry = actionTypeRegistryMock.create();
const config = { isUsingSecurity: true, minimumScheduleInterval: { value: '1m', enforce: false } };
describe('rule_errors', () => {
describe('validateBaseProperties()', () => {
it('should validate the name', () => {
const rule = mockRule();
rule.name = '';
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: ['Name is required.'],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
});
});
it('should validate the interval', () => {
const rule = mockRule();
rule.schedule.interval = '';
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': ['Check interval is required.'],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
});
});
it('should validate the minimumScheduleInterval if enforce = false', () => {
const rule = mockRule();
rule.schedule.interval = '2s';
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
});
});
it('should validate the minimumScheduleInterval if enforce = true', () => {
const rule = mockRule();
rule.schedule.interval = '2s';
const result = validateBaseProperties(
rule,
{
isUsingSecurity: true,
minimumScheduleInterval: { value: '1m', enforce: true },
},
actionTypeRegistry
);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': ['Interval must be at least 1 minute.'],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
});
});
it('should validate the ruleTypeId', () => {
const rule = mockRule();
rule.ruleTypeId = '';
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: ['Rule type is required.'],
actionConnectors: [],
consumer: [],
});
});
it('should get an error when consumer is null', () => {
const rule = mockRule();
rule.consumer = null as unknown as string;
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: ['Scope is required.'],
});
});
it('should not get an error when consumer is undefined', () => {
const rule = mockRule();
rule.consumer = undefined as unknown as string;
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
});
});
it('should validate the connectors', () => {
const rule = mockRule();
rule.actions = [
{
id: '1234',
actionTypeId: 'myActionType',
group: '',
params: {
name: 'yes',
},
},
];
const actionType = {
id: 'test',
name: 'Test',
isSystemActionType: false,
} as unknown as ActionTypeModel;
actionTypeRegistry.get.mockReturnValue(actionType);
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: ['Action for myActionType connector is required.'],
consumer: [],
});
});
it('should not throw an error for system actions', () => {
const rule = mockRule();
rule.actions = [
{
id: '1234',
actionTypeId: '.test-system-action',
params: {},
},
];
const actionType = {
id: '.test-system-action',
name: 'Test',
isSystemActionType: true,
} as unknown as ActionTypeModel;
actionTypeRegistry.get.mockReturnValue(actionType);
const result = validateBaseProperties(rule, config, actionTypeRegistry);
expect(result.errors).toStrictEqual({
name: [],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
});
});
});
describe('getRuleErrors()', () => {
it('should return all errors', () => {
const result = getRuleErrors(
mockRule({
name: '',
}),
mockRuleTypeModel({
validate: () => ({
errors: {
field: ['This is wrong'],
},
}),
}),
config,
actionTypeRegistry
);
expect(result).toStrictEqual({
ruleParamsErrors: { field: ['This is wrong'] },
ruleBaseErrors: {
name: ['Name is required.'],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
},
ruleErrors: {
name: ['Name is required.'],
field: ['This is wrong'],
'schedule.interval': [],
ruleTypeId: [],
actionConnectors: [],
consumer: [],
},
});
});
});
describe('getRuleActionErrors()', () => {
it('should return an array of errors', async () => {
actionTypeRegistry.get.mockImplementation((actionTypeId: string) => ({
...actionTypeRegistryMock.createMockActionTypeModel(),
validateParams: jest.fn().mockImplementation(() => ({
errors: {
[actionTypeId]: ['Yes, this failed'],
},
})),
}));
const result = await getRuleActionErrors(
[
{
id: '1234',
actionTypeId: 'myActionType',
group: '',
params: {
name: 'yes',
},
},
{
id: '5678',
actionTypeId: 'myActionType2',
group: '',
params: {
name: 'yes',
},
},
],
actionTypeRegistry
);
expect(result).toStrictEqual([
{
myActionType: ['Yes, this failed'],
},
{
myActionType2: ['Yes, this failed'],
},
]);
});
});
describe('hasObjectErrors()', () => {
it('should return true for any errors', () => {
expect(
hasObjectErrors({
foo: ['1'],
})
).toBe(true);
expect(
hasObjectErrors({
foo: {
foo: ['1'],
},
})
).toBe(true);
});
it('should return false for no errors', () => {
expect(hasObjectErrors({})).toBe(false);
});
});
describe('isValidRule()', () => {
it('should return true for a valid rule', () => {
const result = isValidRule(mockRule(), {}, []);
expect(result).toBe(true);
});
it('should return false for an invalid rule', () => {
expect(
isValidRule(
mockRule(),
{
name: ['This is wrong'],
},
[]
)
).toBe(false);
expect(
isValidRule(mockRule(), {}, [
{
name: ['This is wrong'],
},
])
).toBe(false);
});
});
});
function mockRuleTypeModel(overloads: Partial<RuleTypeModel> = {}): RuleTypeModel {
return {
id: 'ruleTypeModel',
description: 'some rule',
iconClass: 'something',
documentationUrl: null,
validate: () => ({ errors: {} }),
ruleParamsExpression: () => <Fragment />,
requiresAppContext: false,
...overloads,
};
}
function mockRule(overloads: Partial<Rule> = {}): Rule {
return {
id: uuidv4(),
enabled: true,
name: `rule-${uuidv4()}`,
tags: [],
ruleTypeId: '.noop',
consumer: 'consumer',
schedule: { interval: '1m' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: new Date(),
updatedAt: new Date(),
apiKeyOwner: null,
throttle: null,
notifyWhen: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
},
revision: 0,
...overloads,
};
}

View file

@ -1,186 +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 { isObject } from 'lodash';
import { i18n } from '@kbn/i18n';
import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common';
import { RuleNotifyWhen } from '@kbn/alerting-plugin/common';
import { formatDuration, parseDuration } from '@kbn/alerting-plugin/common/parse_duration';
import type {
RuleTypeModel,
Rule,
IErrorObject,
ValidationResult,
ActionTypeRegistryContract,
TriggersActionsUiConfig,
RuleUiAction,
} from '../../../types';
import type { InitialRule } from './rule_reducer';
export function validateBaseProperties(
ruleObject: InitialRule,
config: TriggersActionsUiConfig,
actionTypeRegistry: ActionTypeRegistryContract
): ValidationResult {
const validationResult = { errors: {} };
const errors = {
name: new Array<string>(),
'schedule.interval': new Array<string>(),
consumer: new Array<string>(),
ruleTypeId: new Array<string>(),
actionConnectors: new Array<string>(),
};
validationResult.errors = errors;
if (!ruleObject.name) {
errors.name.push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredNameText', {
defaultMessage: 'Name is required.',
})
);
}
if (ruleObject.consumer === null) {
errors.consumer.push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredConsumerText', {
defaultMessage: 'Scope is required.',
})
);
}
if (ruleObject.schedule.interval.length < 2) {
errors['schedule.interval'].push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredIntervalText', {
defaultMessage: 'Check interval is required.',
})
);
} else if (config.minimumScheduleInterval && config.minimumScheduleInterval.enforce) {
const duration = parseDuration(ruleObject.schedule.interval);
const minimumDuration = parseDuration(config.minimumScheduleInterval.value);
if (duration < minimumDuration) {
errors['schedule.interval'].push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.belowMinimumText', {
defaultMessage: 'Interval must be at least {minimum}.',
values: {
minimum: formatDuration(config.minimumScheduleInterval.value, true),
},
})
);
}
}
const invalidThrottleActions = ruleObject.actions.filter((a) => {
if (actionTypeRegistry.get(a.actionTypeId).isSystemActionType) return false;
const defaultAction = a as SanitizedRuleAction;
if (!defaultAction.frequency?.throttle) return false;
const throttleDuration = parseDuration(defaultAction.frequency.throttle);
const intervalDuration =
ruleObject.schedule.interval && ruleObject.schedule.interval.length > 1
? parseDuration(ruleObject.schedule.interval)
: 0;
return (
defaultAction.frequency?.notifyWhen === RuleNotifyWhen.THROTTLE &&
throttleDuration < intervalDuration
);
});
if (invalidThrottleActions.length) {
errors['schedule.interval'].push(
i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.error.actionThrottleBelowSchedule',
{
defaultMessage:
"Custom action intervals cannot be shorter than the rule's check interval",
}
)
);
}
if (!ruleObject.ruleTypeId) {
errors.ruleTypeId.push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredRuleTypeIdText', {
defaultMessage: 'Rule type is required.',
})
);
}
const emptyConnectorActions = ruleObject.actions.find(
(actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0
);
if (emptyConnectorActions !== undefined) {
errors.actionConnectors.push(
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.error.requiredActionConnector', {
defaultMessage: 'Action for {actionTypeId} connector is required.',
values: { actionTypeId: emptyConnectorActions.actionTypeId },
})
);
}
return validationResult;
}
export function getRuleErrors(
rule: Rule,
ruleTypeModel: RuleTypeModel | null,
config: TriggersActionsUiConfig,
actionTypeRegistry: ActionTypeRegistryContract,
isServerless?: boolean
) {
const ruleParamsErrors: IErrorObject = ruleTypeModel
? ruleTypeModel.validate(rule.params, isServerless).errors
: {};
const ruleBaseErrors = validateBaseProperties(rule, config, actionTypeRegistry)
.errors as IErrorObject;
const ruleErrors = {
...ruleParamsErrors,
...ruleBaseErrors,
} as IErrorObject;
return {
ruleParamsErrors,
ruleBaseErrors,
ruleErrors,
};
}
export async function getRuleActionErrors(
actions: RuleUiAction[],
actionTypeRegistry: ActionTypeRegistryContract
): Promise<IErrorObject[]> {
return await Promise.all(
actions.map(
async (ruleAction: RuleUiAction) =>
(
await actionTypeRegistry.get(ruleAction.actionTypeId)?.validateParams(ruleAction.params)
).errors
)
);
}
export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) =>
!!Object.values(errors).find((errorList) => {
if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject);
return errorList.length >= 1;
});
export function isValidRule(
ruleObject: InitialRule | Rule,
validationResult: IErrorObject,
actionsErrors: IErrorObject[]
): ruleObject is Rule {
return (
!hasObjectErrors(validationResult) &&
actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error))
);
}

View file

@ -1,4 +0,0 @@
.triggersActionsUI__ruleTypeNodeHeading {
margin-left: $euiSizeS;
margin-right: $euiSizeS;
}

View file

@ -1,232 +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 { fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpServiceMock } from '@kbn/core/public/mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { RuleFormAdvancedOptions } from './rule_form_advanced_options';
import { useKibana } from '../../../common/lib/kibana';
import userEvent from '@testing-library/user-event';
import { ApplicationStart } from '@kbn/core-application-browser';
jest.mock('../../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const queryClient = new QueryClient();
const http = httpServiceMock.createStartContract();
const mockFlappingSettings = {
lookBackWindow: 5,
statusChangeThreshold: 5,
};
const mockOnflappingChange = jest.fn();
const mockAlertDelayChange = jest.fn();
describe('ruleFormAdvancedOptions', () => {
beforeEach(() => {
http.get.mockResolvedValue({
look_back_window: 10,
status_change_threshold: 3,
enabled: true,
});
useKibanaMock().services.http = http;
useKibanaMock().services.application.capabilities = {
rulesSettings: {
writeFlappingSettingsUI: true,
},
} as unknown as ApplicationStart['capabilities'];
});
afterEach(() => {
jest.clearAllMocks();
});
test('should render correctly', async () => {
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RuleFormAdvancedOptions
enabledFlapping
alertDelay={5}
flappingSettings={mockFlappingSettings}
onAlertDelayChange={mockAlertDelayChange}
onFlappingChange={mockOnflappingChange}
/>
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('ruleFormAdvancedOptions')).toBeInTheDocument();
expect(await screen.findByTestId('alertDelayFormRow')).toBeInTheDocument();
expect(await screen.findByTestId('alertFlappingFormRow')).toBeInTheDocument();
});
test('should initialize correctly when global flapping is on and override is not applied', async () => {
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RuleFormAdvancedOptions
enabledFlapping
alertDelay={5}
onAlertDelayChange={mockAlertDelayChange}
onFlappingChange={mockOnflappingChange}
/>
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByText('ON')).toBeInTheDocument();
expect(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeChecked();
expect(screen.queryByText('Custom')).not.toBeInTheDocument();
expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent(
'All rules (in this space) detect an alert is flapping when it changes status at least 3 times in the last 10 rule runs.'
);
await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch'));
expect(mockOnflappingChange).toHaveBeenCalledWith({
lookBackWindow: 10,
statusChangeThreshold: 3,
});
});
test('should initialize correctly when global flapping is on and override is appplied', async () => {
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RuleFormAdvancedOptions
enabledFlapping
alertDelay={5}
flappingSettings={{
lookBackWindow: 6,
statusChangeThreshold: 4,
}}
onAlertDelayChange={mockAlertDelayChange}
onFlappingChange={mockOnflappingChange}
/>
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('ruleFormAdvancedOptionsOverrideSwitch')).toBeChecked();
expect(screen.getByText('Custom')).toBeInTheDocument();
expect(screen.getByTestId('lookBackWindowRangeInput')).toHaveValue('6');
expect(screen.getByTestId('statusChangeThresholdRangeInput')).toHaveValue('4');
expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent(
'This rule detects an alert is flapping if it changes status at least 4 times in the last 6 rule runs.'
);
await userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch'));
expect(mockOnflappingChange).toHaveBeenCalledWith(null);
});
test('should not allow override when global flapping is off', async () => {
http.get.mockResolvedValue({
look_back_window: 10,
status_change_threshold: 3,
enabled: false,
});
useKibanaMock().services.http = http;
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RuleFormAdvancedOptions
enabledFlapping
alertDelay={5}
flappingSettings={{
lookBackWindow: 6,
statusChangeThreshold: 4,
}}
onAlertDelayChange={mockAlertDelayChange}
onFlappingChange={mockOnflappingChange}
/>
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByText('OFF')).toBeInTheDocument();
expect(screen.queryByText('Custom')).not.toBeInTheDocument();
expect(screen.queryByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeInTheDocument();
expect(screen.queryByTestId('ruleSettingsFlappingMessage')).not.toBeInTheDocument();
await userEvent.click(screen.getByTestId('ruleSettingsFlappingFormTooltipButton'));
expect(screen.getByTestId('ruleSettingsFlappingFormTooltipContent')).toBeInTheDocument();
});
test('should allow for flapping inputs to be modified', async () => {
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RuleFormAdvancedOptions
enabledFlapping
alertDelay={5}
flappingSettings={{
lookBackWindow: 10,
statusChangeThreshold: 10,
}}
onAlertDelayChange={mockAlertDelayChange}
onFlappingChange={mockOnflappingChange}
/>
</QueryClientProvider>
</IntlProvider>
);
expect(await screen.findByTestId('lookBackWindowRangeInput')).toBeInTheDocument();
const lookBackWindowInput = screen.getByTestId('lookBackWindowRangeInput');
const statusChangeThresholdInput = screen.getByTestId('statusChangeThresholdRangeInput');
// Change lookBackWindow to a smaller value
fireEvent.change(lookBackWindowInput, { target: { value: 5 } });
// statusChangeThresholdInput gets pinned to be 5
expect(mockOnflappingChange).toHaveBeenLastCalledWith({
lookBackWindow: 5,
statusChangeThreshold: 5,
});
// Try making statusChangeThreshold bigger
fireEvent.change(statusChangeThresholdInput, { target: { value: 20 } });
// Still pinned
expect(mockOnflappingChange).toHaveBeenLastCalledWith({
lookBackWindow: 10,
statusChangeThreshold: 10,
});
fireEvent.change(statusChangeThresholdInput, { target: { value: 3 } });
expect(mockOnflappingChange).toHaveBeenLastCalledWith({
lookBackWindow: 10,
statusChangeThreshold: 3,
});
});
test('should not render flapping if enableFlapping is false', () => {
render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<RuleFormAdvancedOptions
enabledFlapping={false}
alertDelay={5}
flappingSettings={{
lookBackWindow: 10,
statusChangeThreshold: 10,
}}
onAlertDelayChange={mockAlertDelayChange}
onFlappingChange={mockOnflappingChange}
/>
</QueryClientProvider>
</IntlProvider>
);
expect(screen.queryByTestId('alertFlappingFormRow')).not.toBeInTheDocument();
});
});

View file

@ -1,166 +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, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiIconTip,
EuiPanel,
EuiLoadingSpinner,
} from '@elastic/eui';
import { RuleSpecificFlappingProperties } from '@kbn/alerting-types/rule_settings';
import { RuleSettingsFlappingForm } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_form';
import { RuleSettingsFlappingTitleTooltip } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_title_tooltip';
import { useFetchFlappingSettings } from '@kbn/alerts-ui-shared/src/common/hooks/use_fetch_flapping_settings';
import { useKibana } from '../../../common/lib/kibana';
const alertDelayFormRowLabel = i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.alertDelayLabel',
{
defaultMessage: 'Alert delay',
}
);
const alertDelayIconTipDescription = i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp',
{
defaultMessage:
'An alert occurs only when the specified number of consecutive runs meet the rule conditions.',
}
);
const alertDelayPrependLabel = i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel',
{
defaultMessage: 'Alert after',
}
);
const alertDelayAppendLabel = i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel',
{
defaultMessage: 'consecutive matches',
}
);
const flappingFormRowLabel = i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.flappingLabel',
{
defaultMessage: 'Alert flapping detection',
}
);
const INTEGER_REGEX = /^[1-9][0-9]*$/;
export interface RuleFormAdvancedOptionsProps {
alertDelay?: number;
flappingSettings?: RuleSpecificFlappingProperties | null;
onAlertDelayChange: (value: string) => void;
onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void;
enabledFlapping?: boolean;
}
export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => {
const {
alertDelay,
flappingSettings,
enabledFlapping = true,
onAlertDelayChange,
onFlappingChange,
} = props;
const {
application: {
capabilities: { rulesSettings },
},
http,
} = useKibana().services;
const { writeFlappingSettingsUI } = rulesSettings || {};
const [isFlappingTitlePopoverOpen, setIsFlappingTitlePopoverOpen] = useState<boolean>(false);
const { data: spaceFlappingSettings, isInitialLoading } = useFetchFlappingSettings({
http,
enabled: enabledFlapping,
});
const internalOnAlertDelayChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value === '' || INTEGER_REGEX.test(value)) {
onAlertDelayChange(value);
}
},
[onAlertDelayChange]
);
return (
<EuiPanel color="subdued" hasShadow={false} data-test-subj="ruleFormAdvancedOptions">
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiFormRow
fullWidth
label={
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>{alertDelayFormRowLabel}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip content={alertDelayIconTipDescription} position="top" />
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="alertDelayFormRow"
display="rowCompressed"
>
<EuiFieldNumber
fullWidth
min={1}
value={alertDelay || ''}
name="alertDelay"
data-test-subj="alertDelayInput"
prepend={alertDelayPrependLabel}
append={alertDelayAppendLabel}
onChange={internalOnAlertDelayChange}
/>
</EuiFormRow>
</EuiFlexItem>
{isInitialLoading && <EuiLoadingSpinner />}
{spaceFlappingSettings && enabledFlapping && (
<EuiFlexItem grow={false}>
<EuiFormRow
fullWidth
label={
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>{flappingFormRowLabel}</EuiFlexItem>
<EuiFlexItem grow={false}>
<RuleSettingsFlappingTitleTooltip
isOpen={isFlappingTitlePopoverOpen}
setIsPopoverOpen={setIsFlappingTitlePopoverOpen}
/>
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="alertFlappingFormRow"
display="rowCompressed"
>
<RuleSettingsFlappingForm
flappingSettings={flappingSettings}
spaceFlappingSettings={spaceFlappingSettings}
canWriteFlappingSettingsUI={!!writeFlappingSettingsUI}
onFlappingChange={onFlappingChange}
/>
</EuiFormRow>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -1,222 +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 { fireEvent, render, screen } from '@testing-library/react';
import { RuleFormConsumerSelection } from './rule_form_consumer_selection';
import { RuleCreationValidConsumer } from '../../../types';
import { useKibana } from '../../../common/lib/kibana';
const mockConsumers: RuleCreationValidConsumer[] = ['logs', 'infrastructure', 'stackAlerts'];
const mockOnChange = jest.fn();
jest.mock('../../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('RuleFormConsumerSelectionModal', () => {
beforeEach(() => {
jest.clearAllMocks();
useKibanaMock().services.isServerless = false;
});
it('renders correctly', async () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={mockConsumers}
onChange={mockOnChange}
errors={{}}
/>
);
expect(screen.getByTestId('ruleFormConsumerSelect')).toBeInTheDocument();
expect(screen.getByTestId('comboBoxSearchInput')).toHaveAttribute(
'placeholder',
'Select a scope'
);
expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('');
fireEvent.click(screen.getByTestId('comboBoxToggleListButton'));
expect(screen.getByText('Logs')).toBeInTheDocument();
expect(screen.getByText('Metrics')).toBeInTheDocument();
expect(screen.getByText('Stack Rules')).toBeInTheDocument();
});
it('should be able to initialize to the prop initialSelectedConsumer', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={mockConsumers}
onChange={mockOnChange}
initialSelectedConsumer={'logs'}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('logs');
});
it('should NOT initialize if initialSelectedConsumer is equal to null', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={mockConsumers}
onChange={mockOnChange}
initialSelectedConsumer={null}
errors={{}}
/>
);
expect(mockOnChange).not.toBeCalled();
});
it('should initialize to the first valid consumers if initialSelectedConsumer is not valid', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={['logs', 'infrastructure']}
onChange={mockOnChange}
initialSelectedConsumer={'apm' as RuleCreationValidConsumer}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('logs');
});
it('should initialize to stackAlerts if the initialSelectedConsumer is not a valid and consumers has stackAlerts', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={['infrastructure', 'stackAlerts']}
onChange={mockOnChange}
initialSelectedConsumer={'logs'}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts');
});
it('should initialize to stackAlerts if the initialSelectedConsumer is undefined and consumers has stackAlerts', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={['infrastructure', 'stackAlerts']}
onChange={mockOnChange}
initialSelectedConsumer={undefined}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts');
});
it('should be able to select infrastructure and call onChange', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={mockConsumers}
onChange={mockOnChange}
errors={{}}
/>
);
fireEvent.click(screen.getByTestId('comboBoxToggleListButton'));
fireEvent.click(screen.getByTestId('infrastructure'));
expect(mockOnChange).toHaveBeenLastCalledWith('infrastructure');
});
it('should be able to select logs and call onChange', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={mockConsumers}
onChange={mockOnChange}
errors={{}}
/>
);
fireEvent.click(screen.getByTestId('comboBoxToggleListButton'));
fireEvent.click(screen.getByTestId('logs'));
expect(mockOnChange).toHaveBeenLastCalledWith('logs');
});
it('should be able to show errors when there is one', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={mockConsumers}
onChange={mockOnChange}
errors={{ consumer: ['Scope is required'] }}
/>
);
expect(screen.queryAllByText('Scope is required')).toHaveLength(1);
});
it('should display nothing if there is only 1 consumer to select', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={null}
consumers={['stackAlerts']}
onChange={mockOnChange}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts');
expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument();
});
it('should display the initial selected consumer', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={'logs'}
consumers={mockConsumers}
onChange={mockOnChange}
errors={{}}
/>
);
expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('Logs');
});
it('should not display the initial selected consumer if it is not a selectable option', () => {
render(
<RuleFormConsumerSelection
selectedConsumer={'logs'}
consumers={['stackAlerts', 'infrastructure']}
onChange={mockOnChange}
errors={{}}
/>
);
expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('');
});
it('should not show the role visibility dropdown on serverless on an o11y project', () => {
useKibanaMock().services.isServerless = true;
render(
<RuleFormConsumerSelection
selectedConsumer={'logs'}
consumers={['stackAlerts', 'infrastructure', 'observability']}
onChange={mockOnChange}
errors={{}}
/>
);
expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument();
});
it('should set the consumer correctly on an o11y project', () => {
useKibanaMock().services.isServerless = true;
render(
<RuleFormConsumerSelection
selectedConsumer={'logs'}
consumers={['stackAlerts', 'infrastructure', 'observability']}
onChange={mockOnChange}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('observability');
});
});

View file

@ -1,197 +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, { useMemo, useCallback, useEffect } from 'react';
import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AlertConsumers, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils';
import { IErrorObject, RuleCreationValidConsumer } from '../../../types';
import { useKibana } from '../../../common/lib/kibana';
const SELECT_LABEL: string = i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.selectLabel',
{
defaultMessage: 'Role visibility',
}
);
const featureNameMap: Record<string, string> = {
[AlertConsumers.LOGS]: i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.logs',
{
defaultMessage: 'Logs',
}
),
[AlertConsumers.INFRASTRUCTURE]: i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.infrastructure',
{
defaultMessage: 'Metrics',
}
),
[AlertConsumers.APM]: i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.apm',
{
defaultMessage: 'APM and User Experience',
}
),
[AlertConsumers.UPTIME]: i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.uptime',
{
defaultMessage: 'Synthetics and Uptime',
}
),
[AlertConsumers.SLO]: i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.slo',
{
defaultMessage: 'SLOs',
}
),
stackAlerts: i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.stackAlerts',
{
defaultMessage: 'Stack Rules',
}
),
};
export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [
AlertConsumers.LOGS,
AlertConsumers.INFRASTRUCTURE,
'stackAlerts',
'alerts',
];
export interface RuleFormConsumerSelectionProps {
consumers: RuleCreationValidConsumer[];
onChange: (consumer: RuleCreationValidConsumer | null) => void;
errors: IErrorObject;
selectedConsumer?: RuleCreationValidConsumer | null;
/* FUTURE ENGINEER
* if this prop is set to null then we wont initialize the value and the user will have to set it
* if this prop is set to a valid consumers then we will set it up to what was passed
* if this prop is not valid or undefined but the valid consumers has stackAlerts then we will default it to stackAlerts
*/
initialSelectedConsumer?: RuleCreationValidConsumer | null;
}
const SINGLE_SELECTION = { asPlainText: true };
export const RuleFormConsumerSelection = (props: RuleFormConsumerSelectionProps) => {
const { isServerless } = useKibana().services;
const { consumers, errors, onChange, selectedConsumer, initialSelectedConsumer } = props;
const isInvalid = (errors?.consumer as string[])?.length > 0;
const handleOnChange = useCallback(
(selected: Array<EuiComboBoxOptionOption<RuleCreationValidConsumer>>) => {
if (selected.length > 0) {
const newSelectedConsumer = selected[0];
onChange(newSelectedConsumer.value!);
} else {
onChange(null);
}
},
[onChange]
);
const validatedSelectedConsumer = useMemo(() => {
if (
selectedConsumer &&
consumers.includes(selectedConsumer) &&
featureNameMap[selectedConsumer]
) {
return selectedConsumer;
}
return null;
}, [selectedConsumer, consumers]);
const selectedOptions = useMemo(
() =>
validatedSelectedConsumer
? [{ value: validatedSelectedConsumer, label: featureNameMap[validatedSelectedConsumer] }]
: [],
[validatedSelectedConsumer]
);
const formattedSelectOptions: Array<EuiComboBoxOptionOption<RuleCreationValidConsumer>> =
useMemo(() => {
return consumers
.reduce<Array<EuiComboBoxOptionOption<RuleCreationValidConsumer>>>((result, consumer) => {
if (featureNameMap[consumer]) {
result.push({
value: consumer,
'data-test-subj': consumer,
label: featureNameMap[consumer],
});
}
return result;
}, [])
.sort((a, b) => {
return a.value!.localeCompare(b.value!);
});
}, [consumers]);
useEffect(() => {
// At initialization, select initialSelectedConsumer or the first value
if (!validatedSelectedConsumer) {
if (initialSelectedConsumer === null) {
return;
} else if (initialSelectedConsumer && consumers.includes(initialSelectedConsumer)) {
onChange(initialSelectedConsumer);
return;
} else if (consumers.includes(STACK_ALERTS_FEATURE_ID)) {
onChange(STACK_ALERTS_FEATURE_ID);
return;
}
onChange(consumers[0]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (consumers.length === 1) {
onChange(consumers[0] as RuleCreationValidConsumer);
} else if (isServerless && consumers.includes(AlertConsumers.OBSERVABILITY)) {
onChange(AlertConsumers.OBSERVABILITY as RuleCreationValidConsumer);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [consumers]);
if (consumers.length <= 1 || (isServerless && consumers.includes(AlertConsumers.OBSERVABILITY))) {
return null;
}
return (
<EuiFormRow
fullWidth
label={SELECT_LABEL}
isInvalid={isInvalid}
error={(errors?.consumer as string) ?? ''}
>
<EuiComboBox
data-test-subj="ruleFormConsumerSelect"
aria-label={i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.ariaLabel',
{
defaultMessage: 'Select a scope',
}
)}
placeholder={i18n.translate(
'xpack.triggersActionsUI.sections.ruleFormConsumerSelectionModal.comboBox.placeholder',
{
defaultMessage: 'Select a scope',
}
)}
fullWidth
singleSelection={SINGLE_SELECTION}
options={formattedSelectOptions}
selectedOptions={selectedOptions}
onChange={handleOnChange}
/>
</EuiFormRow>
);
};

View file

@ -1,144 +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 { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { Rule } from '../../../types';
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { RuleNotifyWhen } from './rule_notify_when';
describe('rule_notify_when', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const onNotifyWhenChange = jest.fn();
const onThrottleChange = jest.fn();
describe('action_frequency_form new rule', () => {
let wrapper: ReactWrapper<any>;
async function setup(overrides = {}) {
const initialRule = {
name: 'test',
params: {},
consumer: ALERTING_FEATURE_ID,
schedule: {
interval: '1m',
},
actions: [],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
notifyWhen: 'onActionGroupChange',
...overrides,
} as unknown as Rule;
wrapper = mountWithIntl(
<RuleNotifyWhen
rule={initialRule}
throttle={null}
throttleUnit="m"
onNotifyWhenChange={onNotifyWhenChange}
onThrottleChange={onThrottleChange}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
}
it(`should determine initial selection from throttle value if 'notifyWhen' is null`, async () => {
await setup({ notifyWhen: null });
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
expect(notifyWhenSelect.exists()).toBeTruthy();
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActiveAlert');
});
it(`should correctly select 'onActionGroupChange' option on initial render`, async () => {
await setup();
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
expect(notifyWhenSelect.exists()).toBeTruthy();
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActionGroupChange');
expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy();
});
it(`should correctly select 'onActiveAlert' option on initial render`, async () => {
await setup({
notifyWhen: 'onActiveAlert',
});
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
expect(notifyWhenSelect.exists()).toBeTruthy();
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onActiveAlert');
expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy();
});
it(`should correctly select 'onThrottleInterval' option on initial render and render throttle inputs`, async () => {
await setup({
notifyWhen: 'onThrottleInterval',
});
const notifyWhenSelect = wrapper.find('[data-test-subj="notifyWhenSelect"]');
expect(notifyWhenSelect.exists()).toBeTruthy();
expect(notifyWhenSelect.first().prop('valueOfSelected')).toEqual('onThrottleInterval');
const throttleInput = wrapper.find('[data-test-subj="throttleInput"]');
expect(throttleInput.exists()).toBeTruthy();
expect(throttleInput.at(1).prop('value')).toEqual(1);
const throttleUnitInput = wrapper.find('[data-test-subj="throttleUnitInput"]');
expect(throttleUnitInput.exists()).toBeTruthy();
expect(throttleUnitInput.at(1).prop('value')).toEqual('m');
});
it('should update action frequency type correctly', async () => {
await setup();
wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="onActiveAlert"]').simulate('click');
wrapper.update();
expect(onNotifyWhenChange).toHaveBeenCalledWith('onActiveAlert');
expect(onThrottleChange).toHaveBeenCalledWith(null, 'm');
wrapper.find('button[data-test-subj="notifyWhenSelect"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="onActionGroupChange"]').simulate('click');
wrapper.update();
expect(wrapper.find('[data-test-subj="throttleInput"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="throttleUnitInput"]').exists()).toBeFalsy();
expect(onNotifyWhenChange).toHaveBeenCalledWith('onActionGroupChange');
expect(onThrottleChange).toHaveBeenCalledWith(null, 'm');
});
it('should renders throttle input when custom throttle is selected and update throttle value', async () => {
await setup({
notifyWhen: 'onThrottleInterval',
});
const newThrottle = 17;
const throttleField = wrapper.find('[data-test-subj="throttleInput"]');
expect(throttleField.exists()).toBeTruthy();
throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } });
const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]');
expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle);
expect(onThrottleChange).toHaveBeenCalledWith(17, 'm');
const newThrottleUnit = 'h';
const throttleUnitField = wrapper.find('[data-test-subj="throttleUnitInput"]');
expect(throttleUnitField.exists()).toBeTruthy();
throttleUnitField.at(1).simulate('change', { target: { value: newThrottleUnit } });
expect(onThrottleChange).toHaveBeenCalledWith(null, 'h');
});
});
});

View file

@ -1,236 +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, { useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIconTip,
EuiFormRow,
EuiFieldNumber,
EuiSelect,
EuiText,
EuiSpacer,
EuiSuperSelect,
EuiSuperSelectOption,
} from '@elastic/eui';
import { some, filter, map } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
import { InitialRule } from './rule_reducer';
import { getTimeOptions } from '../../../common/lib/get_time_options';
import { RuleNotifyWhenType } from '../../../types';
const DEFAULT_NOTIFY_WHEN_VALUE: RuleNotifyWhenType = 'onActionGroupChange';
export const NOTIFY_WHEN_OPTIONS: Array<EuiSuperSelectOption<RuleNotifyWhenType>> = [
{
value: 'onActionGroupChange',
inputDisplay: i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.display',
{
defaultMessage: 'On status changes',
}
),
'data-test-subj': 'onActionGroupChange',
dropdownDisplay: (
<>
<strong>
<FormattedMessage
defaultMessage="On status changes"
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.label"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
defaultMessage="Actions run if the alert status changes."
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActionGroupChange.description"
/>
</p>
</EuiText>
</>
),
},
{
value: 'onActiveAlert',
inputDisplay: i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.display',
{
defaultMessage: 'On check intervals',
}
),
'data-test-subj': 'onActiveAlert',
dropdownDisplay: (
<>
<strong>
<FormattedMessage
defaultMessage="On check intervals"
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.label"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
defaultMessage="Actions run if rule conditions are met."
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onActiveAlert.description"
/>
</p>
</EuiText>
</>
),
},
{
value: 'onThrottleInterval',
inputDisplay: i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.display',
{
defaultMessage: 'On custom action intervals',
}
),
'data-test-subj': 'onThrottleInterval',
dropdownDisplay: (
<>
<strong>
<FormattedMessage
defaultMessage="On custom action intervals"
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.label"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
defaultMessage="Actions run if rule conditions are met."
id="xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.onThrottleInterval.description"
/>
</p>
</EuiText>
</>
),
},
];
interface RuleNotifyWhenProps {
rule: InitialRule;
throttle: number | null;
throttleUnit: string;
onNotifyWhenChange: (notifyWhen: RuleNotifyWhenType) => void;
onThrottleChange: (throttle: number | null, throttleUnit: string) => void;
}
export const RuleNotifyWhen = ({
rule,
throttle,
throttleUnit,
onNotifyWhenChange,
onThrottleChange,
}: RuleNotifyWhenProps) => {
const [ruleThrottle, setRuleThrottle] = useState<number>(throttle || 1);
const [showCustomThrottleOpts, setShowCustomThrottleOpts] = useState<boolean>(false);
const [notifyWhenValue, setNotifyWhenValue] =
useState<RuleNotifyWhenType>(DEFAULT_NOTIFY_WHEN_VALUE);
useEffect(() => {
if (rule.notifyWhen) {
setNotifyWhenValue(rule.notifyWhen);
} else {
// If 'notifyWhen' is not set, derive value from existence of throttle value
setNotifyWhenValue(rule.throttle ? 'onThrottleInterval' : 'onActiveAlert');
}
}, [rule]);
useEffect(() => {
setShowCustomThrottleOpts(notifyWhenValue === 'onThrottleInterval');
}, [notifyWhenValue]);
const onNotifyWhenValueChange = useCallback((newValue: RuleNotifyWhenType) => {
onThrottleChange(newValue === 'onThrottleInterval' ? ruleThrottle : null, throttleUnit);
onNotifyWhenChange(newValue);
setNotifyWhenValue(newValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const labelForRuleRenotify = (
<>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleForm.renotifyFieldLabel"
defaultMessage="Notify"
/>{' '}
<EuiIconTip
position="right"
type="questionInCircle"
content={i18n.translate('xpack.triggersActionsUI.sections.ruleForm.renotifyWithTooltip', {
defaultMessage: 'Define how often alerts generate actions.',
})}
/>
</>
);
return (
<>
<EuiFormRow fullWidth label={labelForRuleRenotify}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiSuperSelect
data-test-subj="notifyWhenSelect"
options={NOTIFY_WHEN_OPTIONS}
valueOfSelected={notifyWhenValue}
onChange={onNotifyWhenValueChange}
/>
{showCustomThrottleOpts && (
<>
<EuiSpacer />
<EuiFormRow fullWidth>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
<EuiFieldNumber
fullWidth
min={1}
value={ruleThrottle}
name="throttle"
data-test-subj="throttleInput"
prepend={i18n.translate(
'xpack.triggersActionsUI.sections.ruleForm.ruleNotifyWhen.label',
{
defaultMessage: 'Every',
}
)}
onChange={(e) => {
pipe(
some(e.target.value.trim()),
filter((value) => value !== ''),
map((value) => parseInt(value, 10)),
filter((value) => !isNaN(value)),
map((value) => {
setRuleThrottle(value);
onThrottleChange(value, throttleUnit);
})
);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSelect
data-test-subj="throttleUnitInput"
value={throttleUnit}
options={getTimeOptions(throttle ?? 1)}
onChange={(e) => {
onThrottleChange(throttle, e.target.value);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</>
);
};

View file

@ -1,277 +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 { getRuleReducer } from './rule_reducer';
import type { ActionTypeModel, Rule } from '../../../types';
import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common';
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
const actionTypeRegistry = actionTypeRegistryMock.create();
const actionType = {
id: 'test',
name: 'Test',
isSystemActionType: false,
} as unknown as ActionTypeModel;
actionTypeRegistry.get.mockReturnValue(actionType);
describe('rule reducer', () => {
const ruleReducer = getRuleReducer(actionTypeRegistry);
let initialRule: Rule;
beforeAll(() => {
initialRule = {
params: {},
consumer: 'rules',
ruleTypeId: null,
schedule: {
interval: '1m',
},
actions: [],
tags: [],
notifyWhen: 'onActionGroupChange',
alertDelay: {
active: 5,
},
} as unknown as Rule;
});
// setRule
test('if modified rule was reset to initial', () => {
const rule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setProperty' },
payload: {
key: 'name',
value: 'new name',
},
}
);
expect(rule.rule.name).toBe('new name');
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRule' },
payload: {
key: 'rule',
value: initialRule,
},
}
);
expect(updatedRule.rule.name).toBeUndefined();
});
test('if property name was changed', () => {
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setProperty' },
payload: {
key: 'name',
value: 'new name',
},
}
);
expect(updatedRule.rule.name).toBe('new name');
});
test('if initial schedule property was updated', () => {
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setScheduleProperty' },
payload: {
key: 'interval',
value: '10s',
},
}
);
expect(updatedRule.rule.schedule.interval).toBe('10s');
});
test('if rule params property was added and updated', () => {
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleParams' },
payload: {
key: 'testParam',
value: 'new test params property',
},
}
);
expect(updatedRule.rule.params.testParam).toBe('new test params property');
const updatedRuleParamsProperty = ruleReducer(
{ rule: updatedRule.rule },
{
command: { type: 'setRuleParams' },
payload: {
key: 'testParam',
value: 'test params property updated',
},
}
);
expect(updatedRuleParamsProperty.rule.params.testParam).toBe('test params property updated');
});
test('if rule action params property was added and updated', () => {
initialRule.actions.push({
id: '',
actionTypeId: 'testId',
group: 'Rule',
params: {},
uuid: '123-456',
});
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleActionParams' },
payload: {
key: 'testActionParam',
value: 'new test action params property',
index: 0,
},
}
);
expect(updatedRule.rule.actions[0].params.testActionParam).toBe(
'new test action params property'
);
const updatedRuleActionParamsProperty = ruleReducer(
{ rule: updatedRule.rule },
{
command: { type: 'setRuleActionParams' },
payload: {
key: 'testActionParam',
value: 'test action params property updated',
index: 0,
},
}
);
expect(updatedRuleActionParamsProperty.rule.actions[0].params.testActionParam).toBe(
'test action params property updated'
);
});
test('if the existing rule action params property was set to undefined (when other connector was selected)', () => {
initialRule.actions.push({
id: '',
actionTypeId: 'testId',
group: 'Rule',
params: {
testActionParam: 'some value',
},
uuid: '123-456',
});
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleActionParams' },
payload: {
key: 'testActionParam',
value: undefined,
index: 0,
},
}
);
expect(updatedRule.rule.actions[0].params.testActionParam).toBe(undefined);
});
test('if rule action property was updated', () => {
initialRule.actions.push({
id: '',
actionTypeId: 'testId',
group: 'Rule',
params: {},
uuid: '123-456',
});
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleActionProperty' },
payload: {
key: 'group',
value: 'Warning',
index: 0,
},
}
);
expect((updatedRule.rule.actions[0] as SanitizedRuleAction).group).toBe('Warning');
});
test('if rule action frequency was updated', () => {
initialRule.actions.push({
id: '',
actionTypeId: 'testId',
group: 'Rule',
params: {},
uuid: '123-456',
});
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleActionFrequency' },
payload: {
key: 'notifyWhen',
value: 'onThrottleInterval',
index: 0,
},
}
);
expect((updatedRule.rule.actions[0] as SanitizedRuleAction).frequency?.notifyWhen).toBe(
'onThrottleInterval'
);
});
test('if initial alert delay property was updated', () => {
const updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setAlertDelayProperty' },
payload: {
key: 'active',
value: 10,
},
}
);
expect(updatedRule.rule.alertDelay?.active).toBe(10);
});
test('if rule action alerts filter was toggled on, then off', () => {
initialRule.actions.push({
id: '',
actionTypeId: 'testId',
group: 'Rule',
params: {},
uuid: '123-456',
});
let updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleActionAlertsFilter' },
payload: {
key: 'query',
value: 'hello',
index: 0,
},
}
);
expect((updatedRule.rule.actions[0] as SanitizedRuleAction).alertsFilter).toBeDefined();
updatedRule = ruleReducer(
{ rule: initialRule },
{
command: { type: 'setRuleActionAlertsFilter' },
payload: {
key: 'query',
value: undefined,
index: 0,
},
}
);
expect((updatedRule.rule.actions[0] as SanitizedRuleAction).alertsFilter).toBeUndefined();
});
});

View file

@ -1,319 +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 type { SavedObjectAttribute } from '@kbn/core/public';
import { isEqual, isUndefined, omitBy } from 'lodash';
import type { Reducer } from 'react';
import type {
RuleActionParam,
IntervalSchedule,
RuleActionAlertsFilterProperty,
AlertsFilter,
AlertDelay,
SanitizedRuleAction,
} from '@kbn/alerting-plugin/common';
import { isEmpty } from 'lodash/fp';
import type { ActionTypeRegistryContract, Rule, RuleUiAction } from '../../../types';
import { DEFAULT_FREQUENCY } from '../../../common/constants';
export type InitialRule = Partial<Rule> &
Pick<Rule, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags'>;
interface CommandType<
T extends
| 'setRule'
| 'setProperty'
| 'setScheduleProperty'
| 'setRuleParams'
| 'setRuleActionParams'
| 'setRuleActionProperty'
| 'setRuleActionFrequency'
| 'setRuleActionAlertsFilter'
| 'setAlertDelayProperty'
> {
type: T;
}
export interface RuleState {
rule: InitialRule;
}
interface Payload<Keys, Value> {
key: Keys;
value: Value;
index?: number;
}
interface RulePayload<Key extends keyof Rule> {
key: Key;
value: Rule[Key] | null;
index?: number;
}
interface RuleActionPayload<Key extends keyof RuleUiAction> {
key: Key;
value: RuleUiAction[Key] | null;
index?: number;
}
interface RuleSchedulePayload<Key extends keyof IntervalSchedule> {
key: Key;
value: IntervalSchedule[Key];
index?: number;
}
interface AlertDelayPayload<Key extends keyof AlertDelay> {
key: Key;
value: AlertDelay[Key] | null;
index?: number;
}
export type RuleReducerAction =
| {
command: CommandType<'setRule'>;
payload: Payload<'rule', InitialRule>;
}
| {
command: CommandType<'setProperty'>;
payload: RulePayload<keyof Rule>;
}
| {
command: CommandType<'setScheduleProperty'>;
payload: RuleSchedulePayload<keyof IntervalSchedule>;
}
| {
command: CommandType<'setRuleParams'>;
payload: Payload<string, unknown>;
}
| {
command: CommandType<'setRuleActionParams'>;
payload: Payload<string, RuleActionParam>;
}
| {
command: CommandType<'setRuleActionProperty'>;
payload: Payload<string, RuleActionParam>;
}
| {
command: CommandType<'setRuleActionFrequency'>;
payload: Payload<string, RuleActionParam>;
}
| {
command: CommandType<'setRuleActionAlertsFilter'>;
payload: Payload<string, RuleActionAlertsFilterProperty>;
}
| {
command: CommandType<'setAlertDelayProperty'>;
payload: AlertDelayPayload<keyof AlertDelay>;
};
export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>;
export type ConcreteRuleReducer = Reducer<{ rule: Rule }, RuleReducerAction>;
export const getRuleReducer =
<RulePhase extends InitialRule | Rule>(actionTypeRegistry: ActionTypeRegistryContract) =>
(state: { rule: RulePhase }, action: RuleReducerAction) => {
const { rule } = state;
switch (action.command.type) {
case 'setRule': {
const { key, value } = action.payload as Payload<'rule', RulePhase>;
if (key === 'rule') {
return {
...state,
rule: value,
};
} else {
return state;
}
}
case 'setProperty': {
const { key, value } = action.payload as RulePayload<keyof Rule>;
if (isEqual(rule[key], value)) {
return state;
} else {
return {
...state,
rule: {
...rule,
[key]: value,
},
};
}
}
case 'setScheduleProperty': {
const { key, value } = action.payload as RuleSchedulePayload<keyof IntervalSchedule>;
if (rule.schedule && isEqual(rule.schedule[key], value)) {
return state;
} else {
return {
...state,
rule: {
...rule,
schedule: {
...rule.schedule,
[key]: value,
},
},
};
}
}
case 'setRuleParams': {
const { key, value } = action.payload as Payload<string, Record<string, unknown>>;
if (isEqual(rule.params[key], value)) {
return state;
} else {
return {
...state,
rule: {
...rule,
params: {
...rule.params,
[key]: value,
},
},
};
}
}
case 'setRuleActionParams': {
const { key, value, index } = action.payload as Payload<
keyof RuleUiAction,
SavedObjectAttribute
>;
if (
index === undefined ||
rule.actions[index] == null ||
(!!rule.actions[index][key] && isEqual(rule.actions[index][key], value))
) {
return state;
} else {
const oldAction = rule.actions.splice(index, 1)[0];
const updatedAction = {
...oldAction,
params: {
...oldAction.params,
[key]: value,
},
};
rule.actions.splice(index, 0, updatedAction);
return {
...state,
rule: {
...rule,
actions: [...rule.actions],
},
};
}
}
case 'setRuleActionFrequency': {
const { key, value, index } = action.payload as Payload<
keyof RuleUiAction,
SavedObjectAttribute
>;
if (
index === undefined ||
rule.actions[index] == null ||
(!!rule.actions[index][key] && isEqual(rule.actions[index][key], value))
) {
return state;
} else {
const oldAction = rule.actions.splice(index, 1)[0];
if (actionTypeRegistry.get(oldAction.actionTypeId).isSystemActionType) {
return state;
}
const oldSanitizedAction = oldAction as SanitizedRuleAction;
const updatedAction = {
...oldSanitizedAction,
frequency: {
...(oldSanitizedAction?.frequency ?? DEFAULT_FREQUENCY),
[key]: value,
},
};
rule.actions.splice(index, 0, updatedAction);
return {
...state,
rule: {
...rule,
actions: [...rule.actions],
},
};
}
}
case 'setRuleActionAlertsFilter': {
const { key, value, index } = action.payload as Payload<
keyof AlertsFilter,
RuleActionAlertsFilterProperty
>;
if (index === undefined || rule.actions[index] == null) {
return state;
} else {
const oldAction = rule.actions.splice(index, 1)[0];
if (actionTypeRegistry.get(oldAction.actionTypeId).isSystemActionType) {
return state;
}
const oldSanitizedAction = oldAction as SanitizedRuleAction;
if (
oldSanitizedAction.alertsFilter &&
isEqual(oldSanitizedAction.alertsFilter[key], value)
)
return state;
const { alertsFilter, ...rest } = oldSanitizedAction;
const updatedAlertsFilter = omitBy({ ...alertsFilter, [key]: value }, isUndefined);
const updatedAction = {
...rest,
...(!isEmpty(updatedAlertsFilter) ? { alertsFilter: updatedAlertsFilter } : {}),
};
rule.actions.splice(index, 0, updatedAction);
return {
...state,
rule: {
...rule,
actions: [...rule.actions],
},
};
}
}
case 'setRuleActionProperty': {
const { key, value, index } = action.payload as RuleActionPayload<keyof RuleUiAction>;
if (index === undefined || isEqual(rule.actions[index][key], value)) {
return state;
} else {
const oldAction = rule.actions.splice(index, 1)[0];
const updatedAction = {
...oldAction,
[key]: value,
};
rule.actions.splice(index, 0, updatedAction);
return {
...state,
rule: {
...rule,
actions: [...rule.actions],
},
};
}
}
case 'setAlertDelayProperty': {
const { key, value } = action.payload as Payload<keyof AlertDelay, SavedObjectAttribute>;
if (rule.alertDelay && isEqual(rule.alertDelay[key], value)) {
return state;
} else {
return {
...state,
rule: {
...rule,
alertDelay: {
...rule.alertDelay,
[key]: value,
},
},
};
}
}
}
};

View file

@ -1,242 +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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render, cleanup } from '@testing-library/react';
import { ShowRequestModal, ShowRequestModalProps } from './show_request_modal';
import { Rule, RuleTypeParams, RuleUpdates } from '../../../types';
import { InitialRule } from './rule_reducer';
const testDate = new Date('2024-04-04T19:34:24.902Z');
const shared = {
params: {
searchType: 'esQuery',
timeWindowSize: 5,
timeWindowUnit: 'm',
threshold: [1000],
thresholdComparator: '>',
size: 100,
esQuery: '{\n "query":{\n "match_all" : {}\n }\n }',
aggType: 'count',
groupBy: 'all',
termSize: 5,
excludeHitsFromPreviousRun: false,
sourceFields: [],
index: ['.kibana'],
timeField: 'created_at',
},
consumer: 'stackAlerts',
ruleTypeId: '.es-query',
schedule: { interval: '1m' },
actions: [
{
id: '0be65bf4-58b8-4c44-ba4d-5112c65103f5',
actionTypeId: '.server-log',
group: 'query matched',
params: {
level: 'info',
message:
"Elasticsearch query rule '{{rule.name}}' is active:\n\n- Value: {{context.value}}\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\n- Timestamp: {{context.date}}\n- Link: {{context.link}}",
},
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
uuid: 'a330a154-61fb-42a8-9bce-9dfd8513a12d',
},
],
tags: ['test'],
name: 'test',
};
const rule: Rule<RuleTypeParams> | InitialRule = { ...shared };
const editRule: Rule<RuleTypeParams> = {
createdBy: 'elastic',
updatedBy: 'elastic',
createdAt: testDate,
updatedAt: testDate,
apiKeyOwner: 'elastic',
muteAll: false,
mutedInstanceIds: [],
snoozeSchedule: [],
executionStatus: {
lastExecutionDate: testDate,
lastDuration: 46,
status: 'ok',
},
scheduledTaskId: '0de7273e-c5db-4d5c-8e28-1aab363e1abc',
lastRun: {
outcomeMsg: null,
outcomeOrder: 0,
alertsCount: { active: 0, new: 0, recovered: 0, ignored: 0 },
outcome: 'succeeded',
warning: null,
},
nextRun: testDate,
apiKeyCreatedByUser: false,
id: '0de7273e-c5db-4d5c-8e28-1aab363e1abc',
enabled: true,
revision: 0,
running: false,
monitoring: {
run: {
history: [{ success: true, timestamp: 1712259266100, duration: 65 }],
calculated_metrics: { success_ratio: 1, p50: 45, p95: 64.65, p99: 968 },
last_run: {
timestamp: '2024-04-04T20:39:01.655Z',
metrics: {
duration: 46,
total_search_duration_ms: null,
total_indexing_duration_ms: null,
total_alerts_detected: null,
total_alerts_created: null,
gap_duration_s: null,
},
},
},
},
...shared,
};
const ShowRequestModalWithProviders: React.FunctionComponent<ShowRequestModalProps> = (props) => (
<IntlProvider locale="en">
<ShowRequestModal {...props} />
</IntlProvider>
);
describe('showRequestModal', () => {
afterEach(() => {
jest.clearAllMocks();
cleanup();
});
test('renders create request correctly', async () => {
const modalProps: ShowRequestModalProps = {
rule: {
...rule,
} as RuleUpdates,
onClose: jest.fn(),
};
const result = render(<ShowRequestModalWithProviders {...modalProps} />);
expect(result.getByTestId('modalHeaderTitle').textContent).toBe('Create alerting rule request');
expect(result.getByTestId('modalSubtitle').textContent).toBe(
'This Kibana request will create this rule.'
);
expect(result.getByTestId('modalRequestCodeBlock').textContent).toMatchInlineSnapshot(`
"POST kbn:/api/alerting/rule
{
\\"params\\": {
\\"searchType\\": \\"esQuery\\",
\\"timeWindowSize\\": 5,
\\"timeWindowUnit\\": \\"m\\",
\\"threshold\\": [
1000
],
\\"thresholdComparator\\": \\">\\",
\\"size\\": 100,
\\"esQuery\\": \\"{\\\\n \\\\\\"query\\\\\\":{\\\\n \\\\\\"match_all\\\\\\" : {}\\\\n }\\\\n }\\",
\\"aggType\\": \\"count\\",
\\"groupBy\\": \\"all\\",
\\"termSize\\": 5,
\\"excludeHitsFromPreviousRun\\": false,
\\"sourceFields\\": [],
\\"index\\": [
\\".kibana\\"
],
\\"timeField\\": \\"created_at\\"
},
\\"consumer\\": \\"stackAlerts\\",
\\"schedule\\": {
\\"interval\\": \\"1m\\"
},
\\"tags\\": [
\\"test\\"
],
\\"name\\": \\"test\\",
\\"rule_type_id\\": \\".es-query\\",
\\"actions\\": [
{
\\"group\\": \\"query matched\\",
\\"id\\": \\"0be65bf4-58b8-4c44-ba4d-5112c65103f5\\",
\\"params\\": {
\\"level\\": \\"info\\",
\\"message\\": \\"Elasticsearch query rule '{{rule.name}}' is active:\\\\n\\\\n- Value: {{context.value}}\\\\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\\\\n- Timestamp: {{context.date}}\\\\n- Link: {{context.link}}\\"
},
\\"frequency\\": {
\\"notify_when\\": \\"onActionGroupChange\\",
\\"throttle\\": null,
\\"summary\\": false
}
}
]
}"
`);
});
test('renders edit request correctly', async () => {
const modalProps: ShowRequestModalProps = {
edit: true,
ruleId: editRule.id,
rule: {
...editRule,
} as RuleUpdates,
onClose: jest.fn(),
};
const result = render(<ShowRequestModalWithProviders {...modalProps} />);
expect(result.getByTestId('modalHeaderTitle').textContent).toBe('Edit alerting rule request');
expect(result.getByTestId('modalSubtitle').textContent).toBe(
'This Kibana request will edit this rule.'
);
expect(result.getByTestId('modalRequestCodeBlock').textContent).toMatchInlineSnapshot(`
"PUT kbn:/api/alerting/rule/0de7273e-c5db-4d5c-8e28-1aab363e1abc
{
\\"name\\": \\"test\\",
\\"tags\\": [
\\"test\\"
],
\\"schedule\\": {
\\"interval\\": \\"1m\\"
},
\\"params\\": {
\\"searchType\\": \\"esQuery\\",
\\"timeWindowSize\\": 5,
\\"timeWindowUnit\\": \\"m\\",
\\"threshold\\": [
1000
],
\\"thresholdComparator\\": \\">\\",
\\"size\\": 100,
\\"esQuery\\": \\"{\\\\n \\\\\\"query\\\\\\":{\\\\n \\\\\\"match_all\\\\\\" : {}\\\\n }\\\\n }\\",
\\"aggType\\": \\"count\\",
\\"groupBy\\": \\"all\\",
\\"termSize\\": 5,
\\"excludeHitsFromPreviousRun\\": false,
\\"sourceFields\\": [],
\\"index\\": [
\\".kibana\\"
],
\\"timeField\\": \\"created_at\\"
},
\\"actions\\": [
{
\\"group\\": \\"query matched\\",
\\"id\\": \\"0be65bf4-58b8-4c44-ba4d-5112c65103f5\\",
\\"params\\": {
\\"level\\": \\"info\\",
\\"message\\": \\"Elasticsearch query rule '{{rule.name}}' is active:\\\\n\\\\n- Value: {{context.value}}\\\\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\\\\n- Timestamp: {{context.date}}\\\\n- Link: {{context.link}}\\"
},
\\"frequency\\": {
\\"notify_when\\": \\"onActionGroupChange\\",
\\"throttle\\": null,
\\"summary\\": false
},
\\"uuid\\": \\"a330a154-61fb-42a8-9bce-9dfd8513a12d\\"
}
]
}"
`);
});
});

View file

@ -1,85 +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 {
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiText,
EuiTextColor,
} from '@elastic/eui';
import {
UPDATE_FIELDS_WITH_ACTIONS,
transformCreateRuleBody as rewriteCreateBodyRequest,
transformUpdateRuleBody as rewriteUpdateBodyRequest,
} from '@kbn/response-ops-rule-form';
import { pick } from 'lodash';
import React from 'react';
import { RuleUpdates } from '../../../types';
import { BASE_ALERTING_API_PATH } from '../../constants';
import * as i18n from '../translations';
const stringify = (rule: RuleUpdates, edit: boolean): string => {
try {
const request = edit
? rewriteUpdateBodyRequest(pick(rule, UPDATE_FIELDS_WITH_ACTIONS))
: rewriteCreateBodyRequest(rule);
return JSON.stringify(request, null, 2);
} catch {
return i18n.SHOW_REQUEST_MODAL_ERROR;
}
};
export interface ShowRequestModalProps {
onClose: () => void;
rule: RuleUpdates;
ruleId?: string;
edit?: boolean;
}
export const ShowRequestModal: React.FC<ShowRequestModalProps> = ({
onClose,
rule,
edit = false,
ruleId,
}) => {
const formattedRequest = stringify(rule, edit);
return (
<EuiModal aria-labelledby="showRequestModal" onClose={onClose}>
<EuiModalHeader>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiModalHeaderTitle id="showRequestModal" data-test-subj="modalHeaderTitle">
{i18n.SHOW_REQUEST_MODAL_TITLE(edit)}
</EuiModalHeaderTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText data-test-subj="modalSubtitle">
<p>
<EuiTextColor color="subdued">
{i18n.SHOW_REQUEST_MODAL_SUBTITLE(edit)}
</EuiTextColor>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody>
<EuiCodeBlock language="json" isCopyable data-test-subj="modalRequestCodeBlock">
{`${edit ? 'PUT' : 'POST'} kbn:${BASE_ALERTING_API_PATH}/rule${
edit ? `/${ruleId}` : ''
}\n${formattedRequest}`}
</EuiCodeBlock>
</EuiModalBody>
</EuiModal>
);
};

View file

@ -1,84 +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, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFilterGroup,
EuiPopover,
EuiFilterButton,
EuiFilterSelectItem,
useEuiTheme,
} from '@elastic/eui';
interface SolutionFilterProps {
solutions: Map<string, string>;
onChange?: (selectedSolutions: string[]) => void;
}
export const SolutionFilter: React.FunctionComponent<SolutionFilterProps> = ({
solutions,
onChange,
}: SolutionFilterProps) => {
const { euiTheme } = useEuiTheme();
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
useEffect(() => {
if (onChange) {
onChange(selectedValues);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedValues]);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
button={
<EuiFilterButton
iconType="arrowDown"
hasActiveFilters={selectedValues.length > 0}
numActiveFilters={selectedValues.length}
numFilters={selectedValues.length}
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
data-test-subj="solutionsFilterButton"
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.ruleForm.solutionFilterLabel"
defaultMessage="Filter by use case"
/>
</EuiFilterButton>
}
>
{/* EUI NOTE: Please use EuiSelectable (which already has height/scrolling built in)
instead of EuiFilterSelectItem (which is pending deprecation).
@see https://elastic.github.io/eui/#/forms/filter-group#multi-select */}
<div className="eui-yScroll" css={{ maxHeight: euiTheme.base * 30 }}>
{[...solutions.entries()].map(([id, title]) => (
<EuiFilterSelectItem
key={id}
onClick={() => {
const isPreviouslyChecked = selectedValues.includes(id);
if (isPreviouslyChecked) {
setSelectedValues(selectedValues.filter((val) => val !== id));
} else {
setSelectedValues([...selectedValues, id]);
}
}}
checked={selectedValues.includes(id) ? 'on' : undefined}
data-test-subj={`solution${id}FilterOption`}
>
{title}
</EuiFilterSelectItem>
))}
</div>
</EuiPopover>
</EuiFilterGroup>
);
};

View file

@ -14,16 +14,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common';
import { RuleTypeModal } from '@kbn/response-ops-rule-form';
import React, {
lazy,
useEffect,
useState,
ReactNode,
useCallback,
useMemo,
useRef,
Suspense,
} from 'react';
import React, { useEffect, useState, ReactNode, useCallback, useMemo, useRef } from 'react';
import {
EuiSpacer,
EuiPageTemplate,
@ -37,14 +28,12 @@ import { useHistory } from 'react-router-dom';
import {
RuleExecutionStatus,
ALERTING_FEATURE_ID,
RuleExecutionStatusErrorReasons,
RuleLastRunOutcomeValues,
} from '@kbn/alerting-plugin/common';
import {
RuleCreationValidConsumer,
ruleDetailsRoute as commonRuleDetailsRoute,
STACK_ALERTS_FEATURE_ID,
getCreateRuleRoute,
getEditRuleRoute,
} from '@kbn/rule-data-utils';
@ -111,11 +100,6 @@ import { RulesSettingsLink } from '../../../components/rules_setting/rules_setti
import { useRulesListUiState as useUiState } from '../../../hooks/use_rules_list_ui_state';
import { useRulesListFilterStore } from './hooks/use_rules_list_filter_store';
// Directly lazy import the flyouts because the suspendedComponentWithProps component
// cause a visual hitch due to the loading spinner
const RuleAdd = lazy(() => import('../../rule_form/rule_add'));
const RuleEdit = lazy(() => import('../../rule_form/rule_edit'));
export interface RulesListProps {
ruleTypeIds?: string[];
consumers?: string[];
@ -142,7 +126,6 @@ export interface RulesListProps {
onRefresh?: (refresh: Date) => void;
setHeaderActions?: (components?: React.ReactNode[]) => void;
initialSelectedConsumer?: RuleCreationValidConsumer | null;
useNewRuleForm?: boolean;
}
export const percentileFields = {
@ -184,8 +167,6 @@ export const RulesList = ({
onTypeFilterChange,
onRefresh,
setHeaderActions,
initialSelectedConsumer = STACK_ALERTS_FEATURE_ID,
useNewRuleForm = false,
}: RulesListProps) => {
const history = useHistory();
const kibanaServices = useKibana().services;
@ -205,10 +186,6 @@ export const RulesList = ({
const [inputText, setInputText] = useState<string>(searchFilter);
const [ruleTypeModalVisible, setRuleTypeModalVisibility] = useState<boolean>(false);
const [ruleTypeIdToCreate, setRuleTypeIdToCreate] = useState<string | undefined>(undefined);
const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const [currentRuleToEdit, setCurrentRuleToEdit] = useState<RuleTableItem | null>(null);
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, ReactNode>>(
{}
);
@ -216,8 +193,6 @@ export const RulesList = ({
const cloneRuleId = useRef<null | string>(null);
const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter');
// TODO: Remove this when removing the v1 flyout code
const isUsingRuleCreateFlyout = false; // getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
const [percentileOptions, setPercentileOptions] =
useState<EuiSelectableOption[]>(initialPercentileOptions);
@ -322,18 +297,13 @@ export const RulesList = ({
});
const onRuleEdit = (ruleItem: RuleTableItem) => {
if (!isUsingRuleCreateFlyout && useNewRuleForm) {
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(ruleItem.id)}`,
state: {
returnApp: 'management',
returnPath: `insightsAndAlerting/triggersActions/rules`,
},
});
} else {
setEditFlyoutVisibility(true);
setCurrentRuleToEdit(ruleItem);
}
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getEditRuleRoute(ruleItem.id)}`,
state: {
returnApp: 'management',
returnPath: `insightsAndAlerting/triggersActions/rules`,
},
});
};
const onRunRule = async (id: string) => {
@ -1030,15 +1000,9 @@ export const RulesList = ({
<RuleTypeModal
onClose={() => setRuleTypeModalVisibility(false)}
onSelectRuleType={(ruleTypeId) => {
if (!isUsingRuleCreateFlyout) {
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getCreateRuleRoute(ruleTypeId)}`,
});
} else {
setRuleTypeIdToCreate(ruleTypeId);
setRuleTypeModalVisibility(false);
setRuleFlyoutVisibility(true);
}
navigateToApp('management', {
path: `insightsAndAlerting/triggersActions/${getCreateRuleRoute(ruleTypeId)}`,
});
}}
http={http}
toasts={toasts}
@ -1046,39 +1010,6 @@ export const RulesList = ({
filteredRuleTypes={filteredRuleTypes}
/>
)}
{ruleFlyoutVisible && (
<Suspense fallback={<div />}>
<RuleAdd
consumer={ALERTING_FEATURE_ID}
onClose={() => {
setRuleFlyoutVisibility(false);
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
ruleTypeIndex={ruleTypesState.data}
onSave={refreshRules}
initialSelectedConsumer={initialSelectedConsumer}
ruleTypeId={ruleTypeIdToCreate}
canChangeTrigger={false}
/>
</Suspense>
)}
{editFlyoutVisible && currentRuleToEdit && (
<Suspense fallback={<div />}>
<RuleEdit
initialRule={currentRuleToEdit}
onClose={() => {
setEditFlyoutVisibility(false);
}}
actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry}
ruleType={
ruleTypesState.data.get(currentRuleToEdit.ruleTypeId) as RuleType<string, string>
}
onSave={refreshRules}
/>
</Suspense>
)}
</EuiPageTemplate.Section>
</>
);

View file

@ -24,7 +24,6 @@ describe('getIsExperimentalFeatureEnabled', () => {
ruleKqlBar: true,
isMustacheAutocompleteOn: false,
showMustacheAutocompleteSwitch: false,
isUsingRuleCreateFlyout: false,
},
});
@ -64,10 +63,6 @@ describe('getIsExperimentalFeatureEnabled', () => {
expect(result).toEqual(false);
result = getIsExperimentalFeatureEnabled('isUsingRuleCreateFlyout');
expect(result).toEqual(false);
expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError(
`Invalid enable value doesNotExist. Allowed values are: ${allowedExperimentalValueKeys.join(
', '

View file

@ -86,10 +86,6 @@ export {
export { AlertProvidedActionVariables } from '@kbn/alerts-ui-shared';
export type { ActionGroupWithCondition } from './application/sections';
export { AlertConditions, AlertConditionsGroup } from './application/sections';
export function plugin(context: PluginInitializerContext) {
return new Plugin(context);
}
@ -137,11 +133,6 @@ export { getTimeUnitLabel } from './common/lib/get_time_unit_label';
export type { TriggersAndActionsUiServices } from './application/rules_app';
export type { BulkOperationAttributes, BulkOperationResponse } from './types';
export const getNotifyWhenOptions = async () => {
const { NOTIFY_WHEN_OPTIONS } = await import('./application/sections/rule_form/rule_notify_when');
return NOTIFY_WHEN_OPTIONS;
};
export { transformRule } from './application/lib/rule_api/common_transformations';
export { validateActionFilterQuery } from './application/lib/value_validators';

View file

@ -353,7 +353,6 @@ export interface RuleDefinitionProps<Params extends RuleTypeParams = RuleTypePar
onEditRule: () => Promise<void>;
hideEditButton?: boolean;
filteredRuleTypes?: string[];
useNewRuleForm?: boolean;
}
export enum Percentiles {

View file

@ -69,7 +69,6 @@
"@kbn/visualization-utils",
"@kbn/core-ui-settings-browser",
"@kbn/alerting-rule-utils",
"@kbn/core-application-browser",
"@kbn/cloud-plugin",
"@kbn/response-ops-alerts-apis",
"@kbn/response-ops-alerts-table",

View file

@ -40,11 +40,6 @@ export function createTestConfig<TServices extends {} = typeof services>(
serverArgs: [
...svlSharedConfig.get('kbnTestServer.serverArgs'),
`--serverless=${options.serverlessProject}`,
// Ensures the existing E2E tests are backwards compatible with the old rule create flyout
// Remove this experiment once all of the migration has been completed
`--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([
'isUsingRuleCreateFlyout',
])}`,
// custom native roles are enabled only for search and security projects
...(options.serverlessProject !== 'oblt'
? ['--xpack.security.roleManagementEnabled=true']