Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -21655,7 +21655,6 @@
"xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage": "コネクターを読み込めません",
"xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage": "アクションタイプを読み込めません",
"xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage": "アラートタイプを読み込めません",
"xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle": "コネクターを読み込めません。",
"xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors": "許可されたユーザーのみがコネクターを構成できます。管理者にお問い合わせください。",
"xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ",
"xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "アラートの作成",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -21706,7 +21706,6 @@
"xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage": "无法加载连接器",
"xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage": "无法加载操作类型",
"xpack.triggersActionsUI.sections.alertForm.unableToLoadAlertTypesMessage": "无法加载告警类型",
"xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle": "无法加载连接器。",
"xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors": "只有获得授权的用户才能配置连接器。请联系您的管理员。",
"xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型",
"xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "创建告警",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,9 @@ describe('action_form', () => {
]);
expect(setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(true);
expect(wrapper.find(EuiAccordion)).toHaveLength(3);
expect(wrapper.find(`div[data-test-subj="alertActionAccordionCallout"]`)).toHaveLength(2);
expect(
wrapper.find(`EuiIconTip[data-test-subj="alertActionAccordionErrorTooltip"]`)
).toHaveLength(2);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ export const ActionForm = ({
key={`action-form-action-at-${index}`}
actionTypeRegistry={actionTypeRegistry}
emptyActionsIds={emptyActionsIds}
connectors={connectors}
onDeleteConnector={() => {
const updatedActions = actions.filter(
(_item: AlertAction, i: number) => i !== index
Expand All @@ -330,6 +331,9 @@ export const ActionForm = ({
});
setAddModalVisibility(true);
}}
onSelectConnector={(connectorId: string) => {
setActionIdByIndex(connectorId, index);
}}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Fragment } from 'react';
import React, { Fragment, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
Expand All @@ -18,38 +18,51 @@ import {
EuiEmptyPrompt,
EuiCallOut,
EuiText,
EuiFormRow,
EuiButtonEmpty,
EuiComboBox,
EuiComboBoxOptionOption,
EuiIconTip,
} from '@elastic/eui';
import { AlertAction, ActionTypeIndex } from '../../../types';
import { AlertAction, ActionTypeIndex, ActionConnector } from '../../../types';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionAccordionFormProps } from './action_form';
import { useKibana } from '../../../common/lib/kibana';

type AddConnectorInFormProps = {
actionTypesIndex: ActionTypeIndex;
actionItem: AlertAction;
connectors: ActionConnector[];
index: number;
onAddConnector: () => void;
onDeleteConnector: () => void;
onSelectConnector: (connectorId: string) => void;
emptyActionsIds: string[];
} & Pick<ActionAccordionFormProps, 'actionTypeRegistry'>;

export const AddConnectorInline = ({
actionTypesIndex,
actionItem,
index,
connectors,
onAddConnector,
onDeleteConnector,
onSelectConnector,
actionTypeRegistry,
emptyActionsIds,
}: AddConnectorInFormProps) => {
const {
application: { capabilities },
} = useKibana().services;
const canSave = hasSaveActionsCapability(capabilities);
const [connectorOptionsList, setConnectorOptionsList] = useState<EuiComboBoxOptionOption[]>([]);
const [isEmptyActionId, setIsEmptyActionId] = useState<boolean>(false);
const [errors, setErrors] = useState<string[]>([]);

const actionTypeName = actionTypesIndex
? actionTypesIndex[actionItem.actionTypeId].name
: actionItem.actionTypeId;
const actionType = actionTypesIndex[actionItem.actionTypeId];
const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId);

const noConnectorsLabel = (
Expand All @@ -61,6 +74,98 @@ export const AddConnectorInline = ({
}}
/>
);

useEffect(() => {
if (connectors) {
const altConnectorOptions = connectors
.filter(
(connector) =>
connector.actionTypeId === actionItem.actionTypeId &&
// include only enabled by config connectors or preconfigured
(actionType?.enabledInConfig || connector.isPreconfigured)
)
.map(({ name, id, isPreconfigured }) => ({
label: `${name} ${isPreconfigured ? '(preconfigured)' : ''}`,
key: id,
id,
}));
setConnectorOptionsList(altConnectorOptions);

if (altConnectorOptions.length > 0) {
setErrors([`Unable to load ${actionTypeRegistered.actionTypeTitle} connector`]);
}
}

setIsEmptyActionId(!!emptyActionsIds.find((emptyId: string) => actionItem.id === emptyId));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const connectorsDropdown = (
<EuiFlexGroup component="div">
<EuiFlexItem>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.connectorAddInline.actionIdLabel"
defaultMessage="Use another {connectorInstance} connector"
values={{
connectorInstance: actionTypeName,
}}
/>
}
labelAppend={
<EuiButtonEmpty
size="xs"
data-test-subj={`addNewActionConnectorButton-${actionItem.actionTypeId}`}
onClick={onAddConnector}
>
<FormattedMessage
defaultMessage="Add connector"
id="xpack.triggersActionsUI.sections.alertForm.connectorAddInline.addNewConnectorEmptyButton"
/>
</EuiButtonEmpty>
}
error={errors}
isInvalid={errors.length > 0}
>
<EuiComboBox
fullWidth
singleSelection={{ asPlainText: true }}
options={connectorOptionsList}
id={`selectActionConnector-${actionItem.id}-${index}`}
data-test-subj={`selectActionConnector-${actionItem.actionTypeId}-${index}`}
onChange={(selectedOptions) => {
// On selecting a option from this combo box, this component will
// be removed but the EuiComboBox performs some additional updates on
// closing the dropdown. Wrapping in a `setTimeout` to avoid `React state
// update on an unmounted component` warnings.
setTimeout(() => {
onSelectConnector(selectedOptions[0].id ?? '');
});
}}
isClearable={false}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
);

const addConnectorButton = (
<EuiButton
color="primary"
fill
size="s"
data-test-subj={`createActionConnectorButton-${index}`}
onClick={onAddConnector}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel"
defaultMessage="Create a connector"
/>
</EuiButton>
);

return (
<Fragment key={index}>
<EuiAccordion
Expand All @@ -87,6 +192,22 @@ export const AddConnectorInline = ({
</div>
</EuiText>
</EuiFlexItem>
{!isEmptyActionId && (
<EuiFlexItem grow={false}>
<EuiIconTip
type="alert"
size="m"
color="danger"
data-test-subj={`alertActionAccordionErrorTooltip`}
content={
<FormattedMessage
defaultMessage="Unable to load connector."
id="xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle'"
/>
}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
}
extraAction={
Expand All @@ -106,38 +227,15 @@ export const AddConnectorInline = ({
paddingSize="l"
>
{canSave ? (
<EuiEmptyPrompt
title={
emptyActionsIds.find((emptyId: string) => actionItem.id === emptyId) ? (
noConnectorsLabel
) : (
<EuiCallOut
data-test-subj="alertActionAccordionCallout"
title={i18n.translate(
'xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle',
{
defaultMessage: 'Unable to load connector.',
}
)}
color="warning"
/>
)
}
actions={[
<EuiButton
color="primary"
fill
size="s"
data-test-subj={`createActionConnectorButton-${index}`}
onClick={onAddConnector}
>
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel"
defaultMessage="Create a connector"
/>
</EuiButton>,
]}
/>
connectorOptionsList.length > 0 ? (
connectorsDropdown
) : isEmptyActionId ? (
<EuiEmptyPrompt title={noConnectorsLabel} actions={addConnectorButton} />
) : (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>{addConnectorButton}</EuiFlexItem>
</EuiFlexGroup>
)
) : (
<EuiCallOut title={noConnectorsLabel}>
<p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const retry = getService('retry');
const find = getService('find');
const supertest = getService('supertest');
const comboBox = getService('comboBox');
const objectRemover = new ObjectRemover(supertest);

async function createActionManualCleanup(overwrites: Record<string, any> = {}) {
Expand Down Expand Up @@ -313,15 +314,70 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('Edit alert with deleted connector', function () {
const testRunUuid = uuid.v4();

after(async () => {
afterEach(async () => {
await objectRemover.removeAll();
});

it('should show and update deleted connectors', async () => {
it('should show and update deleted connectors when there are existing connectors of the same type', async () => {
const action = await createActionManualCleanup({
name: `slack-${testRunUuid}-${0}`,
});

await pageObjects.common.navigateToApp('triggersActions');
const alert = await createAlwaysFiringAlert({
name: testRunUuid,
actions: [
{
group: 'default',
id: action.id,
params: { level: 'info', message: ' {{context.message}}' },
},
],
});

// refresh to see alert
await browser.refresh();
await pageObjects.header.waitUntilLoadingHasFinished();

// verify content
await testSubjects.existOrFail('alertsList');

// delete connector
await pageObjects.triggersActionsUI.changeTabs('connectorsTab');
await pageObjects.triggersActionsUI.searchConnectors(action.name);
await testSubjects.click('deleteConnector');
await testSubjects.existOrFail('deleteIdsConfirmation');
await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton');
await testSubjects.missingOrFail('deleteIdsConfirmation');

const toastTitle = await pageObjects.common.closeToast();
expect(toastTitle).to.eql('Deleted 1 connector');

// click on first alert
await pageObjects.triggersActionsUI.changeTabs('alertsTab');
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);

const editButton = await testSubjects.find('openEditAlertFlyoutButton');
await editButton.click();
expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false);

expect(await testSubjects.exists('addNewActionConnectorActionGroup-0')).to.eql(false);
expect(await testSubjects.exists('alertActionAccordion-0')).to.eql(true);

await comboBox.set('selectActionConnector-.slack-0', 'Slack#xyztest (preconfigured)');
expect(await testSubjects.exists('addNewActionConnectorActionGroup-0')).to.eql(true);
});

it('should show and update deleted connectors when there are no existing connectors of the same type', async () => {
const action = await createActionManualCleanup({
name: `index-${testRunUuid}-${0}`,
actionTypeId: '.index',
config: {
index: `index-${testRunUuid}-${0}`,
},
secrets: {},
});

await pageObjects.common.navigateToApp('triggersActions');
const alert = await createAlwaysFiringAlert({
name: testRunUuid,
Expand Down Expand Up @@ -373,7 +429,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('createActionConnectorButton-0');
await testSubjects.existOrFail('connectorAddModal');
await testSubjects.setValue('nameInput', 'new connector');
await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
await retry.try(async () => {
// At times we find the driver controlling the ComboBox in tests
// can select the wrong item, this ensures we always select the correct index
await comboBox.set('connectorIndexesComboBox', 'test-index');
expect(
await comboBox.isOptionSelected(
await testSubjects.find('connectorIndexesComboBox'),
'test-index'
)
).to.be(true);
});
await testSubjects.click('connectorAddModal > saveActionButtonModal');
await testSubjects.missingOrFail('deleteIdsConfirmation');

Expand Down