mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[8.x] [Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout (#206154) (#207903)
# Backport This will backport the following commits from `main` to `8.x`: - [[Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout (#206154)](https://github.com/elastic/kibana/pull/206154) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Zacqary Adam Xeper","email":"Zacqary@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-01-22T18:53:08Z","message":"[Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout (#206154)\n\n## Summary\r\n\r\nPart of #195211\r\n\r\n- Adds Show Request screen to the new rule form flyout\r\n\r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"585\" alt=\"Screenshot 2025-01-10 at 1 30 15 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/72500b0d-d959-4d17-944e-a7dc0894fb98\"\r\n/>\r\n</details>\r\n\r\n- Renders the action connectors UI within the flyout instead of opening\r\na modal\r\n \r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"505\" alt=\"Screenshot 2025-01-10 at 1 28 38 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/b5b464c0-7359-43ab-bea1-93d2981a5794\"\r\n/>\r\n</details>\r\n\r\n- Duplicates the dropdown filter design from the flyout UI within the\r\naction connectors modal when displayed on a smaller screen\r\n\r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"809\" alt=\"Screenshot 2025-01-10 at 1 30 28 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/5ef28458-1b6d-4a29-961d-fbcc1640e706\"\r\n/>\r\n</details>\r\n\r\n### Implementation notes\r\n\r\nIn order to get the action connectors UI to render the same way in both\r\na modal and the flyout, without duplicating a large amount of code, I\r\nhad to introduce a little bit of complexity. Within the Rule Page, it's\r\nas simple as opening the UI inside a modal, but the flyout cannot open a\r\nsecond flyout; it has to know when and how to completely replace its own\r\ncontents.\r\n\r\n- The bulk of the action connectors UI is now moved to\r\n`<RuleActionsConnectorsBody>`. `<RuleActionsConnectorsModal>` and\r\n`<RuleFlyoutSelectConnector>` act as wrappers for this component.\r\n- The `<RuleActions>` step no longer handles rendering the connector UI,\r\nbecause it's not at a high enough level to know if it's in the\r\n`<RulePage>` or the `<RuleFlyout>`. Instead, it simply sends a signal up\r\nthe context hierarchy to `setIsConnectorsScreenVisible`.\r\n- A new context called `RuleFormScreenContext` keeps track of\r\n`isConnectorsScreenVisible`, a state for whether or not the action\r\nconnectors \"screen\" is open, regardless of whether that screen is\r\ndisplayed in a modal or a flyout.\r\n- The Rule Page uses `isConnectorsScreenVisible` to determine whether to\r\nrender the modal. This works the same way as it used to, but handled by\r\nthe `<RulePage>` instead of the `<RuleActions>` component.\r\n- The Rule Flyout uses `isConnectorsScreenVisible` to determine whether\r\nto continue to render `<RuleFlyoutBody>` or to completely replace its\r\ncontents with `<RuleFlyoutSelectConnector>`\r\n\r\nFor consistency, this PR also moves the Show Request modal/flyout screen\r\ninto the same system.\r\n\r\n### Testing\r\n\r\nTo test the new flyout, edit\r\n`packages/response-ops/rule_form/src/create_rule_form.tsx` and\r\n`packages/response-ops/rule_form/src/edit_rule_form.tsx` so that they\r\nrender `<RuleFlyout>` instead of `<RulePage>`.\r\n\r\n<details>\r\n<summary><strong>Use this diff block</strong></summary>\r\n\r\n```diff\r\ndiff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx\r\nindex 2f5e0472dcd..564744b96ec 100644\r\n--- a/packages/response-ops/rule_form/src/create_rule_form.tsx\r\n+++ b/packages/response-ops/rule_form/src/create_rule_form.tsx\r\n@@ -31,6 +31,7 @@ import {\r\n parseRuleCircuitBreakerErrorMessage,\r\n } from './utils';\r\n import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations';\r\n+import { RuleFlyout } from './rule_flyout';\r\n \r\n export interface CreateRuleFormProps {\r\n ruleTypeId: string;\r\n@@ -199,7 +200,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {\r\n }),\r\n }}\r\n >\r\n- <RulePage isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />\r\n+ <RuleFlyout isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />\r\n </RuleFormStateProvider>\r\n </div>\r\n );\r\ndiff --git a/packages/response-ops/rule_form/src/edit_rule_form.tsx b/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\nindex 392447114ed..41aecd7245a 100644\r\n--- a/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\n+++ b/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\n@@ -26,6 +26,7 @@ import {\r\n import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations';\r\n import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils';\r\n import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';\r\n+import { RuleFlyout } from './rule_flyout';\r\n \r\n export interface EditRuleFormProps {\r\n id: string;\r\n@@ -193,7 +194,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {\r\n showMustacheAutocompleteSwitch,\r\n }}\r\n >\r\n- <RulePage isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />\r\n+ <RuleFlyout isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />\r\n </RuleFormStateProvider>\r\n </div>\r\n );\r\n```\r\n\r\n</details>\r\n\r\n### Still Todo\r\n\r\n1. Replace all instances of the v1 rule flyout with this new one (it's\r\nused heavily in solutions, not in Stack Management)\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"8004e3e70ad63b938d724eafd561533eeb225cd9","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","Feature:Alerting/RulesManagement","backport:version","v8.18.0"],"title":"[Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout","number":206154,"url":"https://github.com/elastic/kibana/pull/206154","mergeCommit":{"message":"[Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout (#206154)\n\n## Summary\r\n\r\nPart of #195211\r\n\r\n- Adds Show Request screen to the new rule form flyout\r\n\r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"585\" alt=\"Screenshot 2025-01-10 at 1 30 15 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/72500b0d-d959-4d17-944e-a7dc0894fb98\"\r\n/>\r\n</details>\r\n\r\n- Renders the action connectors UI within the flyout instead of opening\r\na modal\r\n \r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"505\" alt=\"Screenshot 2025-01-10 at 1 28 38 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/b5b464c0-7359-43ab-bea1-93d2981a5794\"\r\n/>\r\n</details>\r\n\r\n- Duplicates the dropdown filter design from the flyout UI within the\r\naction connectors modal when displayed on a smaller screen\r\n\r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"809\" alt=\"Screenshot 2025-01-10 at 1 30 28 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/5ef28458-1b6d-4a29-961d-fbcc1640e706\"\r\n/>\r\n</details>\r\n\r\n### Implementation notes\r\n\r\nIn order to get the action connectors UI to render the same way in both\r\na modal and the flyout, without duplicating a large amount of code, I\r\nhad to introduce a little bit of complexity. Within the Rule Page, it's\r\nas simple as opening the UI inside a modal, but the flyout cannot open a\r\nsecond flyout; it has to know when and how to completely replace its own\r\ncontents.\r\n\r\n- The bulk of the action connectors UI is now moved to\r\n`<RuleActionsConnectorsBody>`. `<RuleActionsConnectorsModal>` and\r\n`<RuleFlyoutSelectConnector>` act as wrappers for this component.\r\n- The `<RuleActions>` step no longer handles rendering the connector UI,\r\nbecause it's not at a high enough level to know if it's in the\r\n`<RulePage>` or the `<RuleFlyout>`. Instead, it simply sends a signal up\r\nthe context hierarchy to `setIsConnectorsScreenVisible`.\r\n- A new context called `RuleFormScreenContext` keeps track of\r\n`isConnectorsScreenVisible`, a state for whether or not the action\r\nconnectors \"screen\" is open, regardless of whether that screen is\r\ndisplayed in a modal or a flyout.\r\n- The Rule Page uses `isConnectorsScreenVisible` to determine whether to\r\nrender the modal. This works the same way as it used to, but handled by\r\nthe `<RulePage>` instead of the `<RuleActions>` component.\r\n- The Rule Flyout uses `isConnectorsScreenVisible` to determine whether\r\nto continue to render `<RuleFlyoutBody>` or to completely replace its\r\ncontents with `<RuleFlyoutSelectConnector>`\r\n\r\nFor consistency, this PR also moves the Show Request modal/flyout screen\r\ninto the same system.\r\n\r\n### Testing\r\n\r\nTo test the new flyout, edit\r\n`packages/response-ops/rule_form/src/create_rule_form.tsx` and\r\n`packages/response-ops/rule_form/src/edit_rule_form.tsx` so that they\r\nrender `<RuleFlyout>` instead of `<RulePage>`.\r\n\r\n<details>\r\n<summary><strong>Use this diff block</strong></summary>\r\n\r\n```diff\r\ndiff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx\r\nindex 2f5e0472dcd..564744b96ec 100644\r\n--- a/packages/response-ops/rule_form/src/create_rule_form.tsx\r\n+++ b/packages/response-ops/rule_form/src/create_rule_form.tsx\r\n@@ -31,6 +31,7 @@ import {\r\n parseRuleCircuitBreakerErrorMessage,\r\n } from './utils';\r\n import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations';\r\n+import { RuleFlyout } from './rule_flyout';\r\n \r\n export interface CreateRuleFormProps {\r\n ruleTypeId: string;\r\n@@ -199,7 +200,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {\r\n }),\r\n }}\r\n >\r\n- <RulePage isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />\r\n+ <RuleFlyout isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />\r\n </RuleFormStateProvider>\r\n </div>\r\n );\r\ndiff --git a/packages/response-ops/rule_form/src/edit_rule_form.tsx b/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\nindex 392447114ed..41aecd7245a 100644\r\n--- a/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\n+++ b/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\n@@ -26,6 +26,7 @@ import {\r\n import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations';\r\n import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils';\r\n import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';\r\n+import { RuleFlyout } from './rule_flyout';\r\n \r\n export interface EditRuleFormProps {\r\n id: string;\r\n@@ -193,7 +194,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {\r\n showMustacheAutocompleteSwitch,\r\n }}\r\n >\r\n- <RulePage isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />\r\n+ <RuleFlyout isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />\r\n </RuleFormStateProvider>\r\n </div>\r\n );\r\n```\r\n\r\n</details>\r\n\r\n### Still Todo\r\n\r\n1. Replace all instances of the v1 rule flyout with this new one (it's\r\nused heavily in solutions, not in Stack Management)\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"8004e3e70ad63b938d724eafd561533eeb225cd9"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206154","number":206154,"mergeCommit":{"message":"[Response Ops] [Rule Form] Add Show Request and Add Action screens to flyout (#206154)\n\n## Summary\r\n\r\nPart of #195211\r\n\r\n- Adds Show Request screen to the new rule form flyout\r\n\r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"585\" alt=\"Screenshot 2025-01-10 at 1 30 15 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/72500b0d-d959-4d17-944e-a7dc0894fb98\"\r\n/>\r\n</details>\r\n\r\n- Renders the action connectors UI within the flyout instead of opening\r\na modal\r\n \r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"505\" alt=\"Screenshot 2025-01-10 at 1 28 38 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/b5b464c0-7359-43ab-bea1-93d2981a5794\"\r\n/>\r\n</details>\r\n\r\n- Duplicates the dropdown filter design from the flyout UI within the\r\naction connectors modal when displayed on a smaller screen\r\n\r\n<details>\r\n<summary>Screenshot</summary>\r\n<img width=\"809\" alt=\"Screenshot 2025-01-10 at 1 30 28 PM\"\r\nsrc=\"https://github.com/user-attachments/assets/5ef28458-1b6d-4a29-961d-fbcc1640e706\"\r\n/>\r\n</details>\r\n\r\n### Implementation notes\r\n\r\nIn order to get the action connectors UI to render the same way in both\r\na modal and the flyout, without duplicating a large amount of code, I\r\nhad to introduce a little bit of complexity. Within the Rule Page, it's\r\nas simple as opening the UI inside a modal, but the flyout cannot open a\r\nsecond flyout; it has to know when and how to completely replace its own\r\ncontents.\r\n\r\n- The bulk of the action connectors UI is now moved to\r\n`<RuleActionsConnectorsBody>`. `<RuleActionsConnectorsModal>` and\r\n`<RuleFlyoutSelectConnector>` act as wrappers for this component.\r\n- The `<RuleActions>` step no longer handles rendering the connector UI,\r\nbecause it's not at a high enough level to know if it's in the\r\n`<RulePage>` or the `<RuleFlyout>`. Instead, it simply sends a signal up\r\nthe context hierarchy to `setIsConnectorsScreenVisible`.\r\n- A new context called `RuleFormScreenContext` keeps track of\r\n`isConnectorsScreenVisible`, a state for whether or not the action\r\nconnectors \"screen\" is open, regardless of whether that screen is\r\ndisplayed in a modal or a flyout.\r\n- The Rule Page uses `isConnectorsScreenVisible` to determine whether to\r\nrender the modal. This works the same way as it used to, but handled by\r\nthe `<RulePage>` instead of the `<RuleActions>` component.\r\n- The Rule Flyout uses `isConnectorsScreenVisible` to determine whether\r\nto continue to render `<RuleFlyoutBody>` or to completely replace its\r\ncontents with `<RuleFlyoutSelectConnector>`\r\n\r\nFor consistency, this PR also moves the Show Request modal/flyout screen\r\ninto the same system.\r\n\r\n### Testing\r\n\r\nTo test the new flyout, edit\r\n`packages/response-ops/rule_form/src/create_rule_form.tsx` and\r\n`packages/response-ops/rule_form/src/edit_rule_form.tsx` so that they\r\nrender `<RuleFlyout>` instead of `<RulePage>`.\r\n\r\n<details>\r\n<summary><strong>Use this diff block</strong></summary>\r\n\r\n```diff\r\ndiff --git a/packages/response-ops/rule_form/src/create_rule_form.tsx b/packages/response-ops/rule_form/src/create_rule_form.tsx\r\nindex 2f5e0472dcd..564744b96ec 100644\r\n--- a/packages/response-ops/rule_form/src/create_rule_form.tsx\r\n+++ b/packages/response-ops/rule_form/src/create_rule_form.tsx\r\n@@ -31,6 +31,7 @@ import {\r\n parseRuleCircuitBreakerErrorMessage,\r\n } from './utils';\r\n import { RULE_CREATE_SUCCESS_TEXT, RULE_CREATE_ERROR_TEXT } from './translations';\r\n+import { RuleFlyout } from './rule_flyout';\r\n \r\n export interface CreateRuleFormProps {\r\n ruleTypeId: string;\r\n@@ -199,7 +200,7 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {\r\n }),\r\n }}\r\n >\r\n- <RulePage isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />\r\n+ <RuleFlyout isEdit={false} isSaving={isSaving} onCancel={onCancel} onSave={onSave} />\r\n </RuleFormStateProvider>\r\n </div>\r\n );\r\ndiff --git a/packages/response-ops/rule_form/src/edit_rule_form.tsx b/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\nindex 392447114ed..41aecd7245a 100644\r\n--- a/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\n+++ b/packages/response-ops/rule_form/src/edit_rule_form.tsx\r\n@@ -26,6 +26,7 @@ import {\r\n import { RULE_EDIT_ERROR_TEXT, RULE_EDIT_SUCCESS_TEXT } from './translations';\r\n import { getAvailableRuleTypes, parseRuleCircuitBreakerErrorMessage } from './utils';\r\n import { DEFAULT_VALID_CONSUMERS, getDefaultFormData } from './constants';\r\n+import { RuleFlyout } from './rule_flyout';\r\n \r\n export interface EditRuleFormProps {\r\n id: string;\r\n@@ -193,7 +194,7 @@ export const EditRuleForm = (props: EditRuleFormProps) => {\r\n showMustacheAutocompleteSwitch,\r\n }}\r\n >\r\n- <RulePage isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />\r\n+ <RuleFlyout isEdit={true} isSaving={isSaving} onSave={onSave} onCancel={onCancel} />\r\n </RuleFormStateProvider>\r\n </div>\r\n );\r\n```\r\n\r\n</details>\r\n\r\n### Still Todo\r\n\r\n1. Replace all instances of the v1 rule flyout with this new one (it's\r\nused heavily in solutions, not in Stack Management)\r\n\r\n### Checklist\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"8004e3e70ad63b938d724eafd561533eeb225cd9"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Zacqary Adam Xeper <Zacqary@users.noreply.github.com>
This commit is contained in:
parent
14b57223c4
commit
ae8be0d485
26 changed files with 1127 additions and 650 deletions
|
@ -10,3 +10,4 @@
|
|||
export * from './use_rule_form_dispatch';
|
||||
export * from './use_rule_form_state';
|
||||
export * from './use_rule_form_steps';
|
||||
export * from './use_rule_form_screen_context';
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { RuleFormScreenContext } from '../rule_form_screen_context';
|
||||
|
||||
export const useRuleFormScreenContext = () => {
|
||||
return useContext(RuleFormScreenContext);
|
||||
};
|
|
@ -149,13 +149,7 @@ const useCommonRuleFormSteps = ({
|
|||
? {
|
||||
title: RULE_FORM_PAGE_RULE_ACTIONS_TITLE,
|
||||
status: actionsStatus,
|
||||
children: (
|
||||
<>
|
||||
<RuleActions />
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
),
|
||||
children: <RuleActions />,
|
||||
}
|
||||
: null,
|
||||
[RuleFormStepId.DETAILS]: {
|
||||
|
@ -163,13 +157,7 @@ const useCommonRuleFormSteps = ({
|
|||
? RULE_FORM_PAGE_RULE_DETAILS_TITLE_SHORT
|
||||
: RULE_FORM_PAGE_RULE_DETAILS_TITLE,
|
||||
status: ruleDetailsStatus,
|
||||
children: (
|
||||
<>
|
||||
<RuleDetails />
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
),
|
||||
children: <RuleDetails />,
|
||||
},
|
||||
}),
|
||||
[ruleDefinitionStatus, canReadConnectors, actionsStatus, ruleDetailsStatus, shortTitles]
|
||||
|
@ -210,7 +198,7 @@ export const useRuleFormSteps: () => RuleFormVerticalSteps = () => {
|
|||
|
||||
const mappedSteps = useMemo(() => {
|
||||
return stepOrder
|
||||
.map((stepId) => {
|
||||
.map((stepId, index) => {
|
||||
const step = steps[stepId];
|
||||
return step
|
||||
? {
|
||||
|
@ -227,6 +215,12 @@ export const useRuleFormSteps: () => RuleFormVerticalSteps = () => {
|
|||
stepId={stepId}
|
||||
>
|
||||
{step.children}
|
||||
{index > 0 && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
)}
|
||||
</ReportOnBlur>
|
||||
),
|
||||
}
|
||||
|
@ -246,8 +240,10 @@ interface RuleFormHorizontalSteps {
|
|||
hasNextStep: boolean;
|
||||
hasPreviousStep: boolean;
|
||||
}
|
||||
export const useRuleFormHorizontalSteps: () => RuleFormHorizontalSteps = () => {
|
||||
const [currentStep, setCurrentStep] = useState<RuleFormStepId>(STEP_ORDER[0]);
|
||||
export const useRuleFormHorizontalSteps: (
|
||||
initialStep?: RuleFormStepId
|
||||
) => RuleFormHorizontalSteps = (initialStep = STEP_ORDER[0]) => {
|
||||
const [currentStep, setCurrentStep] = useState<RuleFormStepId>(initialStep);
|
||||
const [touchedSteps, setTouchedSteps] = useState<Record<RuleFormStepId, boolean>>(
|
||||
STEP_ORDER.reduce(
|
||||
(result, stepId) => ({ ...result, [stepId]: false }),
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './request_code_block';
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { omit, pick } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiCodeBlock } from '@elastic/eui';
|
||||
import {
|
||||
CreateRuleBody,
|
||||
UPDATE_FIELDS_WITH_ACTIONS,
|
||||
UpdateRuleBody,
|
||||
transformCreateRuleBody,
|
||||
transformUpdateRuleBody,
|
||||
} from '../common/apis';
|
||||
import { BASE_ALERTING_API_PATH } from '../constants';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import { SHOW_REQUEST_MODAL_ERROR } from '../translations';
|
||||
import { RuleFormData } from '../types';
|
||||
|
||||
const stringifyBodyRequest = ({
|
||||
formData,
|
||||
isEdit,
|
||||
}: {
|
||||
formData: RuleFormData;
|
||||
isEdit: boolean;
|
||||
}): string => {
|
||||
try {
|
||||
const request = isEdit
|
||||
? transformUpdateRuleBody(pick(formData, UPDATE_FIELDS_WITH_ACTIONS) as UpdateRuleBody)
|
||||
: transformCreateRuleBody(omit(formData, 'id') as CreateRuleBody);
|
||||
return JSON.stringify(request, null, 2);
|
||||
} catch {
|
||||
return SHOW_REQUEST_MODAL_ERROR;
|
||||
}
|
||||
};
|
||||
|
||||
interface RequestCodeBlockProps {
|
||||
isEdit: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
export const RequestCodeBlock = (props: RequestCodeBlockProps) => {
|
||||
const { isEdit, 'data-test-subj': dataTestSubj } = props;
|
||||
const { formData, id, multiConsumerSelection } = useRuleFormState();
|
||||
|
||||
const formattedRequest = useMemo(() => {
|
||||
return stringifyBodyRequest({
|
||||
formData: {
|
||||
...formData,
|
||||
...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}),
|
||||
},
|
||||
isEdit,
|
||||
});
|
||||
}, [formData, isEdit, multiConsumerSelection]);
|
||||
|
||||
return (
|
||||
<EuiCodeBlock language="json" isCopyable data-test-subj={dataTestSubj}>
|
||||
{`${isEdit ? 'PUT' : 'POST'} kbn:${BASE_ALERTING_API_PATH}/rule${
|
||||
isEdit ? `/${id}` : ''
|
||||
}\n${formattedRequest}`}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
};
|
|
@ -28,6 +28,7 @@ const http = httpServiceMock.createStartContract();
|
|||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
useRuleFormScreenContext: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./rule_actions_system_actions_item', () => ({
|
||||
|
@ -94,7 +95,8 @@ const mockValidate = jest.fn().mockResolvedValue({
|
|||
errors: {},
|
||||
});
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
const { useRuleFormState, useRuleFormDispatch, useRuleFormScreenContext } =
|
||||
jest.requireMock('../hooks');
|
||||
const { useLoadConnectors, useLoadConnectorTypes, useLoadRuleTypeAadTemplateField } =
|
||||
jest.requireMock('../common/hooks');
|
||||
|
||||
|
@ -109,6 +111,7 @@ const mockActions = [getAction('1'), getAction('2')];
|
|||
const mockSystemActions = [getSystemAction('3')];
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
const mockSetIsConnectorsScreenVisible = jest.fn();
|
||||
|
||||
describe('ruleActions', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -167,6 +170,9 @@ describe('ruleActions', () => {
|
|||
aadTemplateFields: [],
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
useRuleFormScreenContext.mockReturnValue({
|
||||
setIsConnectorsScreenVisible: mockSetIsConnectorsScreenVisible,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -216,29 +222,7 @@ describe('ruleActions', () => {
|
|||
render(<RuleActions />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsAddActionButton'));
|
||||
expect(screen.getByText('RuleActionsConnectorsModal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onSelectConnector with the correct parameters', async () => {
|
||||
render(<RuleActions />);
|
||||
|
||||
await userEvent.click(screen.getByTestId('ruleActionsAddActionButton'));
|
||||
expect(screen.getByText('RuleActionsConnectorsModal')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('select connector'));
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
actionTypeId: 'actionType-1',
|
||||
frequency: { notifyWhen: 'onActionGroupChange', summary: false, throttle: null },
|
||||
group: 'test',
|
||||
id: 'connector-1',
|
||||
params: { key: 'value' },
|
||||
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
||||
},
|
||||
type: 'addAction',
|
||||
});
|
||||
|
||||
expect(screen.queryByText('RuleActionsConnectorsModal')).not.toBeInTheDocument();
|
||||
expect(mockSetIsConnectorsScreenVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should use the rule producer ID if it is not a multi-consumer rule', async () => {
|
||||
|
|
|
@ -9,21 +9,17 @@
|
|||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { RuleSystemAction } from '@kbn/alerting-types';
|
||||
import { ActionConnector } from '@kbn/alerts-ui-shared';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import useEffectOnce from 'react-use/lib/useEffectOnce';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { RuleAction, RuleFormParamsErrors } from '../common/types';
|
||||
import { DEFAULT_FREQUENCY, MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
import { useRuleFormDispatch, useRuleFormState } from '../hooks';
|
||||
import { RuleAction } from '../common/types';
|
||||
import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../constants';
|
||||
import { useRuleFormState, useRuleFormScreenContext } from '../hooks';
|
||||
import {
|
||||
ADD_ACTION_DESCRIPTION_TEXT,
|
||||
ADD_ACTION_HEADER,
|
||||
ADD_ACTION_OPTIONAL_TEXT,
|
||||
ADD_ACTION_TEXT,
|
||||
} from '../translations';
|
||||
import { getDefaultParams } from '../utils';
|
||||
import { RuleActionsConnectorsModal } from './rule_actions_connectors_modal';
|
||||
import { RuleActionsItem } from './rule_actions_item';
|
||||
import { RuleActionsSystemActionsItem } from './rule_actions_system_actions_item';
|
||||
|
||||
|
@ -40,69 +36,19 @@ const useRuleActionsIllustration = () => {
|
|||
};
|
||||
|
||||
export const RuleActions = () => {
|
||||
const [isConnectorModalOpen, setIsConnectorModalOpen] = useState<boolean>(false);
|
||||
const ruleActionsIllustration = useRuleActionsIllustration();
|
||||
const { setIsConnectorsScreenVisible } = useRuleFormScreenContext();
|
||||
|
||||
const {
|
||||
formData: { actions, consumer },
|
||||
plugins: { actionTypeRegistry },
|
||||
multiConsumerSelection,
|
||||
selectedRuleType,
|
||||
connectorTypes,
|
||||
} = useRuleFormState();
|
||||
|
||||
const dispatch = useRuleFormDispatch();
|
||||
|
||||
const onModalOpen = useCallback(() => {
|
||||
setIsConnectorModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const onModalClose = useCallback(() => {
|
||||
setIsConnectorModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const onSelectConnector = useCallback(
|
||||
async (connector: ActionConnector) => {
|
||||
const { id, actionTypeId } = connector;
|
||||
const uuid = uuidv4();
|
||||
const group = selectedRuleType.defaultActionGroupId;
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
const params =
|
||||
getDefaultParams({
|
||||
group,
|
||||
ruleType: selectedRuleType,
|
||||
actionTypeModel,
|
||||
}) || {};
|
||||
|
||||
dispatch({
|
||||
type: 'addAction',
|
||||
payload: {
|
||||
id,
|
||||
actionTypeId,
|
||||
uuid,
|
||||
params,
|
||||
group,
|
||||
frequency: DEFAULT_FREQUENCY,
|
||||
},
|
||||
});
|
||||
|
||||
const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry
|
||||
.get(actionTypeId)
|
||||
?.validateParams(params);
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParamsError',
|
||||
payload: {
|
||||
uuid,
|
||||
errors: res.errors,
|
||||
},
|
||||
});
|
||||
|
||||
onModalClose();
|
||||
},
|
||||
[dispatch, onModalClose, selectedRuleType, actionTypeRegistry]
|
||||
);
|
||||
setIsConnectorsScreenVisible(true);
|
||||
}, [setIsConnectorsScreenVisible]);
|
||||
|
||||
const producerId = useMemo(() => {
|
||||
if (MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleType.id)) {
|
||||
|
@ -184,9 +130,6 @@ export const RuleActions = () => {
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isConnectorModalOpen && (
|
||||
<RuleActionsConnectorsModal onClose={onModalClose} onSelectConnector={onSelectConnector} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { RuleActionsConnectorsBody } from './rule_actions_connectors_body';
|
||||
import type { ActionConnector, ActionTypeModel } from '@kbn/alerts-ui-shared';
|
||||
import { TypeRegistry } from '@kbn/alerts-ui-shared/lib';
|
||||
import { ActionType } from '@kbn/actions-types';
|
||||
import {
|
||||
getActionType,
|
||||
getActionTypeModel,
|
||||
getConnector,
|
||||
} from '../common/test_utils/actions_test_utils';
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
getDefaultParams: jest.fn(),
|
||||
}));
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
|
||||
const mockConnectors: ActionConnector[] = [getConnector('1'), getConnector('2')];
|
||||
|
||||
const mockActionTypes: ActionType[] = [getActionType('1'), getActionType('2')];
|
||||
|
||||
const mockOnSelectConnector = jest.fn();
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('ruleActionsConnectorsBody', () => {
|
||||
beforeEach(() => {
|
||||
const actionTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
actionTypeRegistry.register(getActionTypeModel('1', { id: 'actionType-1' }));
|
||||
actionTypeRegistry.register(getActionTypeModel('2', { id: 'actionType-2' }));
|
||||
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
actionTypeRegistry,
|
||||
},
|
||||
formData: {
|
||||
actions: [],
|
||||
},
|
||||
connectors: mockConnectors,
|
||||
connectorTypes: mockActionTypes,
|
||||
aadTemplateFields: [],
|
||||
selectedRuleType: {
|
||||
defaultActionGroupId: 'default',
|
||||
},
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should call onSelectConnector when connector is clicked', async () => {
|
||||
render(<RuleActionsConnectorsBody onSelectConnector={mockOnSelectConnector} />);
|
||||
|
||||
await userEvent.click(screen.getByText('connector-1'));
|
||||
await waitFor(() =>
|
||||
expect(mockOnSelectConnector).toHaveBeenLastCalledWith({
|
||||
actionTypeId: 'actionType-1',
|
||||
config: { config: 'config-1' },
|
||||
id: 'connector-1',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'connector-1',
|
||||
secrets: { secret: 'secret' },
|
||||
})
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('connector-2'));
|
||||
await waitFor(() =>
|
||||
expect(mockOnSelectConnector).toHaveBeenLastCalledWith({
|
||||
actionTypeId: 'actionType-2',
|
||||
config: { config: 'config-2' },
|
||||
id: 'connector-2',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'connector-2',
|
||||
secrets: { secret: 'secret' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,467 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiEmptyPrompt,
|
||||
EuiFacetButton,
|
||||
EuiFacetGroup,
|
||||
EuiFieldSearch,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiPopover,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
useEuiTheme,
|
||||
EuiSelectable,
|
||||
EuiSelectableProps,
|
||||
useCurrentEuiBreakpoint,
|
||||
} from '@elastic/eui';
|
||||
import { ActionConnector, checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared';
|
||||
import React, { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { RuleFormParamsErrors } from '../common/types';
|
||||
import { DEFAULT_FREQUENCY } from '../constants';
|
||||
import { useRuleFormDispatch, useRuleFormState } from '../hooks';
|
||||
import {
|
||||
ACTION_TYPE_MODAL_EMPTY_TEXT,
|
||||
ACTION_TYPE_MODAL_EMPTY_TITLE,
|
||||
ACTION_TYPE_MODAL_FILTER_ALL,
|
||||
ACTION_TYPE_MODAL_FILTER_LIST_TITLE,
|
||||
MODAL_SEARCH_CLEAR_FILTERS_TEXT,
|
||||
MODAL_SEARCH_PLACEHOLDER,
|
||||
} from '../translations';
|
||||
import { getDefaultParams } from '../utils';
|
||||
|
||||
type ConnectorsMap = Record<string, { actionTypeId: string; name: string; total: number }>;
|
||||
|
||||
export interface RuleActionsConnectorsBodyProps {
|
||||
onSelectConnector: (connector?: ActionConnector) => void;
|
||||
responsiveOverflow?: 'auto' | 'hidden';
|
||||
}
|
||||
|
||||
export const RuleActionsConnectorsBody = ({
|
||||
onSelectConnector,
|
||||
responsiveOverflow = 'auto',
|
||||
}: RuleActionsConnectorsBodyProps) => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [selectedConnectorType, setSelectedConnectorType] = useState<string>('all');
|
||||
const [isConenctorFilterPopoverOpen, setIsConenctorFilterPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const currentBreakpoint = useCurrentEuiBreakpoint() ?? 'm';
|
||||
|
||||
const {
|
||||
plugins: { actionTypeRegistry },
|
||||
formData: { actions },
|
||||
connectors,
|
||||
connectorTypes,
|
||||
selectedRuleType,
|
||||
} = useRuleFormState();
|
||||
|
||||
const dispatch = useRuleFormDispatch();
|
||||
|
||||
const onSelectConnectorInternal = useCallback(
|
||||
async (connector: ActionConnector) => {
|
||||
const { id, actionTypeId } = connector;
|
||||
const uuid = uuidv4();
|
||||
const group = selectedRuleType.defaultActionGroupId;
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
const params =
|
||||
getDefaultParams({
|
||||
group,
|
||||
ruleType: selectedRuleType,
|
||||
actionTypeModel,
|
||||
}) || {};
|
||||
|
||||
dispatch({
|
||||
type: 'addAction',
|
||||
payload: {
|
||||
id,
|
||||
actionTypeId,
|
||||
uuid,
|
||||
params,
|
||||
group,
|
||||
frequency: DEFAULT_FREQUENCY,
|
||||
},
|
||||
});
|
||||
|
||||
const res: { errors: RuleFormParamsErrors } = await actionTypeRegistry
|
||||
.get(actionTypeId)
|
||||
?.validateParams(params);
|
||||
|
||||
dispatch({
|
||||
type: 'setActionParamsError',
|
||||
payload: {
|
||||
uuid,
|
||||
errors: res.errors,
|
||||
},
|
||||
});
|
||||
|
||||
// Send connector to onSelectConnector mainly for testing purposes, dispatch handles form data updates
|
||||
onSelectConnector(connector);
|
||||
},
|
||||
[dispatch, onSelectConnector, selectedRuleType, actionTypeRegistry]
|
||||
);
|
||||
|
||||
const preconfiguredConnectors = useMemo(() => {
|
||||
return connectors.filter((connector) => connector.isPreconfigured);
|
||||
}, [connectors]);
|
||||
|
||||
const availableConnectors = useMemo(() => {
|
||||
return connectors.filter(({ actionTypeId }) => {
|
||||
const actionType = connectorTypes.find(({ id }) => id === actionTypeId);
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
if (!actionType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!actionTypeModel.actionParamsFields) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
|
||||
if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [connectors, connectorTypes, preconfiguredConnectors, actionTypeRegistry]);
|
||||
|
||||
const onSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onConnectorOptionSelect = useCallback(
|
||||
(id: string) => () => {
|
||||
setSelectedConnectorType((prev) => {
|
||||
if (prev === id) {
|
||||
return 'all';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onClearFilters = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setSelectedConnectorType('all');
|
||||
}, []);
|
||||
|
||||
const connectorsMap: ConnectorsMap | null = useMemo(() => {
|
||||
return availableConnectors.reduce<ConnectorsMap>((result, { actionTypeId }) => {
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const subtype = actionTypeModel.subtype;
|
||||
|
||||
const shownActionTypeId = actionTypeModel.hideInUi
|
||||
? subtype?.filter((type) => type.id !== actionTypeId)[0].id
|
||||
: undefined;
|
||||
|
||||
const currentActionTypeId = shownActionTypeId ? shownActionTypeId : actionTypeId;
|
||||
|
||||
if (result[currentActionTypeId]) {
|
||||
result[currentActionTypeId].total += 1;
|
||||
} else {
|
||||
result[currentActionTypeId] = {
|
||||
actionTypeId: currentActionTypeId,
|
||||
total: 1,
|
||||
name: connectorTypes.find(({ id }) => id === currentActionTypeId)?.name || '',
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
}, [availableConnectors, connectorTypes, actionTypeRegistry]);
|
||||
|
||||
const filteredConnectors = useMemo(() => {
|
||||
return availableConnectors
|
||||
.filter(({ actionTypeId }) => {
|
||||
const subtype = actionTypeRegistry.get(actionTypeId).subtype?.map((type) => type.id);
|
||||
|
||||
if (selectedConnectorType === 'all' || selectedConnectorType === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subtype?.includes(selectedConnectorType)) {
|
||||
return subtype.includes(actionTypeId);
|
||||
}
|
||||
|
||||
return selectedConnectorType === actionTypeId;
|
||||
})
|
||||
.filter(({ actionTypeId, name }) => {
|
||||
const trimmedSearchValue = searchValue.trim().toLocaleLowerCase();
|
||||
if (trimmedSearchValue === '') {
|
||||
return true;
|
||||
}
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const actionType = connectorTypes.find(({ id }) => id === actionTypeId);
|
||||
const textSearchTargets = [
|
||||
name.toLocaleLowerCase(),
|
||||
actionTypeModel.selectMessage?.toLocaleLowerCase(),
|
||||
actionTypeModel.actionTypeTitle?.toLocaleLowerCase(),
|
||||
actionType?.name?.toLocaleLowerCase(),
|
||||
];
|
||||
return textSearchTargets.some((text) => text?.includes(trimmedSearchValue));
|
||||
});
|
||||
}, [availableConnectors, selectedConnectorType, searchValue, connectorTypes, actionTypeRegistry]);
|
||||
|
||||
const connectorFacetButtons = useMemo(() => {
|
||||
return (
|
||||
<EuiFacetGroup
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButtonGroup"
|
||||
style={{ overflow: 'auto' }}
|
||||
>
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key="all"
|
||||
quantity={availableConnectors.length}
|
||||
isSelected={selectedConnectorType === 'all'}
|
||||
onClick={onConnectorOptionSelect('all')}
|
||||
>
|
||||
{ACTION_TYPE_MODAL_FILTER_ALL}
|
||||
</EuiFacetButton>
|
||||
{Object.values(connectorsMap)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({ actionTypeId, name, total }) => {
|
||||
return (
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key={actionTypeId}
|
||||
quantity={total}
|
||||
isSelected={selectedConnectorType === actionTypeId}
|
||||
onClick={onConnectorOptionSelect(actionTypeId)}
|
||||
>
|
||||
{name}
|
||||
</EuiFacetButton>
|
||||
);
|
||||
})}
|
||||
</EuiFacetGroup>
|
||||
);
|
||||
}, [availableConnectors, connectorsMap, selectedConnectorType, onConnectorOptionSelect]);
|
||||
|
||||
const toggleFilterPopover = useCallback(() => {
|
||||
setIsConenctorFilterPopoverOpen((prev) => !prev);
|
||||
}, []);
|
||||
const closeFilterPopover = useCallback(() => {
|
||||
setIsConenctorFilterPopoverOpen(false);
|
||||
}, []);
|
||||
const connectorFilterButton = useMemo(() => {
|
||||
const button = (
|
||||
<EuiFilterButton
|
||||
iconType="arrowDown"
|
||||
badgeColor="accent"
|
||||
hasActiveFilters={selectedConnectorType !== 'all'}
|
||||
numActiveFilters={selectedConnectorType !== 'all' ? 1 : undefined}
|
||||
onClick={toggleFilterPopover}
|
||||
isSelected={isConenctorFilterPopoverOpen}
|
||||
>
|
||||
{ACTION_TYPE_MODAL_FILTER_LIST_TITLE}
|
||||
</EuiFilterButton>
|
||||
);
|
||||
|
||||
const options: EuiSelectableProps['options'] = Object.values(connectorsMap)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({ actionTypeId, name }) => ({
|
||||
label: name,
|
||||
checked: selectedConnectorType === actionTypeId ? 'on' : undefined,
|
||||
onClick: onConnectorOptionSelect(actionTypeId),
|
||||
}));
|
||||
|
||||
return (
|
||||
<EuiFilterGroup style={{ width: '100%' }}>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
closePopover={closeFilterPopover}
|
||||
isOpen={isConenctorFilterPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiSelectable singleSelection options={options}>
|
||||
{(list) => <div style={{ width: 400 }}>{list}</div>}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
}, [
|
||||
closeFilterPopover,
|
||||
connectorsMap,
|
||||
isConenctorFilterPopoverOpen,
|
||||
onConnectorOptionSelect,
|
||||
toggleFilterPopover,
|
||||
selectedConnectorType,
|
||||
]);
|
||||
|
||||
const connectorCards = useMemo(() => {
|
||||
if (!filteredConnectors.length) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="ruleActionsConnectorsModalEmpty"
|
||||
color="subdued"
|
||||
iconType="search"
|
||||
title={<h2>{ACTION_TYPE_MODAL_EMPTY_TITLE}</h2>}
|
||||
body={
|
||||
<EuiText>
|
||||
<p>{ACTION_TYPE_MODAL_EMPTY_TEXT}</p>
|
||||
</EuiText>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="ruleActionsConnectorsModalClearFiltersButton"
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
{MODAL_SEARCH_CLEAR_FILTERS_TEXT}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{filteredConnectors.map((connector) => {
|
||||
const { id, actionTypeId, name } = connector;
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const actionType = connectorTypes.find((item) => item.id === actionTypeId);
|
||||
|
||||
if (!actionType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
|
||||
const isSystemActionsSelected = Boolean(
|
||||
actionTypeModel.isSystemActionType &&
|
||||
actions.find((action) => action.actionTypeId === actionTypeModel.id)
|
||||
);
|
||||
|
||||
const isDisabled = !checkEnabledResult.isEnabled || isSystemActionsSelected;
|
||||
|
||||
const connectorCard = (
|
||||
<EuiCard
|
||||
data-test-subj="ruleActionsConnectorsModalCard"
|
||||
hasBorder
|
||||
isDisabled={isDisabled}
|
||||
titleSize="xs"
|
||||
layout="horizontal"
|
||||
icon={
|
||||
<div style={{ marginInlineEnd: `16px` }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<EuiIcon size="l" type={actionTypeModel.iconClass} />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
title={name}
|
||||
description={
|
||||
<>
|
||||
<EuiText size="xs">{actionTypeModel.selectMessage}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued" size="xs" style={{ textTransform: 'uppercase' }}>
|
||||
<strong>{actionType?.name}</strong>
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
onClick={() => onSelectConnectorInternal(connector)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem key={id} grow={false}>
|
||||
{checkEnabledResult.isEnabled && connectorCard}
|
||||
{!checkEnabledResult.isEnabled && (
|
||||
<EuiToolTip position="top" content={checkEnabledResult.message}>
|
||||
{connectorCard}
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [
|
||||
actions,
|
||||
preconfiguredConnectors,
|
||||
filteredConnectors,
|
||||
actionTypeRegistry,
|
||||
connectorTypes,
|
||||
onSelectConnectorInternal,
|
||||
onClearFilters,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" style={{ overflow: responsiveOverflow, height: '100%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexGroup gutterSize="s" wrap={false} responsive={false}>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiFieldSearch
|
||||
fullWidth={
|
||||
/* TODO Determine this using @container breakpoints once we have a better helper function for
|
||||
* determining the size of a CSS @container. This works in practice because when the action connector
|
||||
* UI is displayed in a modal, a screen breakpoint of 'm' is equivalent to a container breakpoint of 's',
|
||||
* but we should replace this with a more robust solution in the future. This may not be very easy until
|
||||
* https://github.com/w3c/csswg-drafts/issues/6205 is resolved, but we could theoretically hack something
|
||||
* together using showForContainer classes and React refs.
|
||||
*/
|
||||
['m', 's', 'xs'].includes(currentBreakpoint)
|
||||
}
|
||||
data-test-subj="ruleActionsConnectorsModalSearch"
|
||||
placeholder={MODAL_SEARCH_PLACEHOLDER}
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="showForContainer--s showForContainer--xs">
|
||||
{connectorFilterButton}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ overflow: responsiveOverflow }}>
|
||||
<EuiFlexGroup style={{ overflow: responsiveOverflow }}>
|
||||
<EuiFlexItem className="hideForContainer--s hideForContainer--xs" grow={1}>
|
||||
{connectorFacetButtons}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
padding: `${euiTheme.size.base} ${euiTheme.size.base} ${euiTheme.size.xl}`,
|
||||
}}
|
||||
>
|
||||
{connectorCards}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -23,18 +23,16 @@ import {
|
|||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormDispatch: jest.fn(),
|
||||
useRuleFormScreenContext: jest.fn(),
|
||||
}));
|
||||
|
||||
const { useRuleFormState, useRuleFormDispatch } = jest.requireMock('../hooks');
|
||||
const { useRuleFormState, useRuleFormDispatch, useRuleFormScreenContext } =
|
||||
jest.requireMock('../hooks');
|
||||
|
||||
const mockConnectors: ActionConnector[] = [getConnector('1'), getConnector('2')];
|
||||
|
||||
const mockActionTypes: ActionType[] = [getActionType('1'), getActionType('2')];
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
const mockOnSelectConnector = jest.fn();
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('ruleActionsConnectorsModal', () => {
|
||||
|
@ -55,6 +53,10 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
aadTemplateFields: [],
|
||||
});
|
||||
useRuleFormDispatch.mockReturnValue(mockOnChange);
|
||||
useRuleFormScreenContext.mockReturnValue({
|
||||
setIsConnectorsScreenVisible: false,
|
||||
setIsShowRequestScreenVisible: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -62,16 +64,12 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
});
|
||||
|
||||
test('renders correctly', () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
expect(screen.getByTestId('ruleActionsConnectorsModal'));
|
||||
});
|
||||
|
||||
test('should render connectors and filters', () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.getByText('connector-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('connector-2')).toBeInTheDocument();
|
||||
|
@ -86,9 +84,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
});
|
||||
|
||||
test('should allow for searching of connectors', async () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
// Type first connector
|
||||
await userEvent.type(screen.getByTestId('ruleActionsConnectorsModalSearch'), 'connector-1');
|
||||
|
@ -116,9 +112,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
});
|
||||
|
||||
test('should allow for filtering of connectors', async () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup');
|
||||
|
||||
|
@ -134,40 +128,8 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
expect(screen.getAllByTestId('ruleActionsConnectorsModalCard').length).toEqual(2);
|
||||
});
|
||||
|
||||
test('should call onSelectConnector when connector is clicked', async () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('connector-1'));
|
||||
expect(mockOnSelectConnector).toHaveBeenLastCalledWith({
|
||||
actionTypeId: 'actionType-1',
|
||||
config: { config: 'config-1' },
|
||||
id: 'connector-1',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'connector-1',
|
||||
secrets: { secret: 'secret' },
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText('connector-2'));
|
||||
expect(mockOnSelectConnector).toHaveBeenLastCalledWith({
|
||||
actionTypeId: 'actionType-2',
|
||||
config: { config: 'config-2' },
|
||||
id: 'connector-2',
|
||||
isDeprecated: false,
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false,
|
||||
name: 'connector-2',
|
||||
secrets: { secret: 'secret' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render connector if action type doesnt exist', () => {
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.queryByText('connector2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -188,9 +150,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.queryByText('connector2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -227,9 +187,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup');
|
||||
expect(within(filterButtonGroup).getByText('actionType: 1')).toBeInTheDocument();
|
||||
expect(within(filterButtonGroup).queryByText('actionType: 2')).not.toBeInTheDocument();
|
||||
|
@ -270,9 +228,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
const filterButtonGroup = screen.getByTestId('ruleActionsConnectorsModalFilterButtonGroup');
|
||||
|
||||
await userEvent.click(within(filterButtonGroup).getByText('actionType: 1'));
|
||||
|
@ -302,9 +258,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: mockActionTypes,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.queryByText('connector-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -326,9 +280,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: [getActionType('1'), getActionType('2', { enabledInConfig: false })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.queryByText('connector-2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -350,9 +302,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: [getActionType('1'), getActionType('2', { enabledInConfig: false })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.getByText('connector-2')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -374,9 +324,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: [getActionType('1'), getActionType('2', { enabledInLicense: false })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.getByText('connector-2')).toBeDisabled();
|
||||
});
|
||||
|
@ -399,9 +347,7 @@ describe('ruleActionsConnectorsModal', () => {
|
|||
connectorTypes: [getActionType('1'), getActionType('2', { isSystemActionType: true })],
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsConnectorsModal onClose={mockOnClose} onSelectConnector={mockOnSelectConnector} />
|
||||
);
|
||||
render(<RuleActionsConnectorsModal />);
|
||||
|
||||
expect(screen.getByText('connector-2')).toBeDisabled();
|
||||
});
|
||||
|
|
|
@ -8,317 +8,39 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiEmptyPrompt,
|
||||
EuiFacetButton,
|
||||
EuiFacetGroup,
|
||||
EuiFieldSearch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiIcon,
|
||||
EuiLoadingSpinner,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
useCurrentEuiBreakpoint,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { ActionConnector, checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared';
|
||||
import React, { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import {
|
||||
ACTION_TYPE_MODAL_EMPTY_TEXT,
|
||||
ACTION_TYPE_MODAL_EMPTY_TITLE,
|
||||
ACTION_TYPE_MODAL_FILTER_ALL,
|
||||
ACTION_TYPE_MODAL_TITLE,
|
||||
MODAL_SEARCH_CLEAR_FILTERS_TEXT,
|
||||
MODAL_SEARCH_PLACEHOLDER,
|
||||
} from '../translations';
|
||||
|
||||
type ConnectorsMap = Record<string, { actionTypeId: string; name: string; total: number }>;
|
||||
|
||||
export interface RuleActionsConnectorsModalProps {
|
||||
onClose: () => void;
|
||||
onSelectConnector: (connector: ActionConnector) => void;
|
||||
}
|
||||
|
||||
export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProps) => {
|
||||
const { onClose, onSelectConnector } = props;
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [selectedConnectorType, setSelectedConnectorType] = useState<string>('all');
|
||||
import React, { useCallback } from 'react';
|
||||
import { ACTION_TYPE_MODAL_TITLE } from '../translations';
|
||||
import { RuleActionsConnectorsBody } from './rule_actions_connectors_body';
|
||||
import { useRuleFormScreenContext } from '../hooks';
|
||||
|
||||
export const RuleActionsConnectorsModal = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const currentBreakpoint = useCurrentEuiBreakpoint() ?? 'm';
|
||||
const isFullscreenPortrait = ['s', 'xs'].includes(currentBreakpoint);
|
||||
|
||||
const {
|
||||
plugins: { actionTypeRegistry },
|
||||
formData: { actions },
|
||||
connectors,
|
||||
connectorTypes,
|
||||
} = useRuleFormState();
|
||||
|
||||
const preconfiguredConnectors = useMemo(() => {
|
||||
return connectors.filter((connector) => connector.isPreconfigured);
|
||||
}, [connectors]);
|
||||
|
||||
const availableConnectors = useMemo(() => {
|
||||
return connectors.filter(({ actionTypeId }) => {
|
||||
const actionType = connectorTypes.find(({ id }) => id === actionTypeId);
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
|
||||
if (!actionType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!actionTypeModel.actionParamsFields) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
|
||||
if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [connectors, connectorTypes, preconfiguredConnectors, actionTypeRegistry]);
|
||||
|
||||
const onSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onConnectorOptionSelect = useCallback(
|
||||
(id: string) => () => {
|
||||
setSelectedConnectorType((prev) => {
|
||||
if (prev === id) {
|
||||
return '';
|
||||
}
|
||||
return id;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onClearFilters = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setSelectedConnectorType('all');
|
||||
}, []);
|
||||
|
||||
const connectorsMap: ConnectorsMap | null = useMemo(() => {
|
||||
return availableConnectors.reduce<ConnectorsMap>((result, { actionTypeId }) => {
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const subtype = actionTypeModel.subtype;
|
||||
|
||||
const shownActionTypeId = actionTypeModel.hideInUi
|
||||
? subtype?.filter((type) => type.id !== actionTypeId)[0].id
|
||||
: undefined;
|
||||
|
||||
const currentActionTypeId = shownActionTypeId ? shownActionTypeId : actionTypeId;
|
||||
|
||||
if (result[currentActionTypeId]) {
|
||||
result[currentActionTypeId].total += 1;
|
||||
} else {
|
||||
result[currentActionTypeId] = {
|
||||
actionTypeId: currentActionTypeId,
|
||||
total: 1,
|
||||
name: connectorTypes.find(({ id }) => id === currentActionTypeId)?.name || '',
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
}, [availableConnectors, connectorTypes, actionTypeRegistry]);
|
||||
|
||||
const filteredConnectors = useMemo(() => {
|
||||
return availableConnectors
|
||||
.filter(({ actionTypeId }) => {
|
||||
const subtype = actionTypeRegistry.get(actionTypeId).subtype?.map((type) => type.id);
|
||||
|
||||
if (selectedConnectorType === 'all' || selectedConnectorType === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subtype?.includes(selectedConnectorType)) {
|
||||
return subtype.includes(actionTypeId);
|
||||
}
|
||||
|
||||
return selectedConnectorType === actionTypeId;
|
||||
})
|
||||
.filter(({ actionTypeId, name }) => {
|
||||
const trimmedSearchValue = searchValue.trim().toLocaleLowerCase();
|
||||
if (trimmedSearchValue === '') {
|
||||
return true;
|
||||
}
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const actionType = connectorTypes.find(({ id }) => id === actionTypeId);
|
||||
const textSearchTargets = [
|
||||
name.toLocaleLowerCase(),
|
||||
actionTypeModel.selectMessage?.toLocaleLowerCase(),
|
||||
actionTypeModel.actionTypeTitle?.toLocaleLowerCase(),
|
||||
actionType?.name?.toLocaleLowerCase(),
|
||||
];
|
||||
return textSearchTargets.some((text) => text?.includes(trimmedSearchValue));
|
||||
});
|
||||
}, [availableConnectors, selectedConnectorType, searchValue, connectorTypes, actionTypeRegistry]);
|
||||
|
||||
const connectorFacetButtons = useMemo(() => {
|
||||
return (
|
||||
<EuiFacetGroup
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButtonGroup"
|
||||
style={{ overflow: 'auto' }}
|
||||
>
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key="all"
|
||||
quantity={availableConnectors.length}
|
||||
isSelected={selectedConnectorType === 'all'}
|
||||
onClick={onConnectorOptionSelect('all')}
|
||||
>
|
||||
{ACTION_TYPE_MODAL_FILTER_ALL}
|
||||
</EuiFacetButton>
|
||||
{Object.values(connectorsMap)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(({ actionTypeId, name, total }) => {
|
||||
return (
|
||||
<EuiFacetButton
|
||||
data-test-subj="ruleActionsConnectorsModalFilterButton"
|
||||
key={actionTypeId}
|
||||
quantity={total}
|
||||
isSelected={selectedConnectorType === actionTypeId}
|
||||
onClick={onConnectorOptionSelect(actionTypeId)}
|
||||
>
|
||||
{name}
|
||||
</EuiFacetButton>
|
||||
);
|
||||
})}
|
||||
</EuiFacetGroup>
|
||||
);
|
||||
}, [availableConnectors, connectorsMap, selectedConnectorType, onConnectorOptionSelect]);
|
||||
|
||||
const connectorCards = useMemo(() => {
|
||||
if (!filteredConnectors.length) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="ruleActionsConnectorsModalEmpty"
|
||||
color="subdued"
|
||||
iconType="search"
|
||||
title={<h2>{ACTION_TYPE_MODAL_EMPTY_TITLE}</h2>}
|
||||
body={
|
||||
<EuiText>
|
||||
<p>{ACTION_TYPE_MODAL_EMPTY_TEXT}</p>
|
||||
</EuiText>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="ruleActionsConnectorsModalClearFiltersButton"
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
{MODAL_SEARCH_CLEAR_FILTERS_TEXT}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{filteredConnectors.map((connector) => {
|
||||
const { id, actionTypeId, name } = connector;
|
||||
const actionTypeModel = actionTypeRegistry.get(actionTypeId);
|
||||
const actionType = connectorTypes.find((item) => item.id === actionTypeId);
|
||||
|
||||
if (!actionType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checkEnabledResult = checkActionFormActionTypeEnabled(
|
||||
actionType,
|
||||
preconfiguredConnectors
|
||||
);
|
||||
|
||||
const isSystemActionsSelected = Boolean(
|
||||
actionTypeModel.isSystemActionType &&
|
||||
actions.find((action) => action.actionTypeId === actionTypeModel.id)
|
||||
);
|
||||
|
||||
const isDisabled = !checkEnabledResult.isEnabled || isSystemActionsSelected;
|
||||
|
||||
const connectorCard = (
|
||||
<EuiCard
|
||||
data-test-subj="ruleActionsConnectorsModalCard"
|
||||
hasBorder
|
||||
isDisabled={isDisabled}
|
||||
titleSize="xs"
|
||||
layout="horizontal"
|
||||
icon={
|
||||
<div style={{ marginInlineEnd: `16px` }}>
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<EuiIcon size="l" type={actionTypeModel.iconClass} />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
title={name}
|
||||
description={
|
||||
<>
|
||||
<EuiText size="xs">{actionTypeModel.selectMessage}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued" size="xs" style={{ textTransform: 'uppercase' }}>
|
||||
<strong>{actionType?.name}</strong>
|
||||
</EuiText>
|
||||
</>
|
||||
}
|
||||
onClick={() => onSelectConnector(connector)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem key={id} grow={false}>
|
||||
{checkEnabledResult.isEnabled && connectorCard}
|
||||
{!checkEnabledResult.isEnabled && (
|
||||
<EuiToolTip position="top" content={checkEnabledResult.message}>
|
||||
{connectorCard}
|
||||
</EuiToolTip>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [
|
||||
actions,
|
||||
preconfiguredConnectors,
|
||||
filteredConnectors,
|
||||
actionTypeRegistry,
|
||||
connectorTypes,
|
||||
onSelectConnector,
|
||||
onClearFilters,
|
||||
]);
|
||||
|
||||
const responseiveHeight = isFullscreenPortrait ? 'initial' : '80vh';
|
||||
const responsiveHeight = isFullscreenPortrait ? 'initial' : '80vh';
|
||||
const responsiveOverflow = isFullscreenPortrait ? 'auto' : 'hidden';
|
||||
|
||||
const { setIsConnectorsScreenVisible } = useRuleFormScreenContext();
|
||||
const onClose = useCallback(() => {
|
||||
setIsConnectorsScreenVisible(false);
|
||||
}, [setIsConnectorsScreenVisible]);
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
onClose={onClose}
|
||||
maxWidth={euiTheme.breakpoint[currentBreakpoint]}
|
||||
style={{
|
||||
width: euiTheme.breakpoint[currentBreakpoint],
|
||||
maxHeight: responseiveHeight,
|
||||
height: responseiveHeight,
|
||||
maxHeight: responsiveHeight,
|
||||
height: responsiveHeight,
|
||||
overflow: responsiveOverflow,
|
||||
}}
|
||||
data-test-subj="ruleActionsConnectorsModal"
|
||||
|
@ -326,37 +48,11 @@ export const RuleActionsConnectorsModal = (props: RuleActionsConnectorsModalProp
|
|||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle size="s">{ACTION_TYPE_MODAL_TITLE}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiFlexGroup direction="column" style={{ overflow: responsiveOverflow, height: '100%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="ruleActionsConnectorsModalSearch"
|
||||
placeholder={MODAL_SEARCH_PLACEHOLDER}
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ overflow: responsiveOverflow }}>
|
||||
<EuiFlexGroup style={{ overflow: responsiveOverflow }}>
|
||||
<EuiFlexItem grow={1}>{connectorFacetButtons}</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={3}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
padding: `${euiTheme.size.base} ${euiTheme.size.base} ${euiTheme.size.xl}`,
|
||||
}}
|
||||
>
|
||||
{connectorCards}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiModalBody className="actionConnectorModal__container">
|
||||
<RuleActionsConnectorsBody
|
||||
responsiveOverflow={responsiveOverflow}
|
||||
onSelectConnector={onClose}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { RuleFlyout } from './rule_flyout';
|
||||
import {
|
||||
RULE_FORM_PAGE_RULE_DEFINITION_TITLE_SHORT,
|
||||
|
@ -110,30 +110,22 @@ describe('ruleFlyout', () => {
|
|||
render(<RuleFlyout onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument()
|
||||
);
|
||||
expect(await screen.findByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument()
|
||||
);
|
||||
expect(await screen.findByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('ruleFlyoutFooterPreviousStepButton'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument()
|
||||
);
|
||||
expect(await screen.findByTestId('ruleFlyoutFooterNextStepButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onSave when save button is pressed', async () => {
|
||||
render(<RuleFlyout onCancel={onCancel} onSave={onSave} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument()
|
||||
);
|
||||
expect(await screen.findByTestId('ruleFlyoutFooterPreviousStepButton')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('ruleFlyoutFooterNextStepButton'));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument()
|
||||
);
|
||||
expect(await screen.findByTestId('ruleFlyoutFooterSaveButton')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('ruleFlyoutFooterSaveButton'));
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith({
|
||||
|
|
|
@ -8,9 +8,13 @@
|
|||
*/
|
||||
|
||||
import { EuiFlyout, EuiPortal } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import type { RuleFormData } from '../types';
|
||||
import { RuleFormStepId } from '../constants';
|
||||
import { RuleFlyoutBody } from './rule_flyout_body';
|
||||
import { RuleFlyoutShowRequest } from './rule_flyout_show_request';
|
||||
import { useRuleFormScreenContext } from '../hooks';
|
||||
import { RuleFlyoutSelectConnector } from './rule_flyout_select_connector';
|
||||
|
||||
interface RuleFlyoutProps {
|
||||
isEdit?: boolean;
|
||||
|
@ -19,10 +23,39 @@ interface RuleFlyoutProps {
|
|||
onSave: (formData: RuleFormData) => void;
|
||||
}
|
||||
|
||||
// Wrapper component for the rule flyout. Currently only displays RuleFlyoutBody, but will be extended to conditionally
|
||||
// display the Show Request UI or the Action Connector UI. These UIs take over the entire flyout, so we need to swap out
|
||||
// their body elements entirely to avoid adding another EuiFlyout element to the DOM
|
||||
export const RuleFlyout = ({ onSave, isEdit, isSaving, onCancel = () => {} }: RuleFlyoutProps) => {
|
||||
export const RuleFlyout = ({
|
||||
onSave,
|
||||
isEdit = false,
|
||||
isSaving = false,
|
||||
onCancel = () => {},
|
||||
}: RuleFlyoutProps) => {
|
||||
const [initialStep, setInitialStep] = useState<RuleFormStepId | undefined>(undefined);
|
||||
|
||||
const {
|
||||
isConnectorsScreenVisible,
|
||||
isShowRequestScreenVisible,
|
||||
setIsShowRequestScreenVisible,
|
||||
setIsConnectorsScreenVisible,
|
||||
} = useRuleFormScreenContext();
|
||||
const onCloseConnectorsScreen = useCallback(() => {
|
||||
setInitialStep(RuleFormStepId.ACTIONS);
|
||||
setIsConnectorsScreenVisible(false);
|
||||
}, [setIsConnectorsScreenVisible]);
|
||||
|
||||
const onOpenShowRequest = useCallback(
|
||||
() => setIsShowRequestScreenVisible(true),
|
||||
[setIsShowRequestScreenVisible]
|
||||
);
|
||||
const onCloseShowRequest = useCallback(() => {
|
||||
setInitialStep(RuleFormStepId.DETAILS);
|
||||
setIsShowRequestScreenVisible(false);
|
||||
}, [setIsShowRequestScreenVisible]);
|
||||
|
||||
const hideCloseButton = useMemo(
|
||||
() => isShowRequestScreenVisible || isConnectorsScreenVisible,
|
||||
[isConnectorsScreenVisible, isShowRequestScreenVisible]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
|
@ -32,8 +65,22 @@ export const RuleFlyout = ({ onSave, isEdit, isSaving, onCancel = () => {} }: Ru
|
|||
size="m"
|
||||
maxWidth={500}
|
||||
className="ruleFormFlyout__container"
|
||||
hideCloseButton={hideCloseButton}
|
||||
>
|
||||
<RuleFlyoutBody onSave={onSave} onCancel={onCancel} isEdit={isEdit} isSaving={isSaving} />
|
||||
{isShowRequestScreenVisible ? (
|
||||
<RuleFlyoutShowRequest isEdit={isEdit} onClose={onCloseShowRequest} />
|
||||
) : isConnectorsScreenVisible ? (
|
||||
<RuleFlyoutSelectConnector onClose={onCloseConnectorsScreen} />
|
||||
) : (
|
||||
<RuleFlyoutBody
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
isEdit={isEdit}
|
||||
isSaving={isSaving}
|
||||
onShowRequest={onOpenShowRequest}
|
||||
initialStep={initialStep}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
|
|
|
@ -28,19 +28,24 @@ import { hasRuleErrors } from '../validation';
|
|||
import { RuleFlyoutCreateFooter } from './rule_flyout_create_footer';
|
||||
import { RuleFlyoutEditFooter } from './rule_flyout_edit_footer';
|
||||
import { RuleFlyoutEditTabs } from './rule_flyout_edit_tabs';
|
||||
import { RuleFormStepId } from '../constants';
|
||||
|
||||
interface RuleFlyoutBodyProps {
|
||||
isEdit?: boolean;
|
||||
isSaving?: boolean;
|
||||
onCancel: () => void;
|
||||
onSave: (formData: RuleFormData) => void;
|
||||
onShowRequest: () => void;
|
||||
initialStep?: RuleFormStepId;
|
||||
}
|
||||
|
||||
export const RuleFlyoutBody = ({
|
||||
isEdit = false,
|
||||
isSaving = false,
|
||||
initialStep,
|
||||
onCancel,
|
||||
onSave,
|
||||
onShowRequest,
|
||||
}: RuleFlyoutBodyProps) => {
|
||||
const {
|
||||
formData,
|
||||
|
@ -77,7 +82,7 @@ export const RuleFlyoutBody = ({
|
|||
goToPreviousStep,
|
||||
hasNextStep,
|
||||
hasPreviousStep,
|
||||
} = useRuleFormHorizontalSteps();
|
||||
} = useRuleFormHorizontalSteps(initialStep);
|
||||
|
||||
const { actions } = formData;
|
||||
|
||||
|
@ -133,7 +138,7 @@ export const RuleFlyoutBody = ({
|
|||
<RuleFlyoutEditFooter
|
||||
onCancel={onCancel}
|
||||
onSave={onSaveInternal}
|
||||
onShowRequest={() => {} /* TODO */}
|
||||
onShowRequest={onShowRequest}
|
||||
isSaving={isSaving}
|
||||
hasErrors={hasErrors}
|
||||
/>
|
||||
|
@ -141,7 +146,7 @@ export const RuleFlyoutBody = ({
|
|||
<RuleFlyoutCreateFooter
|
||||
onCancel={onCancel}
|
||||
onSave={onSaveInternal}
|
||||
onShowRequest={() => {} /* TODO */}
|
||||
onShowRequest={onShowRequest}
|
||||
goToNextStep={goToNextStep}
|
||||
goToPreviousStep={goToPreviousStep}
|
||||
isSaving={isSaving}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import {
|
||||
ACTION_TYPE_MODAL_TITLE,
|
||||
RULE_FLYOUT_FOOTER_BACK_TEXT,
|
||||
RULE_FLYOUT_HEADER_BACK_TEXT,
|
||||
} from '../translations';
|
||||
import { RuleActionsConnectorsBody } from '../rule_actions/rule_actions_connectors_body';
|
||||
|
||||
interface RuleFlyoutSelectConnectorProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
export const RuleFlyoutSelectConnector = ({ onClose }: RuleFlyoutSelectConnectorProps) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="arrowLeft"
|
||||
onClick={onClose}
|
||||
aria-label={RULE_FLYOUT_HEADER_BACK_TEXT}
|
||||
color="text"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" data-test-subj="ruleFlyoutSelectConnectorTitle">
|
||||
<h4 id="flyoutTitle">{ACTION_TYPE_MODAL_TITLE}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<RuleActionsConnectorsBody onSelectConnector={onClose} />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
onClick={onClose}
|
||||
data-test-subj="ruleFlyoutSelectConnectorBackButton"
|
||||
>
|
||||
{RULE_FLYOUT_FOOTER_BACK_TEXT}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import {
|
||||
SHOW_REQUEST_MODAL_SUBTITLE,
|
||||
SHOW_REQUEST_MODAL_TITLE,
|
||||
RULE_FLYOUT_FOOTER_BACK_TEXT,
|
||||
RULE_FLYOUT_HEADER_BACK_TEXT,
|
||||
} from '../translations';
|
||||
import { RequestCodeBlock } from '../request_code_block';
|
||||
|
||||
interface RuleFlyoutShowRequestProps {
|
||||
isEdit: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const RuleFlyoutShowRequest = ({ isEdit, onClose }: RuleFlyoutShowRequestProps) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="arrowLeft"
|
||||
onClick={onClose}
|
||||
aria-label={RULE_FLYOUT_HEADER_BACK_TEXT}
|
||||
color="text"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs" data-test-subj="ruleFlyoutShowRequestTitle">
|
||||
<h4 id="flyoutTitle">{SHOW_REQUEST_MODAL_TITLE(isEdit)}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<p>
|
||||
<EuiText color="subdued">{SHOW_REQUEST_MODAL_SUBTITLE(isEdit)}</EuiText>
|
||||
</p>
|
||||
<EuiSpacer />
|
||||
<RequestCodeBlock isEdit={isEdit} data-test-subj="flyoutRequestCodeBlock" />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
onClick={onClose}
|
||||
data-test-subj="ruleFlyoutShowRequestBackButton"
|
||||
>
|
||||
{RULE_FLYOUT_FOOTER_BACK_TEXT}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,11 @@
|
|||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 768px) {
|
||||
.actionConnectorModal__container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 767px) {
|
||||
.euiDescribedFormGroup {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -24,4 +28,28 @@
|
|||
.ruleDefinitionHeaderRuleTypeDescription, .ruleDefinitionHeaderDocsLink {
|
||||
font-size: $euiFontSizeS;
|
||||
}
|
||||
}
|
||||
|
||||
[class*='showForContainer'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container (max-width: 767px) and (min-width: 575px) {
|
||||
.hideForContainer--s {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showForContainer--s {
|
||||
display: initial !important;
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 574px) {
|
||||
.hideForContainer--xs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.showForContainer--xs {
|
||||
display: initial !important;
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from './translations';
|
||||
import { RuleFormPlugins } from './types';
|
||||
import './rule_form.scss';
|
||||
import { RuleFormScreenContextProvider } from './rule_form_screen_context';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
@ -117,7 +118,9 @@ export const RuleForm = (props: RuleFormProps) => {
|
|||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="ruleForm__container">{ruleFormComponent}</div>
|
||||
<RuleFormScreenContextProvider>
|
||||
<div className="ruleForm__container">{ruleFormComponent}</div>
|
||||
</RuleFormScreenContextProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './rule_form_screen_context';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
/*
|
||||
* A generic wrapper for keeping track of which screens to show on top of the Rule Form
|
||||
* This provides logic that works on both the Rule Page, which displays these screens in a modal,
|
||||
* and the Rule Flyout, which displays these screens by replacing the entire content of the flyout.
|
||||
*/
|
||||
const initialRuleFormScreenContextState = {
|
||||
isConnectorsScreenVisible: false,
|
||||
isShowRequestScreenVisible: false,
|
||||
setIsConnectorsScreenVisible: (show: boolean) => {},
|
||||
setIsShowRequestScreenVisible: (show: boolean) => {},
|
||||
};
|
||||
|
||||
export const RuleFormScreenContext = createContext(initialRuleFormScreenContextState);
|
||||
|
||||
export const RuleFormScreenContextProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const [isConnectorsScreenVisible, setIsConnectorsScreenVisible] = useState(false);
|
||||
const [isShowRequestScreenVisible, setIsShowRequestScreenVisible] = useState(false);
|
||||
return (
|
||||
<RuleFormScreenContext.Provider
|
||||
value={{
|
||||
isConnectorsScreenVisible,
|
||||
isShowRequestScreenVisible,
|
||||
setIsConnectorsScreenVisible,
|
||||
setIsShowRequestScreenVisible,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RuleFormScreenContext.Provider>
|
||||
);
|
||||
};
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { checkActionFormActionTypeEnabled } from '@kbn/alerts-ui-shared';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useRuleFormState, useRuleFormSteps } from '../hooks';
|
||||
import { useRuleFormScreenContext, useRuleFormState, useRuleFormSteps } from '../hooks';
|
||||
import {
|
||||
DISABLED_ACTIONS_WARNING_TITLE,
|
||||
RULE_FORM_CANCEL_MODAL_CANCEL,
|
||||
|
@ -32,6 +32,8 @@ import {
|
|||
import type { RuleFormData } from '../types';
|
||||
import { RulePageFooter } from './rule_page_footer';
|
||||
import { RulePageNameInput } from './rule_page_name_input';
|
||||
import { RuleActionsConnectorsModal } from '../rule_actions/rule_actions_connectors_modal';
|
||||
import { RulePageShowRequestModal } from './rule_page_show_request_modal';
|
||||
|
||||
export interface RulePageProps {
|
||||
isEdit?: boolean;
|
||||
|
@ -68,6 +70,8 @@ export const RulePage = (props: RulePageProps) => {
|
|||
}
|
||||
}, [touched, onCancel]);
|
||||
|
||||
const { isConnectorsScreenVisible, isShowRequestScreenVisible } = useRuleFormScreenContext();
|
||||
|
||||
const hasActionsDisabled = useMemo(() => {
|
||||
const preconfiguredConnectors = connectors.filter((connector) => connector.isPreconfigured);
|
||||
return actions.some((action) => {
|
||||
|
@ -149,6 +153,8 @@ export const RulePage = (props: RulePageProps) => {
|
|||
<p>{RULE_FORM_CANCEL_MODAL_DESCRIPTION}</p>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
{isConnectorsScreenVisible && <RuleActionsConnectorsModal />}
|
||||
{isShowRequestScreenVisible && <RulePageShowRequestModal isEdit={isEdit} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,16 +23,19 @@ jest.mock('../validation/validate_form', () => ({
|
|||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormScreenContext: jest.fn(),
|
||||
}));
|
||||
|
||||
const { hasRuleErrors } = jest.requireMock('../validation/validate_form');
|
||||
const { useRuleFormState } = jest.requireMock('../hooks');
|
||||
const { useRuleFormState, useRuleFormScreenContext } = jest.requireMock('../hooks');
|
||||
|
||||
const onSave = jest.fn();
|
||||
const onCancel = jest.fn();
|
||||
|
||||
hasRuleErrors.mockReturnValue(false);
|
||||
|
||||
const mockSetIsShowRequestScreenVisible = jest.fn();
|
||||
|
||||
describe('rulePageFooter', () => {
|
||||
beforeEach(() => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
|
@ -51,6 +54,9 @@ describe('rulePageFooter', () => {
|
|||
actions: [],
|
||||
},
|
||||
});
|
||||
useRuleFormScreenContext.mockReturnValue({
|
||||
setIsShowRequestScreenVisible: mockSetIsShowRequestScreenVisible,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -77,7 +83,7 @@ describe('rulePageFooter', () => {
|
|||
render(<RulePageFooter onSave={onSave} onCancel={onCancel} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('rulePageFooterShowRequestButton'));
|
||||
expect(screen.getByTestId('rulePageShowRequestModal')).toBeInTheDocument();
|
||||
expect(mockSetIsShowRequestScreenVisible).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should show create rule confirmation', () => {
|
||||
|
|
|
@ -15,9 +15,8 @@ import {
|
|||
RULE_PAGE_FOOTER_CREATE_TEXT,
|
||||
RULE_PAGE_FOOTER_SAVE_TEXT,
|
||||
} from '../translations';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import { useRuleFormScreenContext, useRuleFormState } from '../hooks';
|
||||
import { hasRuleErrors } from '../validation';
|
||||
import { RulePageShowRequestModal } from './rule_page_show_request_modal';
|
||||
import { RulePageConfirmCreateRule } from './rule_page_confirm_create_rule';
|
||||
|
||||
export interface RulePageFooterProps {
|
||||
|
@ -28,9 +27,10 @@ export interface RulePageFooterProps {
|
|||
}
|
||||
|
||||
export const RulePageFooter = (props: RulePageFooterProps) => {
|
||||
const [showRequestModal, setShowRequestModal] = useState<boolean>(false);
|
||||
const [showCreateConfirmation, setShowCreateConfirmation] = useState<boolean>(false);
|
||||
|
||||
const { setIsShowRequestScreenVisible } = useRuleFormScreenContext();
|
||||
|
||||
const { isEdit = false, isSaving = false, onCancel, onSave } = props;
|
||||
|
||||
const {
|
||||
|
@ -68,12 +68,8 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
|
|||
}, [isEdit]);
|
||||
|
||||
const onOpenShowRequestModalClick = useCallback(() => {
|
||||
setShowRequestModal(true);
|
||||
}, []);
|
||||
|
||||
const onCloseShowRequestModalClick = useCallback(() => {
|
||||
setShowRequestModal(false);
|
||||
}, []);
|
||||
setIsShowRequestScreenVisible(true);
|
||||
}, [setIsShowRequestScreenVisible]);
|
||||
|
||||
const onSaveClick = useCallback(() => {
|
||||
if (isEdit) {
|
||||
|
@ -134,9 +130,6 @@ export const RulePageFooter = (props: RulePageFooterProps) => {
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{showRequestModal && (
|
||||
<RulePageShowRequestModal onClose={onCloseShowRequestModalClick} isEdit={isEdit} />
|
||||
)}
|
||||
{showCreateConfirmation && (
|
||||
<RulePageConfirmCreateRule
|
||||
onConfirm={onCreateConfirmClick}
|
||||
|
|
|
@ -14,9 +14,10 @@ import { RuleFormData } from '../types';
|
|||
|
||||
jest.mock('../hooks', () => ({
|
||||
useRuleFormState: jest.fn(),
|
||||
useRuleFormScreenContext: jest.fn(),
|
||||
}));
|
||||
|
||||
const { useRuleFormState } = jest.requireMock('../hooks');
|
||||
const { useRuleFormState, useRuleFormScreenContext } = jest.requireMock('../hooks');
|
||||
|
||||
const formData: RuleFormData = {
|
||||
params: {
|
||||
|
@ -46,6 +47,13 @@ const formData: RuleFormData = {
|
|||
const onCloseMock = jest.fn();
|
||||
|
||||
describe('rulePageShowRequestModal', () => {
|
||||
beforeEach(() => {
|
||||
useRuleFormScreenContext.mockReturnValue({
|
||||
isShowRequestScreenVisible: false,
|
||||
setIsShowRequestScreenVisible: onCloseMock,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -53,7 +61,7 @@ describe('rulePageShowRequestModal', () => {
|
|||
test('renders create request correctly', async () => {
|
||||
useRuleFormState.mockReturnValue({ formData, multiConsumerSelection: 'logs' });
|
||||
|
||||
render(<RulePageShowRequestModal onClose={onCloseMock} />);
|
||||
render(<RulePageShowRequestModal />);
|
||||
|
||||
expect(screen.getByTestId('modalHeaderTitle').textContent).toBe('Create alerting rule request');
|
||||
expect(screen.getByTestId('modalSubtitle').textContent).toBe(
|
||||
|
@ -103,7 +111,7 @@ describe('rulePageShowRequestModal', () => {
|
|||
id: 'test-id',
|
||||
});
|
||||
|
||||
render(<RulePageShowRequestModal isEdit onClose={onCloseMock} />);
|
||||
render(<RulePageShowRequestModal isEdit />);
|
||||
|
||||
expect(screen.getByTestId('modalHeaderTitle').textContent).toBe('Edit alerting rule request');
|
||||
expect(screen.getByTestId('modalSubtitle').textContent).toBe(
|
||||
|
@ -151,7 +159,7 @@ describe('rulePageShowRequestModal', () => {
|
|||
id: 'test-id',
|
||||
});
|
||||
|
||||
render(<RulePageShowRequestModal isEdit onClose={onCloseMock} />);
|
||||
render(<RulePageShowRequestModal isEdit />);
|
||||
fireEvent.click(screen.getByLabelText('Closes this modal window'));
|
||||
expect(onCloseMock).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -7,67 +7,32 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { pick, omit } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiCodeBlock,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { BASE_ALERTING_API_PATH } from '../constants';
|
||||
import { RuleFormData } from '../types';
|
||||
import {
|
||||
CreateRuleBody,
|
||||
UPDATE_FIELDS_WITH_ACTIONS,
|
||||
UpdateRuleBody,
|
||||
transformCreateRuleBody,
|
||||
transformUpdateRuleBody,
|
||||
} from '../common/apis';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
|
||||
const stringifyBodyRequest = ({
|
||||
formData,
|
||||
isEdit,
|
||||
}: {
|
||||
formData: RuleFormData;
|
||||
isEdit: boolean;
|
||||
}): string => {
|
||||
try {
|
||||
const request = isEdit
|
||||
? transformUpdateRuleBody(pick(formData, UPDATE_FIELDS_WITH_ACTIONS) as UpdateRuleBody)
|
||||
: transformCreateRuleBody(omit(formData, 'id') as CreateRuleBody);
|
||||
return JSON.stringify(request, null, 2);
|
||||
} catch {
|
||||
return SHOW_REQUEST_MODAL_ERROR;
|
||||
}
|
||||
};
|
||||
import React, { useCallback } from 'react';
|
||||
import { RequestCodeBlock } from '../request_code_block';
|
||||
import { SHOW_REQUEST_MODAL_SUBTITLE, SHOW_REQUEST_MODAL_TITLE } from '../translations';
|
||||
import { useRuleFormScreenContext } from '../hooks';
|
||||
|
||||
export interface RulePageShowRequestModalProps {
|
||||
onClose: () => void;
|
||||
isEdit?: boolean;
|
||||
}
|
||||
|
||||
export const RulePageShowRequestModal = (props: RulePageShowRequestModalProps) => {
|
||||
const { onClose, isEdit = false } = props;
|
||||
const { isEdit = false } = props;
|
||||
const { setIsShowRequestScreenVisible } = useRuleFormScreenContext();
|
||||
|
||||
const { formData, id, multiConsumerSelection } = useRuleFormState();
|
||||
|
||||
const formattedRequest = useMemo(() => {
|
||||
return stringifyBodyRequest({
|
||||
formData: {
|
||||
...formData,
|
||||
...(multiConsumerSelection ? { consumer: multiConsumerSelection } : {}),
|
||||
},
|
||||
isEdit,
|
||||
});
|
||||
}, [formData, isEdit, multiConsumerSelection]);
|
||||
const onClose = useCallback(() => {
|
||||
setIsShowRequestScreenVisible(false);
|
||||
}, [setIsShowRequestScreenVisible]);
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
|
@ -92,61 +57,8 @@ export const RulePageShowRequestModal = (props: RulePageShowRequestModalProps) =
|
|||
</EuiFlexGroup>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiCodeBlock language="json" isCopyable data-test-subj="modalRequestCodeBlock">
|
||||
{`${isEdit ? 'PUT' : 'POST'} kbn:${BASE_ALERTING_API_PATH}/rule${
|
||||
isEdit ? `/${id}` : ''
|
||||
}\n${formattedRequest}`}
|
||||
</EuiCodeBlock>
|
||||
<RequestCodeBlock isEdit={isEdit} data-test-subj="modalRequestCodeBlock" />
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
const SHOW_REQUEST_MODAL_EDIT = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleEdit',
|
||||
{
|
||||
defaultMessage: 'edit',
|
||||
}
|
||||
);
|
||||
|
||||
const SHOW_REQUEST_MODAL_CREATE = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleCreate',
|
||||
{
|
||||
defaultMessage: 'create',
|
||||
}
|
||||
);
|
||||
|
||||
const SHOW_REQUEST_MODAL_SUBTITLE = (edit: boolean) =>
|
||||
i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitle', {
|
||||
defaultMessage: 'This Kibana request will {requestType} this rule.',
|
||||
values: { requestType: edit ? SHOW_REQUEST_MODAL_EDIT : SHOW_REQUEST_MODAL_CREATE },
|
||||
});
|
||||
|
||||
const SHOW_REQUEST_MODAL_TITLE_EDIT = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleEdit',
|
||||
{
|
||||
defaultMessage: 'Edit',
|
||||
}
|
||||
);
|
||||
|
||||
const SHOW_REQUEST_MODAL_TITLE_CREATE = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleCreate',
|
||||
{
|
||||
defaultMessage: 'Create',
|
||||
}
|
||||
);
|
||||
|
||||
const SHOW_REQUEST_MODAL_TITLE = (edit: boolean) =>
|
||||
i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.headerTitle', {
|
||||
defaultMessage: '{requestType} alerting rule request',
|
||||
values: {
|
||||
requestType: edit ? SHOW_REQUEST_MODAL_TITLE_EDIT : SHOW_REQUEST_MODAL_TITLE_CREATE,
|
||||
},
|
||||
});
|
||||
|
||||
const SHOW_REQUEST_MODAL_ERROR = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.somethingWentWrongDescription',
|
||||
{
|
||||
defaultMessage: 'Sorry about that, something went wrong.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -343,6 +343,13 @@ export const RULE_FLYOUT_HEADER_EDIT_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const RULE_FLYOUT_HEADER_BACK_TEXT = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.ruleFlyoutHeader.backText',
|
||||
{
|
||||
defaultMessage: 'Back',
|
||||
}
|
||||
);
|
||||
|
||||
export const RULE_FLYOUT_FOOTER_CANCEL_TEXT = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.ruleFlyoutFooter.cancelText',
|
||||
{
|
||||
|
@ -663,6 +670,13 @@ export const ACTION_TYPE_MODAL_FILTER_ALL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const ACTION_TYPE_MODAL_FILTER_LIST_TITLE = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.actionTypeModalFilterListTitle',
|
||||
{
|
||||
defaultMessage: 'Filter',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_TYPE_MODAL_EMPTY_TITLE = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.actionTypeModalEmptyTitle',
|
||||
{
|
||||
|
@ -730,3 +744,52 @@ export const DISABLED_ACTIONS_WARNING_TITLE = i18n.translate(
|
|||
defaultMessage: 'This rule has actions that are disabled',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_REQUEST_MODAL_EDIT = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleEdit',
|
||||
{
|
||||
defaultMessage: 'edit',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_REQUEST_MODAL_CREATE = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitleCreate',
|
||||
{
|
||||
defaultMessage: 'create',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_REQUEST_MODAL_SUBTITLE = (edit: boolean) =>
|
||||
i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.subheadingTitle', {
|
||||
defaultMessage: 'This Kibana request will {requestType} this rule.',
|
||||
values: { requestType: edit ? SHOW_REQUEST_MODAL_EDIT : SHOW_REQUEST_MODAL_CREATE },
|
||||
});
|
||||
|
||||
export const SHOW_REQUEST_MODAL_TITLE_EDIT = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleEdit',
|
||||
{
|
||||
defaultMessage: 'Edit',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_REQUEST_MODAL_TITLE_CREATE = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.headerTitleCreate',
|
||||
{
|
||||
defaultMessage: 'Create',
|
||||
}
|
||||
);
|
||||
|
||||
export const SHOW_REQUEST_MODAL_TITLE = (edit: boolean) =>
|
||||
i18n.translate('responseOpsRuleForm.ruleForm.showRequestModal.headerTitle', {
|
||||
defaultMessage: '{requestType} alerting rule request',
|
||||
values: {
|
||||
requestType: edit ? SHOW_REQUEST_MODAL_TITLE_EDIT : SHOW_REQUEST_MODAL_TITLE_CREATE,
|
||||
},
|
||||
});
|
||||
|
||||
export const SHOW_REQUEST_MODAL_ERROR = i18n.translate(
|
||||
'responseOpsRuleForm.ruleForm.showRequestModal.somethingWentWrongDescription',
|
||||
{
|
||||
defaultMessage: 'Sorry about that, something went wrong.',
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue