[RAM] Fix rule definition not showing actions of the same connector type (#142533)

* Fix rule definition actions is incomplete

* Fix translation and tests

* Address comments and add back tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2022-10-06 13:39:17 -07:00 committed by GitHub
parent 45dd05037d
commit d206d8382a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 153 additions and 128 deletions

View file

@ -32638,8 +32638,6 @@
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed": "Impossible de modifier les paramètres de répétition de la règle",
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeSuccess": "Répétition de la règle activée",
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.unsnoozeSuccess": "Répétition de la règle désactivée",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex": "Actions",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip": "Impossible de charger l'un des connecteurs associés à cette règle. Modifiez la règle pour sélectionner un nouveau connecteur.",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.apiKeyOwnerTitle": "Propriétaire de la clé d'API",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel": "Supprimer",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteButtonTooltip": "Supprimer",

View file

@ -32612,8 +32612,6 @@
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed": "ルールスヌーズ設定を変更できません",
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeSuccess": "ルールが正常にスヌーズされました",
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.unsnoozeSuccess": "ルールが正常にスヌーズ解除されました",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex": "アクション",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip": "このルールに関連付けられたコネクターの1つを読み込めません。ルールを編集して、新しいコネクターを選択します。",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.apiKeyOwnerTitle": "APIキー所有者",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel": "削除",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteButtonTooltip": "削除",

View file

@ -32649,8 +32649,6 @@
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeFailed": "无法更改规则暂停设置",
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.snoozeSuccess": "已成功暂停规则",
"xpack.triggersActionsUI.sections.rulesList.rulesListSnoozePanel.unsnoozeSuccess": "已成功取消暂停规则",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex": "操作",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip": "无法加载与此规则关联的连接器之一。请编辑该规则以选择新连接器。",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.apiKeyOwnerTitle": "API 密钥所有者",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteAriaLabel": "删除",
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.deleteButtonTooltip": "删除",

View file

@ -6,7 +6,6 @@
*/
import { useEffect, useState, useCallback } from 'react';
import { intersectionBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ActionConnector, loadAllActions } from '../..';
import { useKibana } from '../../common/lib/kibana';
@ -50,11 +49,10 @@ export function useFetchRuleActionConnectors({ ruleActions }: FetchRuleActionCon
const allActions = await loadAllActions({
http,
});
const actions = intersectionBy(allActions, ruleActions, 'actionTypeId');
setActionConnector((oldState: FetchActionConnectors) => ({
...oldState,
isLoadingActionConnectors: false,
actionConnectors: actions,
actionConnectors: allActions,
}));
} catch (error) {
const errorMsg = ACTIONS_LOAD_ERROR(

View file

@ -80,4 +80,91 @@ describe('Rule Actions', () => {
expect(wrapper.find('[data-euiicon-type="index"]').length).toBe(0);
expect(wrapper.find('[data-euiicon-type="email"]').length).toBe(0);
});
it('renders multiple rule action connectors of the same type and connector', async () => {
const ruleActions = [
{
id: '1',
group: 'metrics.inventory_threshold.fired',
actionTypeId: '.server-log',
params: {},
},
{
id: '1',
group: 'metrics.inventory_threshold.fired',
actionTypeId: '.server-log',
params: {},
},
{
id: '2',
group: 'metrics.inventory_threshold.fired',
actionTypeId: '.server-log',
params: {},
},
{
id: '3',
group: 'metrics.inventory_threshold.fired',
actionTypeId: '.slack',
params: {},
},
{
id: '4',
group: 'metrics.inventory_threshold.fired',
actionTypeId: '.slack',
params: {},
},
];
mockedUseFetchRuleActionConnectorsHook.mockReturnValue({
isLoadingActionConnectors: false,
actionConnectors: [
{
id: '1',
name: 'logs1',
config: {},
actionTypeId: '.server-log',
},
{
id: '2',
name: 'logs2',
config: {},
actionTypeId: '.server-log',
},
{
id: '3',
name: 'Slack1',
actionTypeId: '.slack',
},
{
id: '4',
name: 'Slack1',
actionTypeId: '.slack',
},
] as Array<ActionConnector<Record<string, unknown>>>,
errorActionConnectors: undefined,
reloadRuleActionConnectors: jest.fn(),
});
actionTypeRegistry.list.mockReturnValue([
{ id: '.server-log', iconClass: 'logsApp' },
{ id: '.slack', iconClass: 'logoSlack' },
{ id: '.email', iconClass: 'email' },
{ id: '.index', iconClass: 'indexOpen' },
] as ActionTypeModel[]);
const wrapper = mount(
<RuleActions ruleActions={ruleActions} actionTypeRegistry={actionTypeRegistry} />
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="actionConnectorName-0-logs1"]').exists).toBeTruthy();
expect(wrapper.find('[data-test-subj="actionConnectorName-1-logs1"]').exists).toBeTruthy();
expect(wrapper.find('[data-test-subj="actionConnectorName-2-logs2"]').exists).toBeTruthy();
expect(wrapper.find('[data-test-subj="actionConnectorName-3-slack1"]').exists).toBeTruthy();
expect(wrapper.find('[data-test-subj="actionConnectorName-4-slack2"]').exists).toBeTruthy();
});
});

View file

@ -27,7 +27,11 @@ export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProp
ruleActions,
});
if (!actionConnectors || actionConnectors.length <= 0)
const hasConnectors = actionConnectors && actionConnectors.length > 0;
const hasActions = ruleActions && ruleActions.length > 0;
if (!hasConnectors || !hasActions) {
return (
<EuiFlexItem>
<EuiText size="s">
@ -37,31 +41,44 @@ export function RuleActions({ ruleActions, actionTypeRegistry }: RuleActionsProp
</EuiText>
</EuiFlexItem>
);
}
function getActionIconClass(actionGroupId?: string): IconType | undefined {
const getActionIconClass = (actionGroupId?: string): IconType | undefined => {
const actionGroup = actionTypeRegistry.list().find((group) => group.id === actionGroupId);
return typeof actionGroup?.iconClass === 'string'
? actionGroup?.iconClass
: suspendedComponentWithProps(actionGroup?.iconClass as React.ComponentType);
}
};
const getActionName = (actionTypeId?: string) => {
const actionConnector = actionConnectors.find((connector) => connector.id === actionTypeId);
return actionConnector?.name;
};
if (isLoadingActionConnectors) return <EuiLoadingSpinner size="s" />;
return (
<EuiFlexGroup direction="column" gutterSize="none">
{actionConnectors.map(({ actionTypeId, name }) => (
<EuiFlexItem key={actionTypeId}>
<EuiFlexGroup alignItems="center" gutterSize="s" component="span">
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={getActionIconClass(actionTypeId) ?? 'apps'} />
</EuiFlexItem>
<EuiFlexItem>
<EuiText data-test-subj={`actionConnectorName-${name}`} size="s">
{name}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</EuiFlexItem>
))}
{ruleActions.map(({ actionTypeId, id }, index) => {
const actionName = getActionName(id);
return (
<EuiFlexItem key={index}>
<EuiFlexGroup alignItems="center" gutterSize="s" component="span">
<EuiFlexItem grow={false}>
<EuiIcon size="m" type={getActionIconClass(actionTypeId) ?? 'apps'} />
</EuiFlexItem>
<EuiFlexItem>
<EuiText
data-test-subj={`actionConnectorName-${index}-${actionName || actionTypeId}`}
size="s"
>
{actionName}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
}

View file

@ -164,6 +164,21 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
<EuiSpacer size="m" />
<EuiFlexGroup>
<ItemTitleRuleSummary>
{i18n.translate('xpack.triggersActionsUI.ruleDetails.runsEvery', {
defaultMessage: 'Runs every',
})}
</ItemTitleRuleSummary>
<ItemValueRuleSummary
data-test-subj="ruleSummaryRuleInterval"
itemValue={formatDuration(rule.schedule.interval)}
/>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup alignItems="center">
<ItemTitleRuleSummary>
{i18n.translate('xpack.triggersActionsUI.ruleDetails.conditionsTitle', {
@ -188,20 +203,6 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup>
<ItemTitleRuleSummary>
{i18n.translate('xpack.triggersActionsUI.ruleDetails.runsEvery', {
defaultMessage: 'Runs every',
})}
</ItemTitleRuleSummary>
<ItemValueRuleSummary
data-test-subj="ruleSummaryRuleInterval"
itemValue={formatDuration(rule.schedule.interval)}
/>
</EuiFlexGroup>
<EuiSpacer size="m" />
@ -213,8 +214,8 @@ export const RuleDefinition: React.FunctionComponent<RuleDefinitionProps> = ({
</ItemTitleRuleSummary>
<ItemValueRuleSummary itemValue={String(getNotifyText())} />
</EuiFlexGroup>
<EuiSpacer size="m" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="baseline">
<ItemTitleRuleSummary>
{i18n.translate('xpack.triggersActionsUI.ruleDetails.actions', {

View file

@ -12,7 +12,7 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { act } from '@testing-library/react';
import { RuleDetails } from './rule_details';
import { Rule, ActionType, RuleTypeModel, RuleType } from '../../../../types';
import { EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiPageHeaderProps } from '@elastic/eui';
import { EuiBadge, EuiButtonEmpty, EuiPageHeaderProps } from '@elastic/eui';
import {
ActionGroup,
RuleExecutionStatusErrorReasons,
@ -211,19 +211,17 @@ describe('rule_details', () => {
},
];
const wrapper = mountWithIntl(
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={actionTypes}
{...mockRuleApis}
/>
);
expect(
mountWithIntl(
<RuleDetails
rule={rule}
ruleType={ruleType}
actionTypes={actionTypes}
{...mockRuleApis}
/>
).containsMatchingElement(
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{actionTypes[0].name}</EuiBadge>
</EuiFlexItem>
)
wrapper.find('[data-test-subj="actionConnectorName-0-Server log"]').exists
).toBeTruthy();
});
@ -275,19 +273,10 @@ describe('rule_details', () => {
);
expect(
details.containsMatchingElement(
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{actionTypes[0].name}</EuiBadge>
</EuiFlexItem>
)
details.find('[data-test-subj="actionConnectorName-0-Server log"]').exists
).toBeTruthy();
expect(
details.containsMatchingElement(
<EuiFlexItem grow={false}>
<EuiBadge color="hollow">{actionTypes[1].name}</EuiBadge>
</EuiFlexItem>
)
details.find('[data-test-subj="actionConnectorName-0-Send email"]').exists
).toBeTruthy();
});
});
@ -579,16 +568,12 @@ describe('rule_details', () => {
await nextTick();
wrapper.update();
});
const brokenConnectorIndicator = wrapper
.find('[data-test-subj="actionWithBrokenConnector"]')
.first();
const brokenConnectorWarningBanner = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]')
.first();
const brokenConnectorWarningBannerAction = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]')
.first();
expect(brokenConnectorIndicator.exists()).toBeTruthy();
expect(brokenConnectorWarningBanner.exists()).toBeTruthy();
expect(brokenConnectorWarningBannerAction.exists()).toBeTruthy();
});
@ -627,16 +612,12 @@ describe('rule_details', () => {
await nextTick();
wrapper.update();
});
const brokenConnectorIndicator = wrapper
.find('[data-test-subj="actionWithBrokenConnector"]')
.first();
const brokenConnectorWarningBanner = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]')
.first();
const brokenConnectorWarningBannerAction = wrapper
.find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]')
.first();
expect(brokenConnectorIndicator.exists()).toBeTruthy();
expect(brokenConnectorWarningBanner.exists()).toBeTruthy();
expect(brokenConnectorWarningBannerAction.exists()).toBeFalsy();
});

View file

@ -7,7 +7,6 @@
import { i18n } from '@kbn/i18n';
import React, { useState, useEffect, useReducer } from 'react';
import { keyBy } from 'lodash';
import { useHistory } from 'react-router-dom';
import {
EuiPageHeader,
@ -20,7 +19,6 @@ import {
EuiSpacer,
EuiButtonEmpty,
EuiButton,
EuiIconTip,
EuiIcon,
EuiLink,
} from '@elastic/eui';
@ -150,7 +148,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
// if the rule has actions, can the user save the rule's action params
(canExecuteActions || (!canExecuteActions && rule.actions.length === 0));
const actionTypesByTypeId = keyBy(actionTypes, 'id');
const hasEditButton =
// can the user save the rule
canSaveRule &&
@ -159,8 +156,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
? !ruleTypeRegistry.get(rule.ruleTypeId).requiresAppContext
: false);
const ruleActions = rule.actions;
const uniqueActions = Array.from(new Set(ruleActions.map((item: any) => item.actionTypeId)));
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const onRunRule = async (id: string) => {
await runRule(http, toasts, id);
@ -352,46 +347,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
</EuiFlexGroup>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
{uniqueActions && uniqueActions.length ? (
<EuiFlexGroup responsive={false} gutterSize="xs">
<EuiFlexItem>
<EuiText size="s">
<FormattedMessage
id="xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsTex"
defaultMessage="Actions"
/>{' '}
{hasActionsWithBrokenConnector && (
<EuiIconTip
data-test-subj="actionWithBrokenConnector"
type="alert"
color="danger"
content={i18n.translate(
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.actionsWarningTooltip',
{
defaultMessage:
'Unable to load one of the connectors associated with this rule. Edit the rule to select a new connector.',
}
)}
position="right"
/>
)}
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs">
{uniqueActions.map((action, index) => (
<EuiFlexItem key={index} grow={false}>
<EuiBadge color="hollow" data-test-subj="actionTypeLabel">
{actionTypesByTypeId[action].name ?? action}
</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
</EuiFlexItem>
</EuiFlexGroup>
}
rightSideItems={[

View file

@ -168,9 +168,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const owner = await pageObjects.ruleDetailsUI.getAPIKeyOwner();
expect(owner).to.be('elastic');
const { connectorType } = await pageObjects.ruleDetailsUI.getActionsLabels();
expect(connectorType).to.be(`Slack`);
});
it('renders toast when schedule is less than configured minimum', async () => {

View file

@ -24,11 +24,6 @@ export function RuleDetailsPageProvider({ getService }: FtrProviderContext) {
async getAPIKeyOwner() {
return await testSubjects.getVisibleText('apiKeyOwnerLabel');
},
async getActionsLabels() {
return {
connectorType: await testSubjects.getVisibleText('actionTypeLabel'),
};
},
async getAlertsList() {
const table = await find.byCssSelector(
'.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)'