Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Alerting] Allow user to select existing connector of same type when fixing broken connector #89062

Merged
merged 16 commits into from Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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
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'));
});
};