Skip to content

Commit 055d8e7

Browse files
committed
[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.
1 parent b03c8bc commit 055d8e7

File tree

15 files changed

+929
-199
lines changed

15 files changed

+929
-199
lines changed

x-pack/plugins/actions/common/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,13 @@ export interface ActionResult {
2424
config: Record<string, any>;
2525
isPreconfigured: boolean;
2626
}
27+
28+
// the result returned from an action type executor function
29+
export interface ActionTypeExecutorResult<Data> {
30+
actionId: string;
31+
status: 'ok' | 'error';
32+
message?: string;
33+
serviceMessage?: string;
34+
data?: Data;
35+
retry?: null | boolean | Date;
36+
}

x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,47 @@ describe('execute()', () => {
284284
]
285285
`);
286286
});
287+
288+
test('resolves with an error when an error occurs in the indexing operation', async () => {
289+
const secrets = {};
290+
// minimal params
291+
const config = { index: 'index-value', refresh: false, executionTimeField: null };
292+
const params = {
293+
documents: [{ '': 'bob' }],
294+
};
295+
296+
const actionId = 'some-id';
297+
298+
services.callCluster.mockResolvedValue({
299+
took: 0,
300+
errors: true,
301+
items: [
302+
{
303+
index: {
304+
_index: 'indexme',
305+
_id: '7buTjHQB0SuNSiS9Hayt',
306+
status: 400,
307+
error: {
308+
type: 'mapper_parsing_exception',
309+
reason: 'failed to parse',
310+
caused_by: {
311+
type: 'illegal_argument_exception',
312+
reason: 'field name cannot be an empty string',
313+
},
314+
},
315+
},
316+
},
317+
],
318+
});
319+
320+
expect(await actionType.executor({ actionId, config, secrets, params, services }))
321+
.toMatchInlineSnapshot(`
322+
Object {
323+
"actionId": "some-id",
324+
"message": "error indexing documents",
325+
"serviceMessage": "failed to parse (field name cannot be an empty string)",
326+
"status": "error",
327+
}
328+
`);
329+
});
287330
});

x-pack/plugins/actions/server/builtin_action_types/es_index.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { curry } from 'lodash';
7+
import { curry, find } from 'lodash';
88
import { i18n } from '@kbn/i18n';
99
import { schema, TypeOf } from '@kbn/config-schema';
1010

@@ -85,21 +85,39 @@ async function executor(
8585
refresh: config.refresh,
8686
};
8787

88-
let result;
8988
try {
90-
result = await services.callCluster('bulk', bulkParams);
89+
const result = await services.callCluster('bulk', bulkParams);
90+
91+
const err = find(result.items, 'index.error.reason');
92+
if (err) {
93+
return wrapErr(
94+
`${err.index.error!.reason}${
95+
err.index.error?.caused_by ? ` (${err.index.error?.caused_by?.reason})` : ''
96+
}`,
97+
actionId,
98+
logger
99+
);
100+
}
101+
102+
return { status: 'ok', data: result, actionId };
91103
} catch (err) {
92-
const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', {
93-
defaultMessage: 'error indexing documents',
94-
});
95-
logger.error(`error indexing documents: ${err.message}`);
96-
return {
97-
status: 'error',
98-
actionId,
99-
message,
100-
serviceMessage: err.message,
101-
};
104+
return wrapErr(err.message, actionId, logger);
102105
}
106+
}
103107

104-
return { status: 'ok', data: result, actionId };
108+
function wrapErr(
109+
errMessage: string,
110+
actionId: string,
111+
logger: Logger
112+
): ActionTypeExecutorResult<unknown> {
113+
const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', {
114+
defaultMessage: 'error indexing documents',
115+
});
116+
logger.error(`error indexing documents: ${errMessage}`);
117+
return {
118+
status: 'error',
119+
actionId,
120+
message,
121+
serviceMessage: errMessage,
122+
};
105123
}

x-pack/plugins/actions/server/types.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
SavedObjectsClientContract,
1616
SavedObjectAttributes,
1717
} from '../../../../src/core/server';
18+
import { ActionTypeExecutorResult } from '../common';
19+
export { ActionTypeExecutorResult } from '../common';
1820

1921
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
2022
export type GetServicesFunction = (request: KibanaRequest) => Services;
@@ -80,16 +82,6 @@ export interface FindActionResult extends ActionResult {
8082
referencedByCount: number;
8183
}
8284

83-
// the result returned from an action type executor function
84-
export interface ActionTypeExecutorResult<Data> {
85-
actionId: string;
86-
status: 'ok' | 'error';
87-
message?: string;
88-
serviceMessage?: string;
89-
data?: Data;
90-
retry?: null | boolean | Date;
91-
}
92-
9385
// signature of the action type executor function
9486
export type ExecutorType<Config, Secrets, Params, ResultData> = (
9587
options: ActionTypeExecutorOptions<Config, Secrets, Params>

x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const AddMessageVariables: React.FunctionComponent<Props> = ({
6161
<EuiButtonIcon
6262
id={`${paramsProperty}AddVariableButton`}
6363
data-test-subj={`${paramsProperty}AddVariableButton`}
64+
isDisabled={(messageVariables?.length ?? 0) === 0}
6465
title={addVariableButtonTitle}
6566
onClick={() => setIsVariablesPopoverOpen(true)}
6667
iconType="indexOpen"

x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,48 +32,47 @@ export const IndexParamsFields = ({
3232
};
3333

3434
return (
35-
<>
36-
<JsonEditorWithMessageVariables
37-
messageVariables={messageVariables}
38-
paramsProperty={'documents'}
39-
inputTargetValue={
40-
documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined
35+
<JsonEditorWithMessageVariables
36+
messageVariables={messageVariables}
37+
paramsProperty={'documents'}
38+
data-test-subj="documentToIndex"
39+
inputTargetValue={
40+
documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined
41+
}
42+
label={i18n.translate(
43+
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel',
44+
{
45+
defaultMessage: 'Document to index',
4146
}
42-
label={i18n.translate(
43-
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel',
44-
{
45-
defaultMessage: 'Document to index',
46-
}
47-
)}
48-
aria-label={i18n.translate(
49-
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel',
50-
{
51-
defaultMessage: 'Code editor',
52-
}
53-
)}
54-
errors={errors.documents as string[]}
55-
onDocumentsChange={onDocumentsChange}
56-
helpText={
57-
<EuiLink
58-
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/index-action-type.html#index-action-configuration`}
59-
target="_blank"
60-
>
61-
<FormattedMessage
62-
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel"
63-
defaultMessage="Index document example."
64-
/>
65-
</EuiLink>
47+
)}
48+
aria-label={i18n.translate(
49+
'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel',
50+
{
51+
defaultMessage: 'Code editor',
6652
}
67-
onBlur={() => {
68-
if (
69-
!(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined)
70-
) {
71-
// set document as empty to turn on the validation for non empty valid JSON object
72-
onDocumentsChange('{}');
73-
}
74-
}}
75-
/>
76-
</>
53+
)}
54+
errors={errors.documents as string[]}
55+
onDocumentsChange={onDocumentsChange}
56+
helpText={
57+
<EuiLink
58+
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/index-action-type.html#index-action-configuration`}
59+
target="_blank"
60+
>
61+
<FormattedMessage
62+
id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel"
63+
defaultMessage="Index document example."
64+
/>
65+
</EuiLink>
66+
}
67+
onBlur={() => {
68+
if (
69+
!(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined)
70+
) {
71+
// set document as empty to turn on the validation for non empty valid JSON object
72+
onDocumentsChange('{}');
73+
}
74+
}}
75+
/>
7776
);
7877
};
7978

x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
loadActionTypes,
1313
loadAllActions,
1414
updateActionConnector,
15+
executeAction,
1516
} from './action_connector_api';
1617

1718
const http = httpServiceMock.createStartContract();
@@ -128,3 +129,32 @@ describe('deleteActions', () => {
128129
`);
129130
});
130131
});
132+
133+
describe('executeAction', () => {
134+
test('should call execute API', async () => {
135+
const id = '123';
136+
const params = {
137+
stringParams: 'someString',
138+
numericParams: 123,
139+
};
140+
141+
http.post.mockResolvedValueOnce({
142+
actionId: id,
143+
status: 'ok',
144+
});
145+
146+
const result = await executeAction({ id, http, params });
147+
expect(result).toEqual({
148+
actionId: id,
149+
status: 'ok',
150+
});
151+
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
152+
Array [
153+
"/api/actions/action/123/_execute",
154+
Object {
155+
"body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}",
156+
},
157+
]
158+
`);
159+
});
160+
});

x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { HttpSetup } from 'kibana/public';
88
import { BASE_ACTION_API_PATH } from '../constants';
99
import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types';
10+
import { ActionTypeExecutorResult } from '../../../../../plugins/actions/common';
1011

1112
export async function loadActionTypes({ http }: { http: HttpSetup }): Promise<ActionType[]> {
1213
return await http.get(`${BASE_ACTION_API_PATH}/list_action_types`);
@@ -65,3 +66,17 @@ export async function deleteActions({
6566
);
6667
return { successes, errors };
6768
}
69+
70+
export async function executeAction({
71+
id,
72+
params,
73+
http,
74+
}: {
75+
id: string;
76+
http: HttpSetup;
77+
params: Record<string, unknown>;
78+
}): Promise<ActionTypeExecutorResult<unknown>> {
79+
return await http.post(`${BASE_ACTION_API_PATH}/action/${id}/_execute`, {
80+
body: JSON.stringify({ params }),
81+
});
82+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.connectorEditFlyoutTabs {
2+
margin-bottom: '-25px';
3+
}

x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,6 @@ describe('connector_edit_flyout', () => {
152152

153153
const preconfiguredBadge = wrapper.find('[data-test-subj="preconfiguredBadge"]');
154154
expect(preconfiguredBadge.exists()).toBeTruthy();
155-
expect(wrapper.find('[data-test-subj="saveEditedActionButton"]').exists()).toBeFalsy();
155+
expect(wrapper.find('[data-test-subj="saveAndCloseEditedActionButton"]').exists()).toBeFalsy();
156156
});
157157
});

0 commit comments

Comments
 (0)