[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:
Kibana Machine 2025-01-23 08:00:58 +11:00 committed by GitHub
parent 14b57223c4
commit ae8be0d485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1127 additions and 650 deletions

View file

@ -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';

View file

@ -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);
};

View file

@ -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 }),

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './request_code_block';

View file

@ -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>
);
};

View file

@ -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 () => {

View file

@ -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} />
)}
</>
);
};

View file

@ -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' },
})
);
});
});

View file

@ -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>
</>
);
};

View file

@ -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();
});

View file

@ -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>
);

View file

@ -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({

View file

@ -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>
);

View file

@ -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}

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);
};

View file

@ -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;
}
}

View file

@ -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>
);
};

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export * from './rule_form_screen_context';

View file

@ -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>
);
};

View file

@ -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} />}
</>
);
};

View file

@ -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', () => {

View file

@ -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}

View file

@ -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();
});

View file

@ -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.',
}
);

View file

@ -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.',
}
);