mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# 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:
parent
5a5519034d
commit
68195d3afb
55 changed files with 49 additions and 8228 deletions
|
@ -29,4 +29,5 @@ export {
|
|||
RuleActionsNotifyWhen,
|
||||
RuleActionsAlertsFilter,
|
||||
RuleActionsAlertsFilterTimeframe,
|
||||
NOTIFY_WHEN_OPTIONS,
|
||||
} from './src/rule_actions';
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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';
|
|
@ -32,5 +32,6 @@
|
|||
"@kbn/unified-search-plugin",
|
||||
"@kbn/response-ops-rule-form",
|
||||
"@kbn/fields-metadata-plugin",
|
||||
"@kbn/i18n-react",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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é d’API.",
|
||||
"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",
|
||||
|
|
|
@ -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": "ルールの実行がスケジュールされています",
|
||||
|
|
|
@ -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": "计划运行您的规则",
|
||||
|
|
|
@ -71,7 +71,6 @@ export const StorybookContextDecorator: FC<PropsWithChildren<StorybookContextDec
|
|||
ruleKqlBar: true,
|
||||
isMustacheAutocompleteOn: false,
|
||||
showMustacheAutocompleteSwitch: false,
|
||||
isUsingRuleCreateFlyout: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
ruleKqlBar: false,
|
||||
isMustacheAutocompleteOn: false,
|
||||
showMustacheAutocompleteSwitch: false,
|
||||
isUsingRuleCreateFlyout: false,
|
||||
});
|
||||
|
||||
const deprecatedExperimentalValues = new Set(['ruleFormV2']);
|
||||
|
|
|
@ -72,7 +72,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
'xl'
|
||||
)({
|
||||
showCreateRuleButtonInPrompt: true,
|
||||
useNewRuleForm: true,
|
||||
setHeaderActions,
|
||||
});
|
||||
}, []);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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})`}
|
||||
|
|
|
@ -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'))
|
||||
);
|
||||
|
|
|
@ -223,7 +223,6 @@ export function RuleComponent({
|
|||
actionTypeRegistry,
|
||||
ruleTypeRegistry,
|
||||
hideEditButton: true,
|
||||
useNewRuleForm: true,
|
||||
onEditRule: requestRefresh,
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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))
|
||||
);
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.triggersActionsUI__ruleTypeNodeHeading {
|
||||
margin-left: $euiSizeS;
|
||||
margin-right: $euiSizeS;
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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\\"
|
||||
}
|
||||
]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
', '
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -353,7 +353,6 @@ export interface RuleDefinitionProps<Params extends RuleTypeParams = RuleTypePar
|
|||
onEditRule: () => Promise<void>;
|
||||
hideEditButton?: boolean;
|
||||
filteredRuleTypes?: string[];
|
||||
useNewRuleForm?: boolean;
|
||||
}
|
||||
|
||||
export enum Percentiles {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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']
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue