Skip to content

Commit

Permalink
[Alerting] Allow user to select existing connector of same type when …
Browse files Browse the repository at this point in the history
…fixing broken connector (elastic#89062)

* Adding dropdown for selecting different connector of same type

* Updating design

* Cleanup and i18n

* Adding functiional test

* Fixing unit test

* Fixing functional test

* Updating design

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
ymao1 and kibanamachine committed Feb 2, 2021
1 parent 2273d9e commit 5114ce5
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 39 deletions.
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);
});
});
});
Expand Up @@ -318,6 +318,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 @@ -340,6 +341,9 @@ export const ActionForm = ({
});
setAddModalVisibility(true);
}}
onSelectConnector={(connectorId: string) => {
setActionIdByIndex(connectorId, index);
}}
/>
);
}
Expand Down
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,92 @@ export const AddConnectorInline = ({
}}
/>
);

const unableToLoadConnectorLabel = (
<EuiText color="danger">
<FormattedMessage
id="xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle"
defaultMessage="Unable to load connector."
/>
</EuiText>
);

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

return (
<Fragment key={index}>
<EuiAccordion
Expand All @@ -87,6 +186,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 +221,27 @@ 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
) : (
<EuiEmptyPrompt
title={isEmptyActionId ? noConnectorsLabel : unableToLoadConnectorLabel}
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>
}
/>
)
) : (
<EuiCallOut title={noConnectorsLabel}>
<p>
Expand Down
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
Expand Up @@ -10,9 +10,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
describe('Actions and Triggers app', function () {
this.tags('ciGroup10');
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./connectors'));
loadTestFile(require.resolve('./alerts_list'));
loadTestFile(require.resolve('./alert_create_flyout'));
loadTestFile(require.resolve('./details'));
loadTestFile(require.resolve('./connectors'));
});
};

0 comments on commit 5114ce5

Please sign in to comment.