mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Actions] adds a Test Connector tab in the Connectors list (#77365)
Adds a tab in the _Edit Alert_ flyout which allows the user to _test_ their connector by executing it using an example action. The execution relies on the connector being updated, so is only enabled when there are no saved changes in the Connector form itself.
This commit is contained in:
parent
d3dd50520b
commit
41a7f1a1c2
15 changed files with 932 additions and 202 deletions
|
@ -24,3 +24,13 @@ export interface ActionResult {
|
|||
config: Record<string, any>;
|
||||
isPreconfigured: boolean;
|
||||
}
|
||||
|
||||
// the result returned from an action type executor function
|
||||
export interface ActionTypeExecutorResult<Data> {
|
||||
actionId: string;
|
||||
status: 'ok' | 'error';
|
||||
message?: string;
|
||||
serviceMessage?: string;
|
||||
data?: Data;
|
||||
retry?: null | boolean | Date;
|
||||
}
|
||||
|
|
|
@ -284,4 +284,47 @@ describe('execute()', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('resolves with an error when an error occurs in the indexing operation', async () => {
|
||||
const secrets = {};
|
||||
// minimal params
|
||||
const config = { index: 'index-value', refresh: false, executionTimeField: null };
|
||||
const params = {
|
||||
documents: [{ '': 'bob' }],
|
||||
};
|
||||
|
||||
const actionId = 'some-id';
|
||||
|
||||
services.callCluster.mockResolvedValue({
|
||||
took: 0,
|
||||
errors: true,
|
||||
items: [
|
||||
{
|
||||
index: {
|
||||
_index: 'indexme',
|
||||
_id: '7buTjHQB0SuNSiS9Hayt',
|
||||
status: 400,
|
||||
error: {
|
||||
type: 'mapper_parsing_exception',
|
||||
reason: 'failed to parse',
|
||||
caused_by: {
|
||||
type: 'illegal_argument_exception',
|
||||
reason: 'field name cannot be an empty string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(await actionType.executor({ actionId, config, secrets, params, services }))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actionId": "some-id",
|
||||
"message": "error indexing documents",
|
||||
"serviceMessage": "failed to parse (field name cannot be an empty string)",
|
||||
"status": "error",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { curry } from 'lodash';
|
||||
import { curry, find } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
|
@ -85,21 +85,39 @@ async function executor(
|
|||
refresh: config.refresh,
|
||||
};
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await services.callCluster('bulk', bulkParams);
|
||||
} catch (err) {
|
||||
const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', {
|
||||
defaultMessage: 'error indexing documents',
|
||||
});
|
||||
logger.error(`error indexing documents: ${err.message}`);
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
serviceMessage: err.message,
|
||||
};
|
||||
}
|
||||
const result = await services.callCluster('bulk', bulkParams);
|
||||
|
||||
return { status: 'ok', data: result, actionId };
|
||||
const err = find(result.items, 'index.error.reason');
|
||||
if (err) {
|
||||
return wrapErr(
|
||||
`${err.index.error!.reason}${
|
||||
err.index.error?.caused_by ? ` (${err.index.error?.caused_by?.reason})` : ''
|
||||
}`,
|
||||
actionId,
|
||||
logger
|
||||
);
|
||||
}
|
||||
|
||||
return { status: 'ok', data: result, actionId };
|
||||
} catch (err) {
|
||||
return wrapErr(err.message, actionId, logger);
|
||||
}
|
||||
}
|
||||
|
||||
function wrapErr(
|
||||
errMessage: string,
|
||||
actionId: string,
|
||||
logger: Logger
|
||||
): ActionTypeExecutorResult<unknown> {
|
||||
const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', {
|
||||
defaultMessage: 'error indexing documents',
|
||||
});
|
||||
logger.error(`error indexing documents: ${errMessage}`);
|
||||
return {
|
||||
status: 'error',
|
||||
actionId,
|
||||
message,
|
||||
serviceMessage: errMessage,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
SavedObjectsClientContract,
|
||||
SavedObjectAttributes,
|
||||
} from '../../../../src/core/server';
|
||||
import { ActionTypeExecutorResult } from '../common';
|
||||
export { ActionTypeExecutorResult } from '../common';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
export type GetServicesFunction = (request: KibanaRequest) => Services;
|
||||
|
@ -80,16 +82,6 @@ export interface FindActionResult extends ActionResult {
|
|||
referencedByCount: number;
|
||||
}
|
||||
|
||||
// the result returned from an action type executor function
|
||||
export interface ActionTypeExecutorResult<Data> {
|
||||
actionId: string;
|
||||
status: 'ok' | 'error';
|
||||
message?: string;
|
||||
serviceMessage?: string;
|
||||
data?: Data;
|
||||
retry?: null | boolean | Date;
|
||||
}
|
||||
|
||||
// signature of the action type executor function
|
||||
export type ExecutorType<Config, Secrets, Params, ResultData> = (
|
||||
options: ActionTypeExecutorOptions<Config, Secrets, Params>
|
||||
|
|
|
@ -61,6 +61,7 @@ export const AddMessageVariables: React.FunctionComponent<Props> = ({
|
|||
<EuiButtonIcon
|
||||
id={`${paramsProperty}AddVariableButton`}
|
||||
data-test-subj={`${paramsProperty}AddVariableButton`}
|
||||
isDisabled={(messageVariables?.length ?? 0) === 0}
|
||||
title={addVariableButtonTitle}
|
||||
onClick={() => setIsVariablesPopoverOpen(true)}
|
||||
iconType="indexOpen"
|
||||
|
|
|
@ -32,48 +32,47 @@ export const IndexParamsFields = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonEditorWithMessageVariables
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'documents'}
|
||||
inputTargetValue={
|
||||
documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined
|
||||
<JsonEditorWithMessageVariables
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'documents'}
|
||||
data-test-subj="documentToIndex"
|
||||
inputTargetValue={
|
||||
documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Document to index',
|
||||
}
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Document to index',
|
||||
}
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Code editor',
|
||||
}
|
||||
)}
|
||||
errors={errors.documents as string[]}
|
||||
onDocumentsChange={onDocumentsChange}
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/index-action-type.html#index-action-configuration`}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel"
|
||||
defaultMessage="Index document example."
|
||||
/>
|
||||
</EuiLink>
|
||||
)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Code editor',
|
||||
}
|
||||
onBlur={() => {
|
||||
if (
|
||||
!(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined)
|
||||
) {
|
||||
// set document as empty to turn on the validation for non empty valid JSON object
|
||||
onDocumentsChange('{}');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
errors={errors.documents as string[]}
|
||||
onDocumentsChange={onDocumentsChange}
|
||||
helpText={
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/index-action-type.html#index-action-configuration`}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel"
|
||||
defaultMessage="Index document example."
|
||||
/>
|
||||
</EuiLink>
|
||||
}
|
||||
onBlur={() => {
|
||||
if (
|
||||
!(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined)
|
||||
) {
|
||||
// set document as empty to turn on the validation for non empty valid JSON object
|
||||
onDocumentsChange('{}');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
loadActionTypes,
|
||||
loadAllActions,
|
||||
updateActionConnector,
|
||||
executeAction,
|
||||
} from './action_connector_api';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
@ -128,3 +129,32 @@ describe('deleteActions', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeAction', () => {
|
||||
test('should call execute API', async () => {
|
||||
const id = '123';
|
||||
const params = {
|
||||
stringParams: 'someString',
|
||||
numericParams: 123,
|
||||
};
|
||||
|
||||
http.post.mockResolvedValueOnce({
|
||||
actionId: id,
|
||||
status: 'ok',
|
||||
});
|
||||
|
||||
const result = await executeAction({ id, http, params });
|
||||
expect(result).toEqual({
|
||||
actionId: id,
|
||||
status: 'ok',
|
||||
});
|
||||
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"/api/actions/action/123/_execute",
|
||||
Object {
|
||||
"body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { HttpSetup } from 'kibana/public';
|
||||
import { BASE_ACTION_API_PATH } from '../constants';
|
||||
import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types';
|
||||
import { ActionTypeExecutorResult } from '../../../../../plugins/actions/common';
|
||||
|
||||
export async function loadActionTypes({ http }: { http: HttpSetup }): Promise<ActionType[]> {
|
||||
return await http.get(`${BASE_ACTION_API_PATH}/list_action_types`);
|
||||
|
@ -65,3 +66,17 @@ export async function deleteActions({
|
|||
);
|
||||
return { successes, errors };
|
||||
}
|
||||
|
||||
export async function executeAction({
|
||||
id,
|
||||
params,
|
||||
http,
|
||||
}: {
|
||||
id: string;
|
||||
http: HttpSetup;
|
||||
params: Record<string, unknown>;
|
||||
}): Promise<ActionTypeExecutorResult<unknown>> {
|
||||
return await http.post(`${BASE_ACTION_API_PATH}/action/${id}/_execute`, {
|
||||
body: JSON.stringify({ params }),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.connectorEditFlyoutTabs {
|
||||
margin-bottom: '-25px';
|
||||
}
|
|
@ -152,6 +152,6 @@ describe('connector_edit_flyout', () => {
|
|||
|
||||
const preconfiguredBadge = wrapper.find('[data-test-subj="preconfiguredBadge"]');
|
||||
expect(preconfiguredBadge.exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="saveEditedActionButton"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="saveAndCloseEditedActionButton"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,15 +19,21 @@ import {
|
|||
EuiBetaBadge,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Option, none, some } from 'fp-ts/lib/Option';
|
||||
import { ActionConnectorForm, validateBaseProperties } from './action_connector_form';
|
||||
import { TestConnectorForm } from './test_connector_form';
|
||||
import { ActionConnectorTableItem, ActionConnector, IErrorObject } from '../../../types';
|
||||
import { connectorReducer } from './connector_reducer';
|
||||
import { updateActionConnector } from '../../lib/action_connector_api';
|
||||
import { updateActionConnector, executeAction } from '../../lib/action_connector_api';
|
||||
import { hasSaveActionsCapability } from '../../lib/capabilities';
|
||||
import { useActionsConnectorsContext } from '../../context/actions_connectors_context';
|
||||
import { PLUGIN } from '../../constants/plugin';
|
||||
import { ActionTypeExecutorResult } from '../../../../../actions/common';
|
||||
import './connector_edit_flyout.scss';
|
||||
|
||||
export interface ConnectorEditProps {
|
||||
initialConnector: ActionConnectorTableItem;
|
||||
|
@ -40,7 +46,6 @@ export const ConnectorEditFlyout = ({
|
|||
editFlyoutVisible,
|
||||
setEditFlyoutVisibility,
|
||||
}: ConnectorEditProps) => {
|
||||
let hasErrors = false;
|
||||
const {
|
||||
http,
|
||||
toastNotifications,
|
||||
|
@ -56,13 +61,26 @@ export const ConnectorEditFlyout = ({
|
|||
connector: { ...initialConnector, secrets: {} },
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [selectedTab, setTab] = useState<'config' | 'test'>('config');
|
||||
|
||||
const [hasChanges, setHasChanges] = useState<boolean>(false);
|
||||
const setConnector = (key: string, value: any) => {
|
||||
dispatch({ command: { type: 'setConnector' }, payload: { key, value } });
|
||||
};
|
||||
|
||||
const [testExecutionActionParams, setTestExecutionActionParams] = useState<
|
||||
Record<string, unknown>
|
||||
>({});
|
||||
const [testExecutionResult, setTestExecutionResult] = useState<
|
||||
Option<ActionTypeExecutorResult<unknown>>
|
||||
>(none);
|
||||
const [isExecutingAction, setIsExecutinAction] = useState<boolean>(false);
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
setEditFlyoutVisibility(false);
|
||||
setConnector('connector', { ...initialConnector, secrets: {} });
|
||||
setHasChanges(false);
|
||||
setTestExecutionResult(none);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [setEditFlyoutVisibility]);
|
||||
|
||||
|
@ -71,11 +89,13 @@ export const ConnectorEditFlyout = ({
|
|||
}
|
||||
|
||||
const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId);
|
||||
const errors = {
|
||||
const errorsInConnectorConfig = {
|
||||
...actionTypeModel?.validateConnector(connector).errors,
|
||||
...validateBaseProperties(connector).errors,
|
||||
} as IErrorObject;
|
||||
hasErrors = !!Object.keys(errors).find((errorKey) => errors[errorKey].length >= 1);
|
||||
const hasErrorsInConnectorConfig = !!Object.keys(errorsInConnectorConfig).find(
|
||||
(errorKey) => errorsInConnectorConfig[errorKey].length >= 1
|
||||
);
|
||||
|
||||
const onActionConnectorSave = async (): Promise<ActionConnector | undefined> =>
|
||||
await updateActionConnector({ http, connector, id: connector.id })
|
||||
|
@ -173,6 +193,32 @@ export const ConnectorEditFlyout = ({
|
|||
</EuiTitle>
|
||||
);
|
||||
|
||||
const onExecutAction = () => {
|
||||
setIsExecutinAction(true);
|
||||
return executeAction({ id: connector.id, params: testExecutionActionParams, http }).then(
|
||||
(result) => {
|
||||
setIsExecutinAction(false);
|
||||
setTestExecutionResult(some(result));
|
||||
return result;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onSaveClicked = async (closeAfterSave: boolean = true) => {
|
||||
setIsSaving(true);
|
||||
const savedAction = await onActionConnectorSave();
|
||||
setIsSaving(false);
|
||||
if (savedAction) {
|
||||
setHasChanges(false);
|
||||
if (closeAfterSave) {
|
||||
closeFlyout();
|
||||
}
|
||||
if (reloadConnectors) {
|
||||
reloadConnectors();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={closeFlyout} aria-labelledby="flyoutActionEditTitle" size="m">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
|
@ -184,40 +230,78 @@ export const ConnectorEditFlyout = ({
|
|||
) : null}
|
||||
<EuiFlexItem>{flyoutTitle}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiTabs className="connectorEditFlyoutTabs">
|
||||
<EuiTab
|
||||
onClick={() => setTab('config')}
|
||||
data-test-subj="configureConnectorTab"
|
||||
isSelected={'config' === selectedTab}
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.editConnectorForm.tabText', {
|
||||
defaultMessage: 'Configuration',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
onClick={() => setTab('test')}
|
||||
data-test-subj="testConnectorTab"
|
||||
isSelected={'test' === selectedTab}
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', {
|
||||
defaultMessage: 'Test',
|
||||
})}
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{!connector.isPreconfigured ? (
|
||||
<ActionConnectorForm
|
||||
connector={connector}
|
||||
errors={errors}
|
||||
actionTypeName={connector.actionType}
|
||||
dispatch={dispatch}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
http={http}
|
||||
docLinks={docLinks}
|
||||
capabilities={capabilities}
|
||||
consumer={consumer}
|
||||
/>
|
||||
{selectedTab === 'config' ? (
|
||||
!connector.isPreconfigured ? (
|
||||
<ActionConnectorForm
|
||||
connector={connector}
|
||||
errors={errorsInConnectorConfig}
|
||||
actionTypeName={connector.actionType}
|
||||
dispatch={(changes) => {
|
||||
setHasChanges(true);
|
||||
// if the user changes the connector, "forget" the last execution
|
||||
// so the user comes back to a clean form ready to run a fresh test
|
||||
setTestExecutionResult(none);
|
||||
dispatch(changes);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
http={http}
|
||||
docLinks={docLinks}
|
||||
capabilities={capabilities}
|
||||
consumer={consumer}
|
||||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText',
|
||||
{
|
||||
defaultMessage: 'This connector is readonly.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/pre-configured-action-types-and-connectors.html`}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel"
|
||||
defaultMessage="Learn more about preconfigured connectors."
|
||||
/>
|
||||
</EuiLink>
|
||||
</Fragment>
|
||||
)
|
||||
) : (
|
||||
<Fragment>
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText',
|
||||
{
|
||||
defaultMessage: 'This connector is readonly.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/pre-configured-action-types-and-connectors.html`}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel"
|
||||
defaultMessage="Learn more about preconfigured connectors."
|
||||
/>
|
||||
</EuiLink>
|
||||
</Fragment>
|
||||
<TestConnectorForm
|
||||
connector={connector}
|
||||
executeEnabled={!hasChanges}
|
||||
actionParams={testExecutionActionParams}
|
||||
setActionParams={setTestExecutionActionParams}
|
||||
onExecutAction={onExecutAction}
|
||||
isExecutingAction={isExecutingAction}
|
||||
executionResult={testExecutionResult}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
|
@ -232,35 +316,48 @@ export const ConnectorEditFlyout = ({
|
|||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{canSave && actionTypeModel && !connector.isPreconfigured ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
data-test-subj="saveEditedActionButton"
|
||||
type="submit"
|
||||
iconType="check"
|
||||
isDisabled={hasErrors}
|
||||
isLoading={isSaving}
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
const savedAction = await onActionConnectorSave();
|
||||
setIsSaving(false);
|
||||
if (savedAction) {
|
||||
closeFlyout();
|
||||
if (reloadConnectors) {
|
||||
reloadConnectors();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
{canSave && actionTypeModel && !connector.isPreconfigured ? (
|
||||
<Fragment>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
color="secondary"
|
||||
data-test-subj="saveEditedActionButton"
|
||||
isDisabled={hasErrorsInConnectorConfig || !hasChanges}
|
||||
isLoading={isSaving || isExecutingAction}
|
||||
onClick={async () => {
|
||||
await onSaveClicked(false);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
data-test-subj="saveAndCloseEditedActionButton"
|
||||
type="submit"
|
||||
isDisabled={hasErrorsInConnectorConfig || !hasChanges}
|
||||
isLoading={isSaving || isExecutingAction}
|
||||
onClick={async () => {
|
||||
await onSaveClicked();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.editConnectorForm.saveAndCloseButtonLabel"
|
||||
defaultMessage="Save & Close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { lazy } from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import TestConnectorForm from './test_connector_form';
|
||||
import { none, some } from 'fp-ts/lib/Option';
|
||||
import { ActionConnector, ValidationResult } from '../../../types';
|
||||
import { actionTypeRegistryMock } from '../../action_type_registry.mock';
|
||||
import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context';
|
||||
import { EuiFormRow, EuiFieldText, EuiText, EuiLink, EuiForm, EuiSelect } from '@elastic/eui';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
const mockedActionParamsFields = lazy(async () => ({
|
||||
default() {
|
||||
return (
|
||||
<EuiForm component="form">
|
||||
<EuiFormRow label="Text field" helpText="I am some friendly help text.">
|
||||
<EuiFieldText data-test-subj="testInputField" />
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label="Select (with no initial selection)"
|
||||
labelAppend={
|
||||
<EuiText size="xs">
|
||||
<EuiLink>Link to some help</EuiLink>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
hasNoInitialSelection
|
||||
options={[
|
||||
{ value: 'option_one', text: 'Option one' },
|
||||
{ value: 'option_two', text: 'Option two' },
|
||||
{ value: 'option_three', text: 'Option three' },
|
||||
]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const actionType = {
|
||||
id: 'my-action-type',
|
||||
iconClass: 'test',
|
||||
selectMessage: 'test',
|
||||
validateConnector: (): ValidationResult => {
|
||||
return { errors: {} };
|
||||
},
|
||||
validateParams: (): ValidationResult => {
|
||||
const validationResult = { errors: {} };
|
||||
return validationResult;
|
||||
},
|
||||
actionConnectorFields: null,
|
||||
actionParamsFields: mockedActionParamsFields,
|
||||
};
|
||||
|
||||
describe('test_connector_form', () => {
|
||||
let deps: any;
|
||||
let actionTypeRegistry;
|
||||
beforeAll(async () => {
|
||||
actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
|
||||
const mocks = coreMock.createSetup();
|
||||
const [
|
||||
{
|
||||
application: { capabilities },
|
||||
},
|
||||
] = await mocks.getStartServices();
|
||||
deps = {
|
||||
http: mocks.http,
|
||||
toastNotifications: mocks.notifications.toasts,
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||
actionTypeRegistry,
|
||||
capabilities,
|
||||
};
|
||||
actionTypeRegistry.get.mockReturnValue(actionType);
|
||||
});
|
||||
|
||||
it('renders initially as the action form and execute button and no result', async () => {
|
||||
const connector = {
|
||||
actionTypeId: actionType.id,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as ActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<I18nProvider>
|
||||
<ActionsConnectorsContextProvider
|
||||
value={{
|
||||
http: deps!.http,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
capabilities: deps!.capabilities,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
reloadConnectors: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
docLinks: deps!.docLinks,
|
||||
}}
|
||||
>
|
||||
<TestConnectorForm
|
||||
connector={connector}
|
||||
executeEnabled={true}
|
||||
actionParams={{}}
|
||||
setActionParams={() => {}}
|
||||
isExecutingAction={false}
|
||||
onExecutAction={async () => ({
|
||||
actionId: '',
|
||||
status: 'ok',
|
||||
})}
|
||||
executionResult={none}
|
||||
/>
|
||||
</ActionsConnectorsContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
const executeActionButton = wrapper?.find('[data-test-subj="executeActionButton"]');
|
||||
expect(executeActionButton?.exists()).toBeTruthy();
|
||||
expect(executeActionButton?.first().prop('isDisabled')).toBe(false);
|
||||
|
||||
const result = wrapper?.find('[data-test-subj="executionAwaiting"]');
|
||||
expect(result?.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders successful results', async () => {
|
||||
const connector = {
|
||||
actionTypeId: actionType.id,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as ActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<I18nProvider>
|
||||
<ActionsConnectorsContextProvider
|
||||
value={{
|
||||
http: deps!.http,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
capabilities: deps!.capabilities,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
reloadConnectors: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
docLinks: deps!.docLinks,
|
||||
}}
|
||||
>
|
||||
<TestConnectorForm
|
||||
connector={connector}
|
||||
executeEnabled={true}
|
||||
actionParams={{}}
|
||||
setActionParams={() => {}}
|
||||
isExecutingAction={false}
|
||||
onExecutAction={async () => ({
|
||||
actionId: '',
|
||||
status: 'ok',
|
||||
})}
|
||||
executionResult={some({
|
||||
actionId: '',
|
||||
status: 'ok',
|
||||
})}
|
||||
/>
|
||||
</ActionsConnectorsContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
const result = wrapper?.find('[data-test-subj="executionSuccessfulResult"]');
|
||||
expect(result?.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders failure results', async () => {
|
||||
const connector = {
|
||||
actionTypeId: actionType.id,
|
||||
config: {},
|
||||
secrets: {},
|
||||
} as ActionConnector;
|
||||
const wrapper = mountWithIntl(
|
||||
<I18nProvider>
|
||||
<ActionsConnectorsContextProvider
|
||||
value={{
|
||||
http: deps!.http,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
capabilities: deps!.capabilities,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
reloadConnectors: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
docLinks: deps!.docLinks,
|
||||
}}
|
||||
>
|
||||
<TestConnectorForm
|
||||
connector={connector}
|
||||
executeEnabled={true}
|
||||
actionParams={{}}
|
||||
setActionParams={() => {}}
|
||||
isExecutingAction={false}
|
||||
onExecutAction={async () => ({
|
||||
actionId: '',
|
||||
status: 'error',
|
||||
message: 'Error Message',
|
||||
})}
|
||||
executionResult={some({
|
||||
actionId: '',
|
||||
status: 'error',
|
||||
message: 'Error Message',
|
||||
})}
|
||||
/>
|
||||
</ActionsConnectorsContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
const result = wrapper?.find('[data-test-subj="executionFailureResult"]');
|
||||
expect(result?.exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment, Suspense } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiButton,
|
||||
EuiSteps,
|
||||
EuiLoadingSpinner,
|
||||
EuiDescriptionList,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { Option, map, getOrElse } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionConnector } from '../../../types';
|
||||
import { useActionsConnectorsContext } from '../../context/actions_connectors_context';
|
||||
import { ActionTypeExecutorResult } from '../../../../../actions/common';
|
||||
|
||||
export interface ConnectorAddFlyoutProps {
|
||||
connector: ActionConnector;
|
||||
executeEnabled: boolean;
|
||||
isExecutingAction: boolean;
|
||||
setActionParams: (params: Record<string, unknown>) => void;
|
||||
actionParams: Record<string, unknown>;
|
||||
onExecutAction: () => Promise<ActionTypeExecutorResult<unknown>>;
|
||||
executionResult: Option<ActionTypeExecutorResult<unknown>>;
|
||||
}
|
||||
|
||||
export const TestConnectorForm = ({
|
||||
connector,
|
||||
executeEnabled,
|
||||
executionResult,
|
||||
actionParams,
|
||||
setActionParams,
|
||||
onExecutAction,
|
||||
isExecutingAction,
|
||||
}: ConnectorAddFlyoutProps) => {
|
||||
const { actionTypeRegistry, docLinks } = useActionsConnectorsContext();
|
||||
const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId);
|
||||
const ParamsFieldsComponent = actionTypeModel.actionParamsFields;
|
||||
|
||||
const actionErrors = actionTypeModel?.validateParams(actionParams);
|
||||
const hasErrors = !!Object.values(actionErrors.errors).find((errors) => errors.length > 0);
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Create an action',
|
||||
children: ParamsFieldsComponent ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<ParamsFieldsComponent
|
||||
actionParams={actionParams}
|
||||
index={0}
|
||||
errors={actionErrors.errors}
|
||||
editAction={(field, value) =>
|
||||
setActionParams({
|
||||
...actionParams,
|
||||
[field]: value,
|
||||
})
|
||||
}
|
||||
messageVariables={[]}
|
||||
docLinks={docLinks}
|
||||
actionConnector={connector}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<EuiText>
|
||||
<p>This Connector does not require any Action Parameter.</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Run the action',
|
||||
children: (
|
||||
<Fragment>
|
||||
{executeEnabled ? null : (
|
||||
<Fragment>
|
||||
<EuiCallOut iconType="alert" color="warning">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Save your changes before testing the connector."
|
||||
id="xpack.triggersActionsUI.sections.testConnectorForm.executeTestDisabled"
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
)}
|
||||
<EuiText>
|
||||
<EuiButton
|
||||
iconType={'play'}
|
||||
isLoading={isExecutingAction}
|
||||
isDisabled={!executeEnabled || hasErrors || isExecutingAction}
|
||||
data-test-subj="executeActionButton"
|
||||
onClick={onExecutAction}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Run"
|
||||
id="xpack.triggersActionsUI.sections.testConnectorForm.executeTestButton"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Results',
|
||||
children: pipe(
|
||||
executionResult,
|
||||
map((result) =>
|
||||
result.status === 'ok' ? (
|
||||
<SuccessfulExecution />
|
||||
) : (
|
||||
<FailedExecussion executionResult={result} />
|
||||
)
|
||||
),
|
||||
getOrElse(() => <AwaitingExecution />)
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return <EuiSteps steps={steps} />;
|
||||
};
|
||||
|
||||
const AwaitingExecution = () => (
|
||||
<EuiCallOut data-test-subj="executionAwaiting">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="When you run the action, the results will show up here."
|
||||
id="xpack.triggersActionsUI.sections.testConnectorForm.awaitingExecutionDescription"
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
||||
const SuccessfulExecution = () => (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.testConnectorForm.executionSuccessfulTitle',
|
||||
{
|
||||
defaultMessage: 'Action was successful',
|
||||
values: {},
|
||||
}
|
||||
)}
|
||||
color="success"
|
||||
data-test-subj="executionSuccessfulResult"
|
||||
iconType="check"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ensure the results are what you expect."
|
||||
id="xpack.triggersActionsUI.sections.testConnectorForm.executionSuccessfulDescription"
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
|
||||
const FailedExecussion = ({
|
||||
executionResult: { message, serviceMessage },
|
||||
}: {
|
||||
executionResult: ActionTypeExecutorResult<unknown>;
|
||||
}) => {
|
||||
const items = [
|
||||
{
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureDescription',
|
||||
{
|
||||
defaultMessage: 'The following error was found:',
|
||||
}
|
||||
),
|
||||
description:
|
||||
message ??
|
||||
i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureUnknownReason',
|
||||
{
|
||||
defaultMessage: 'Unknown reason',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
if (serviceMessage) {
|
||||
items.push({
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureAdditionalDetails',
|
||||
{
|
||||
defaultMessage: 'Details:',
|
||||
}
|
||||
),
|
||||
description: serviceMessage,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureTitle',
|
||||
{
|
||||
defaultMessage: 'Action failed to run',
|
||||
}
|
||||
)}
|
||||
data-test-subj="executionFailureResult"
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<EuiDescriptionList textStyle="reverse" listItems={items} />
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { TestConnectorForm as default };
|
|
@ -194,55 +194,15 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'isPreconfigured',
|
||||
name: '',
|
||||
render: (value: number, item: ActionConnectorTableItem) => {
|
||||
if (item.isPreconfigured) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
data-test-subj="preConfiguredTitleMessage"
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.preconfiguredTitleMessage',
|
||||
{
|
||||
defaultMessage: 'Preconfigured',
|
||||
}
|
||||
)}
|
||||
tooltipContent="This connector can't be deleted."
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
render: (item: ActionConnectorTableItem) => {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
canDelete
|
||||
? i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription',
|
||||
{ defaultMessage: 'Delete this connector' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription',
|
||||
{ defaultMessage: 'Unable to delete connectors' }
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
isDisabled={!canDelete}
|
||||
data-test-subj="deleteConnector"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName',
|
||||
{ defaultMessage: 'Delete' }
|
||||
)}
|
||||
onClick={() => setConnectorsToDelete([item.id])}
|
||||
iconType={'trash'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<DeleteOperation
|
||||
canDelete={canDelete}
|
||||
item={item}
|
||||
onDelete={() => setConnectorsToDelete([item.id])}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
|
@ -344,28 +304,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const noPermissionPrompt = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle"
|
||||
defaultMessage="No permissions to create connectors"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<p data-test-subj="permissionDeniedMessage">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateDescription"
|
||||
defaultMessage="Contact your system administrator."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<section data-test-subj="actionsList">
|
||||
<DeleteModalConfirmation
|
||||
|
@ -411,7 +349,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
{data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && (
|
||||
<EmptyConnectorsPrompt onCTAClicked={() => setAddFlyoutVisibility(true)} />
|
||||
)}
|
||||
{data.length === 0 && !canSave && noPermissionPrompt}
|
||||
{data.length === 0 && !canSave && <NoPermissionPrompt />}
|
||||
<ActionsConnectorsContextProvider
|
||||
value={{
|
||||
actionTypeRegistry,
|
||||
|
@ -442,3 +380,76 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
function getActionsCountByActionType(actions: ActionConnector[], actionTypeId: string) {
|
||||
return actions.filter((action) => action.actionTypeId === actionTypeId).length;
|
||||
}
|
||||
|
||||
const DeleteOperation: React.FunctionComponent<{
|
||||
item: ActionConnectorTableItem;
|
||||
canDelete: boolean;
|
||||
onDelete: () => void;
|
||||
}> = ({ item, canDelete, onDelete }) => {
|
||||
if (item.isPreconfigured) {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
data-test-subj="preConfiguredTitleMessage"
|
||||
label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertForm.preconfiguredTitleMessage',
|
||||
{
|
||||
defaultMessage: 'Preconfigured',
|
||||
}
|
||||
)}
|
||||
tooltipContent="This connector can't be deleted."
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={
|
||||
canDelete
|
||||
? i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription',
|
||||
{ defaultMessage: 'Delete this connector' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDisabledDescription',
|
||||
{ defaultMessage: 'Unable to delete connectors' }
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
isDisabled={!canDelete}
|
||||
data-test-subj="deleteConnector"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionName',
|
||||
{ defaultMessage: 'Delete' }
|
||||
)}
|
||||
onClick={onDelete}
|
||||
iconType={'trash'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
const NoPermissionPrompt: React.FunctionComponent<{}> = () => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateTitle"
|
||||
defaultMessage="No permissions to create connectors"
|
||||
/>
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<p data-test-subj="permissionDeniedMessage">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.noPermissionToCreateDescription"
|
||||
defaultMessage="Contact your system administrator."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -17,6 +17,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
|
||||
const find = getService('find');
|
||||
const retry = getService('retry');
|
||||
const comboBox = getService('comboBox');
|
||||
|
||||
describe('Connectors', function () {
|
||||
before(async () => {
|
||||
|
@ -76,7 +78,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
|
||||
await testSubjects.setValue('slackWebhookUrlInput', 'https://test');
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="saveEditedActionButton"]:not(disabled)');
|
||||
await find.clickByCssSelector(
|
||||
'[data-test-subj="saveAndCloseEditedActionButton"]:not(disabled)'
|
||||
);
|
||||
|
||||
const toastTitle = await pageObjects.common.closeToast();
|
||||
expect(toastTitle).to.eql(`Updated '${updatedConnectorName}'`);
|
||||
|
@ -92,6 +96,64 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should test a connector and display a successful result', async () => {
|
||||
const connectorName = generateUniqueKey();
|
||||
const indexName = generateUniqueKey();
|
||||
await createIndexConnector(connectorName, indexName);
|
||||
|
||||
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
|
||||
|
||||
const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList();
|
||||
expect(searchResultsBeforeEdit.length).to.eql(1);
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="testConnectorTab"]');
|
||||
|
||||
// test success
|
||||
await testSubjects.setValue('documentsJsonEditor', '{ "key": "value" }');
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="executeActionButton"]:not(disabled)');
|
||||
|
||||
await retry.try(async () => {
|
||||
await testSubjects.find('executionSuccessfulResult');
|
||||
});
|
||||
|
||||
await find.clickByCssSelector(
|
||||
'[data-test-subj="cancelSaveEditedConnectorButton"]:not(disabled)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should test a connector and display a failure result', async () => {
|
||||
const connectorName = generateUniqueKey();
|
||||
const indexName = generateUniqueKey();
|
||||
await createIndexConnector(connectorName, indexName);
|
||||
|
||||
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
|
||||
|
||||
const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList();
|
||||
expect(searchResultsBeforeEdit.length).to.eql(1);
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="testConnectorTab"]');
|
||||
|
||||
await testSubjects.setValue('documentsJsonEditor', '{ "": "value" }');
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="executeActionButton"]:not(disabled)');
|
||||
|
||||
await retry.try(async () => {
|
||||
const executionFailureResultCallout = await testSubjects.find('executionFailureResult');
|
||||
expect(await executionFailureResultCallout.getVisibleText()).to.match(
|
||||
/error indexing documents/
|
||||
);
|
||||
});
|
||||
|
||||
await find.clickByCssSelector(
|
||||
'[data-test-subj="cancelSaveEditedConnectorButton"]:not(disabled)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset connector when canceling an edit', async () => {
|
||||
const connectorName = generateUniqueKey();
|
||||
await createConnector(connectorName);
|
||||
|
@ -193,7 +255,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
|
||||
|
||||
expect(await testSubjects.exists('preconfiguredBadge')).to.be(true);
|
||||
expect(await testSubjects.exists('saveEditedActionButton')).to.be(false);
|
||||
expect(await testSubjects.exists('saveAndCloseEditedActionButton')).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -209,4 +271,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)');
|
||||
await pageObjects.common.closeToast();
|
||||
}
|
||||
|
||||
async function createIndexConnector(connectorName: string, indexName: string) {
|
||||
await pageObjects.triggersActionsUI.clickCreateConnectorButton();
|
||||
|
||||
await testSubjects.click('.index-card');
|
||||
|
||||
await testSubjects.setValue('nameInput', connectorName);
|
||||
|
||||
await comboBox.set('connectorIndexesComboBox', indexName);
|
||||
|
||||
await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)');
|
||||
await pageObjects.common.closeToast();
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue