[105264] Fix error not surfacing bug for Jira (#114800) (#116828)

* [105264] Fix error not surfacing bug for Jira

* [105264] Fix ServiceNow errors not surfacing to UI

* Fix tests with updated functon

* Fix PR errors

Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co>

Co-authored-by: Kristof C <kpac.ja@gmail.com>
Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co>
This commit is contained in:
Kibana Machine 2021-11-04 10:16:11 -04:00 committed by GitHub
parent 9fc68bb1b6
commit 468bc21f95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 342 additions and 200 deletions

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './connectors_api';

View file

@ -74,13 +74,33 @@ const fieldsResponse = {
}, },
}; };
const issueResponse = { const issue = {
id: '10267', id: '10267',
key: 'RJ-107', key: 'RJ-107',
fields: { summary: 'Test title' }, title: 'test title',
}; };
const issuesResponse = [issueResponse]; const issueResponse = {
status: 'ok' as const,
connector_id: '1',
data: issue,
};
const issuesResponse = {
...issueResponse,
data: [issue],
};
const camelCasedIssueResponse = {
status: 'ok' as const,
actionId: '1',
data: issue,
};
const camelCasedIssuesResponse = {
...camelCasedIssueResponse,
data: [issue],
};
describe('Jira API', () => { describe('Jira API', () => {
const http = httpServiceMock.createStartContract(); const http = httpServiceMock.createStartContract();
@ -131,7 +151,7 @@ describe('Jira API', () => {
title: 'test issue', title: 'test issue',
}); });
expect(res).toEqual(issuesResponse); expect(res).toEqual(camelCasedIssuesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', {
body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}', body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -142,7 +162,7 @@ describe('Jira API', () => {
describe('getIssue', () => { describe('getIssue', () => {
test('should call get fields API', async () => { test('should call get fields API', async () => {
const abortCtrl = new AbortController(); const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issuesResponse); http.post.mockResolvedValueOnce(issueResponse);
const res = await getIssue({ const res = await getIssue({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -150,7 +170,7 @@ describe('Jira API', () => {
id: 'RJ-107', id: 'RJ-107',
}); });
expect(res).toEqual(issuesResponse); expect(res).toEqual(camelCasedIssueResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', {
body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}', body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,

View file

@ -7,7 +7,11 @@
import { HttpSetup } from 'kibana/public'; import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../actions/common'; import { ActionTypeExecutorResult } from '../../../../../actions/common';
import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api'; import { getExecuteConnectorUrl } from '../../../../common/utils';
import {
ConnectorExecutorResult,
rewriteResponseToCamelCase,
} from '../rewrite_response_to_camel_case';
import { IssueTypes, Fields, Issues, Issue } from './types'; import { IssueTypes, Fields, Issues, Issue } from './types';
export interface GetIssueTypesProps { export interface GetIssueTypesProps {
@ -17,12 +21,17 @@ export interface GetIssueTypesProps {
} }
export async function getIssueTypes({ http, signal, connectorId }: GetIssueTypesProps) { export async function getIssueTypes({ http, signal, connectorId }: GetIssueTypesProps) {
return http.post<ActionTypeExecutorResult<IssueTypes>>(getExecuteConnectorUrl(connectorId), { const res = await http.post<ConnectorExecutorResult<IssueTypes>>(
body: JSON.stringify({ getExecuteConnectorUrl(connectorId),
params: { subAction: 'issueTypes', subActionParams: {} }, {
}), body: JSON.stringify({
signal, params: { subAction: 'issueTypes', subActionParams: {} },
}); }),
signal,
}
);
return rewriteResponseToCamelCase(res);
} }
export interface GetFieldsByIssueTypeProps { export interface GetFieldsByIssueTypeProps {
@ -38,12 +47,16 @@ export async function getFieldsByIssueType({
connectorId, connectorId,
id, id,
}: GetFieldsByIssueTypeProps): Promise<ActionTypeExecutorResult<Fields>> { }: GetFieldsByIssueTypeProps): Promise<ActionTypeExecutorResult<Fields>> {
return http.post(getExecuteConnectorUrl(connectorId), { const res = await http.post<ConnectorExecutorResult<Fields>>(
body: JSON.stringify({ getExecuteConnectorUrl(connectorId),
params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'fieldsByIssueType', subActionParams: { id } },
}); }),
signal,
}
);
return rewriteResponseToCamelCase(res);
} }
export interface GetIssuesTypeProps { export interface GetIssuesTypeProps {
@ -59,12 +72,16 @@ export async function getIssues({
connectorId, connectorId,
title, title,
}: GetIssuesTypeProps): Promise<ActionTypeExecutorResult<Issues>> { }: GetIssuesTypeProps): Promise<ActionTypeExecutorResult<Issues>> {
return http.post(getExecuteConnectorUrl(connectorId), { const res = await http.post<ConnectorExecutorResult<Issues>>(
body: JSON.stringify({ getExecuteConnectorUrl(connectorId),
params: { subAction: 'issues', subActionParams: { title } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'issues', subActionParams: { title } },
}); }),
signal,
}
);
return rewriteResponseToCamelCase(res);
} }
export interface GetIssueTypeProps { export interface GetIssueTypeProps {
@ -80,10 +97,11 @@ export async function getIssue({
connectorId, connectorId,
id, id,
}: GetIssueTypeProps): Promise<ActionTypeExecutorResult<Issue>> { }: GetIssueTypeProps): Promise<ActionTypeExecutorResult<Issue>> {
return http.post(getExecuteConnectorUrl(connectorId), { const res = await http.post<ConnectorExecutorResult<Issue>>(getExecuteConnectorUrl(connectorId), {
body: JSON.stringify({ body: JSON.stringify({
params: { subAction: 'issue', subActionParams: { id } }, params: { subAction: 'issue', subActionParams: { id } },
}), }),
signal, signal,
}); });
return rewriteResponseToCamelCase(res);
} }

View file

@ -6,7 +6,7 @@
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../common'; import { ActionConnector } from '../../../../common';
import { getFieldsByIssueType } from './api'; import { getFieldsByIssueType } from './api';
import { Fields } from './types'; import { Fields } from './types';
@ -14,10 +14,7 @@ import * as i18n from './translations';
interface Props { interface Props {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
issueType: string | null; issueType: string | null;
connector?: ActionConnector; connector?: ActionConnector;
} }

View file

@ -6,7 +6,7 @@
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../common'; import { ActionConnector } from '../../../../common';
import { getIssueTypes } from './api'; import { getIssueTypes } from './api';
import { IssueTypes } from './types'; import { IssueTypes } from './types';
@ -14,10 +14,7 @@ import * as i18n from './translations';
interface Props { interface Props {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
connector?: ActionConnector; connector?: ActionConnector;
handleIssueType: (options: Array<{ value: string; text: string }>) => void; handleIssueType: (options: Array<{ value: string; text: string }>) => void;
} }

View file

@ -6,8 +6,11 @@
*/ */
import { HttpSetup } from 'kibana/public'; import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../actions/common';
import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api'; import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api';
import {
ConnectorExecutorResult,
rewriteResponseToCamelCase,
} from '../rewrite_response_to_camel_case';
import { ResilientIncidentTypes, ResilientSeverity } from './types'; import { ResilientIncidentTypes, ResilientSeverity } from './types';
export const BASE_ACTION_API_PATH = '/api/actions'; export const BASE_ACTION_API_PATH = '/api/actions';
@ -19,7 +22,7 @@ export interface Props {
} }
export async function getIncidentTypes({ http, signal, connectorId }: Props) { export async function getIncidentTypes({ http, signal, connectorId }: Props) {
return http.post<ActionTypeExecutorResult<ResilientIncidentTypes>>( const res = await http.post<ConnectorExecutorResult<ResilientIncidentTypes>>(
getExecuteConnectorUrl(connectorId), getExecuteConnectorUrl(connectorId),
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -28,10 +31,12 @@ export async function getIncidentTypes({ http, signal, connectorId }: Props) {
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }
export async function getSeverity({ http, signal, connectorId }: Props) { export async function getSeverity({ http, signal, connectorId }: Props) {
return http.post<ActionTypeExecutorResult<ResilientSeverity>>( const res = await http.post<ConnectorExecutorResult<ResilientSeverity>>(
getExecuteConnectorUrl(connectorId), getExecuteConnectorUrl(connectorId),
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -40,4 +45,6 @@ export async function getSeverity({ http, signal, connectorId }: Props) {
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
ConnectorExecutorResult,
rewriteResponseToCamelCase,
} from './rewrite_response_to_camel_case';
const responseWithSnakeCasedFields: ConnectorExecutorResult<{}> = {
service_message: 'oh noooooo',
connector_id: '1213',
data: {},
status: 'ok',
};
describe('rewriteResponseToCamelCase works correctly', () => {
it('correctly transforms snake case to camel case for ActionTypeExecuteResults', () => {
const camelCasedData = rewriteResponseToCamelCase(responseWithSnakeCasedFields);
expect(camelCasedData).toEqual({
serviceMessage: 'oh noooooo',
actionId: '1213',
data: {},
status: 'ok',
});
});
});

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ActionTypeExecutorResult, RewriteResponseCase } from '../../../../actions/common';
export type ConnectorExecutorResult<T> = ReturnType<
RewriteResponseCase<ActionTypeExecutorResult<T>>
>;
export const rewriteResponseToCamelCase = <T>({
connector_id: actionId,
service_message: serviceMessage,
...data
}: ConnectorExecutorResult<T>): ActionTypeExecutorResult<T> => ({
...data,
actionId,
...(serviceMessage && { serviceMessage }),
});

View file

@ -6,8 +6,11 @@
*/ */
import { HttpSetup } from 'kibana/public'; import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../actions/common';
import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api'; import { getExecuteConnectorUrl } from '../../../../common/utils/connectors_api';
import {
ConnectorExecutorResult,
rewriteResponseToCamelCase,
} from '../rewrite_response_to_camel_case';
import { Choice } from './types'; import { Choice } from './types';
export const BASE_ACTION_API_PATH = '/api/actions'; export const BASE_ACTION_API_PATH = '/api/actions';
@ -20,10 +23,14 @@ export interface GetChoicesProps {
} }
export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) { export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) {
return http.post<ActionTypeExecutorResult<Choice[]>>(getExecuteConnectorUrl(connectorId), { const res = await http.post<ConnectorExecutorResult<Choice[]>>(
body: JSON.stringify({ getExecuteConnectorUrl(connectorId),
params: { subAction: 'getChoices', subActionParams: { fields } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'getChoices', subActionParams: { fields } },
}); }),
signal,
}
);
return rewriteResponseToCamelCase(res);
} }

View file

@ -6,7 +6,7 @@
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../common'; import { ActionConnector } from '../../../../common';
import { getChoices } from './api'; import { getChoices } from './api';
import { Choice } from './types'; import { Choice } from './types';
@ -14,10 +14,7 @@ import * as i18n from './translations';
export interface UseGetChoicesProps { export interface UseGetChoicesProps {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
connector?: ActionConnector; connector?: ActionConnector;
fields: string[]; fields: string[];
onSuccess?: (choices: Choice[]) => void; onSuccess?: (choices: Choice[]) => void;

View file

@ -8,87 +8,96 @@
import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; import { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
import { getIssueTypes, getFieldsByIssueType, getIssues, getIssue } from './api'; import { getIssueTypes, getFieldsByIssueType, getIssues, getIssue } from './api';
const issueTypesResponse = { const issueTypesData = {
status: 'ok', projects: [
data: { {
projects: [ issuetypes: [
{ {
issuetypes: [ id: '10006',
{ name: 'Task',
id: '10006', },
name: 'Task', {
}, id: '10007',
{ name: 'Bug',
id: '10007', },
name: 'Bug', ],
}, },
], ],
},
],
},
actionId: 'test',
}; };
const fieldsResponse = { const fieldData = {
status: 'ok', projects: [
data: { {
projects: [ issuetypes: [
{ {
issuetypes: [ id: '10006',
{ name: 'Task',
id: '10006', fields: {
name: 'Task', summary: { fieldId: 'summary' },
fields: { priority: {
summary: { fieldId: 'summary' }, fieldId: 'priority',
priority: { allowedValues: [
fieldId: 'priority', {
allowedValues: [ name: 'Highest',
{ id: '1',
name: 'Highest', },
id: '1', {
}, name: 'High',
{ id: '2',
name: 'High', },
id: '2', {
},
{
name: 'Medium',
id: '3',
},
{
name: 'Low',
id: '4',
},
{
name: 'Lowest',
id: '5',
},
],
defaultValue: {
name: 'Medium', name: 'Medium',
id: '3', id: '3',
}, },
{
name: 'Low',
id: '4',
},
{
name: 'Lowest',
id: '5',
},
],
defaultValue: {
name: 'Medium',
id: '3',
}, },
}, },
}, },
], },
}, ],
], },
actionId: 'test', ],
}, };
const issueTypesResponse = {
status: 'ok' as const,
connector_id: 'test',
data: issueTypesData,
};
const fieldsResponse = {
status: 'ok' as const,
data: fieldData,
connector_id: 'test',
};
const singleIssue = {
id: '10267',
key: 'RJ-107',
title: 'some title',
}; };
const issueResponse = { const issueResponse = {
status: 'ok', status: 'ok' as const,
data: { data: singleIssue,
id: '10267', connector_id: 'test',
key: 'RJ-107',
fields: { summary: 'Test title' },
},
actionId: 'test',
}; };
const issuesResponse = [issueResponse]; const issuesResponse = {
...issueResponse,
data: [singleIssue],
};
describe('Jira API', () => { describe('Jira API', () => {
const http = httpServiceMock.createStartContract(); const http = httpServiceMock.createStartContract();
@ -100,8 +109,11 @@ describe('Jira API', () => {
const abortCtrl = new AbortController(); const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issueTypesResponse); http.post.mockResolvedValueOnce(issueTypesResponse);
const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'te/st' }); const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'te/st' });
expect(res).toEqual({
expect(res).toEqual(issueTypesResponse); status: 'ok' as const,
actionId: 'test',
data: issueTypesData,
});
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}', body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -120,7 +132,7 @@ describe('Jira API', () => {
id: '10006', id: '10006',
}); });
expect(res).toEqual(fieldsResponse); expect(res).toEqual({ status: 'ok', data: fieldData, actionId: 'test' });
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}', body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -139,7 +151,11 @@ describe('Jira API', () => {
title: 'test issue', title: 'test issue',
}); });
expect(res).toEqual(issuesResponse); expect(res).toEqual({
status: 'ok',
data: [singleIssue],
actionId: 'test',
});
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}', body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -150,7 +166,7 @@ describe('Jira API', () => {
describe('getIssue', () => { describe('getIssue', () => {
test('should call get fields API', async () => { test('should call get fields API', async () => {
const abortCtrl = new AbortController(); const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issuesResponse); http.post.mockResolvedValueOnce(issueResponse);
const res = await getIssue({ const res = await getIssue({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -158,7 +174,11 @@ describe('Jira API', () => {
id: 'RJ-107', id: 'RJ-107',
}); });
expect(res).toEqual(issuesResponse); expect(res).toEqual({
status: 'ok',
data: singleIssue,
actionId: 'test',
});
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}', body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,

View file

@ -6,7 +6,10 @@
*/ */
import { HttpSetup } from 'kibana/public'; import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../../actions/common';
import { BASE_ACTION_API_PATH } from '../../../constants'; import { BASE_ACTION_API_PATH } from '../../../constants';
import { ConnectorExecutorResult, rewriteResponseToCamelCase } from '../rewrite_response_body';
import { Fields, Issue, IssueTypes } from './types';
export async function getIssueTypes({ export async function getIssueTypes({
http, http,
@ -16,8 +19,8 @@ export async function getIssueTypes({
http: HttpSetup; http: HttpSetup;
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
}): Promise<Record<string, any>> { }): Promise<ActionTypeExecutorResult<IssueTypes>> {
return await http.post( const res = await http.post<ConnectorExecutorResult<IssueTypes>>(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -26,6 +29,7 @@ export async function getIssueTypes({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }
export async function getFieldsByIssueType({ export async function getFieldsByIssueType({
@ -38,8 +42,8 @@ export async function getFieldsByIssueType({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
id: string; id: string;
}): Promise<Record<string, any>> { }): Promise<ActionTypeExecutorResult<Fields>> {
return await http.post( const res = await http.post<ConnectorExecutorResult<Fields>>(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -48,6 +52,7 @@ export async function getFieldsByIssueType({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }
export async function getIssues({ export async function getIssues({
@ -60,8 +65,8 @@ export async function getIssues({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
title: string; title: string;
}): Promise<Record<string, any>> { }): Promise<ActionTypeExecutorResult<Issue[]>> {
return await http.post( const res = await http.post<ConnectorExecutorResult<Issue[]>>(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -70,6 +75,7 @@ export async function getIssues({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }
export async function getIssue({ export async function getIssue({
@ -82,8 +88,8 @@ export async function getIssue({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
id: string; id: string;
}): Promise<Record<string, any>> { }): Promise<ActionTypeExecutorResult<Issue>> {
return await http.post( const res = await http.post<ConnectorExecutorResult<Issue>>(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -92,4 +98,5 @@ export async function getIssue({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }

View file

@ -8,7 +8,7 @@
import React, { useMemo, useEffect, useCallback, useState, memo } from 'react'; import React, { useMemo, useEffect, useCallback, useState, memo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { useGetIssues } from './use_get_issues'; import { useGetIssues } from './use_get_issues';
import { useGetSingleIssue } from './use_get_single_issue'; import { useGetSingleIssue } from './use_get_single_issue';
@ -17,10 +17,7 @@ import * as i18n from './translations';
interface Props { interface Props {
selectedValue?: string | null; selectedValue?: string | null;
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
onChange: (parentIssueKey: string) => void; onChange: (parentIssueKey: string) => void;
} }

View file

@ -24,3 +24,18 @@ export interface JiraSecrets {
email: string; email: string;
apiToken: string; apiToken: string;
} }
export type IssueTypes = Array<{ id: string; name: string }>;
export interface Issue {
id: string;
key: string;
title: string;
}
export interface Fields {
[key: string]: {
allowedValues: Array<{ name: string; id: string }> | [];
defaultValue: { name: string; id: string } | {};
};
}

View file

@ -6,24 +6,15 @@
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { Fields } from './types';
import { getFieldsByIssueType } from './api'; import { getFieldsByIssueType } from './api';
import * as i18n from './translations'; import * as i18n from './translations';
interface Fields {
[key: string]: {
allowedValues: Array<{ name: string; id: string }> | [];
defaultValue: { name: string; id: string } | {};
};
}
interface Props { interface Props {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
issueType: string | undefined; issueType: string | undefined;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
} }

View file

@ -6,19 +6,16 @@
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { IssueTypes } from './types';
import { getIssueTypes } from './api'; import { getIssueTypes } from './api';
import * as i18n from './translations'; import * as i18n from './translations';
type IssueTypes = Array<{ id: string; name: string }>;
interface Props { interface Props {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
} }

View file

@ -7,25 +7,21 @@
import { isEmpty, debounce } from 'lodash/fp'; import { isEmpty, debounce } from 'lodash/fp';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { Issue } from './types';
import { getIssues } from './api'; import { getIssues } from './api';
import * as i18n from './translations'; import * as i18n from './translations';
type Issues = Array<{ id: string; key: string; title: string }>;
interface Props { interface Props {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
query: string | null; query: string | null;
} }
export interface UseGetIssues { export interface UseGetIssues {
issues: Issues; issues: Issue[];
isLoading: boolean; isLoading: boolean;
} }
@ -36,7 +32,7 @@ export const useGetIssues = ({
query, query,
}: Props): UseGetIssues => { }: Props): UseGetIssues => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [issues, setIssues] = useState<Issues>([]); const [issues, setIssues] = useState<Issue[]>([]);
const abortCtrl = useRef(new AbortController()); const abortCtrl = useRef(new AbortController());
useEffect(() => { useEffect(() => {

View file

@ -6,23 +6,15 @@
*/ */
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { Issue } from './types';
import { getIssue } from './api'; import { getIssue } from './api';
import * as i18n from './translations'; import * as i18n from './translations';
interface Issue {
id: string;
key: string;
title: string;
}
interface Props { interface Props {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
id?: string | null; id?: string | null;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
} }

View file

@ -32,7 +32,6 @@ const incidentTypesResponse = {
{ id: 16, name: 'TBD / Unknown' }, { id: 16, name: 'TBD / Unknown' },
{ id: 15, name: 'Vendor / 3rd party error' }, { id: 15, name: 'Vendor / 3rd party error' },
], ],
actionId: 'te/st',
}; };
const severityResponse = { const severityResponse = {
@ -42,7 +41,6 @@ const severityResponse = {
{ id: 5, name: 'Medium' }, { id: 5, name: 'Medium' },
{ id: 6, name: 'High' }, { id: 6, name: 'High' },
], ],
actionId: 'te/st',
}; };
describe('Resilient API', () => { describe('Resilient API', () => {
@ -53,14 +51,14 @@ describe('Resilient API', () => {
describe('getIncidentTypes', () => { describe('getIncidentTypes', () => {
test('should call get choices API', async () => { test('should call get choices API', async () => {
const abortCtrl = new AbortController(); const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(incidentTypesResponse); http.post.mockResolvedValueOnce({ ...incidentTypesResponse, connector_id: 'te/st' });
const res = await getIncidentTypes({ const res = await getIncidentTypes({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'te/st', connectorId: 'te/st',
}); });
expect(res).toEqual(incidentTypesResponse); expect(res).toEqual({ ...incidentTypesResponse, actionId: 'te/st' });
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"incidentTypes","subActionParams":{}}}', body: '{"params":{"subAction":"incidentTypes","subActionParams":{}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
@ -71,14 +69,15 @@ describe('Resilient API', () => {
describe('getSeverity', () => { describe('getSeverity', () => {
test('should call get choices API', async () => { test('should call get choices API', async () => {
const abortCtrl = new AbortController(); const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(severityResponse); http.post.mockResolvedValueOnce({ ...severityResponse, connector_id: 'te/st' });
const res = await getSeverity({ const res = await getSeverity({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'te/st', connectorId: 'te/st',
}); });
expect(res).toEqual(severityResponse); expect(res).toEqual({ ...severityResponse, actionId: 'te/st' });
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"severity","subActionParams":{}}}', body: '{"params":{"subAction":"severity","subActionParams":{}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,

View file

@ -7,6 +7,7 @@
import { HttpSetup } from 'kibana/public'; import { HttpSetup } from 'kibana/public';
import { BASE_ACTION_API_PATH } from '../../../constants'; import { BASE_ACTION_API_PATH } from '../../../constants';
import { rewriteResponseToCamelCase } from '../rewrite_response_body';
export async function getIncidentTypes({ export async function getIncidentTypes({
http, http,
@ -17,7 +18,7 @@ export async function getIncidentTypes({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post( const res = await http.post(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -26,6 +27,7 @@ export async function getIncidentTypes({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }
export async function getSeverity({ export async function getSeverity({
@ -37,7 +39,7 @@ export async function getSeverity({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post( const res = await http.post(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -46,4 +48,5 @@ export async function getSeverity({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ActionTypeExecutorResult, RewriteResponseCase } from '../../../../../actions/common';
export type ConnectorExecutorResult<T> = ReturnType<
RewriteResponseCase<ActionTypeExecutorResult<T>>
>;
export const rewriteResponseToCamelCase = <T>({
connector_id: actionId,
service_message: serviceMessage,
...data
}: ConnectorExecutorResult<T>): ActionTypeExecutorResult<T> => ({
...data,
actionId,
...(serviceMessage && { serviceMessage }),
});

View file

@ -6,11 +6,15 @@
*/ */
import { HttpSetup } from 'kibana/public'; import { HttpSetup } from 'kibana/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths // eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { snExternalServiceConfig } from '../../../../../../actions/server/builtin_action_types/servicenow/config'; import { snExternalServiceConfig } from '../../../../../../actions/server/builtin_action_types/servicenow/config';
import { BASE_ACTION_API_PATH } from '../../../constants'; import { BASE_ACTION_API_PATH } from '../../../constants';
import { API_INFO_ERROR } from './translations'; import { API_INFO_ERROR } from './translations';
import { AppInfo, RESTApiError } from './types'; import { AppInfo, RESTApiError } from './types';
import { ConnectorExecutorResult, rewriteResponseToCamelCase } from '../rewrite_response_body';
import { ActionTypeExecutorResult } from '../../../../../../actions/common';
import { Choice } from './types';
export async function getChoices({ export async function getChoices({
http, http,
@ -22,8 +26,8 @@ export async function getChoices({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
fields: string[]; fields: string[];
}): Promise<Record<string, any>> { }): Promise<ActionTypeExecutorResult<Choice[]>> {
return await http.post( const res = await http.post<ConnectorExecutorResult<Choice[]>>(
`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`, `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
{ {
body: JSON.stringify({ body: JSON.stringify({
@ -32,6 +36,7 @@ export async function getChoices({
signal, signal,
} }
); );
return rewriteResponseToCamelCase(res);
} }
/** /**

View file

@ -122,7 +122,7 @@ describe('UseChoices', () => {
it('it displays an error when service fails', async () => { it('it displays an error when service fails', async () => {
getChoicesMock.mockResolvedValue({ getChoicesMock.mockResolvedValue({
status: 'error', status: 'error',
service_message: 'An error occurred', serviceMessage: 'An error occurred',
}); });
const { waitForNextUpdate } = renderHook<UseChoicesProps, UseChoices>(() => const { waitForNextUpdate } = renderHook<UseChoicesProps, UseChoices>(() =>

View file

@ -6,7 +6,7 @@
*/ */
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { Choice, Fields } from './types'; import { Choice, Fields } from './types';
@ -14,10 +14,7 @@ import { useGetChoices } from './use_get_choices';
export interface UseChoicesProps { export interface UseChoicesProps {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
fields: string[]; fields: string[];
} }

View file

@ -121,7 +121,7 @@ describe('useGetChoices', () => {
it('it displays an error when service fails', async () => { it('it displays an error when service fails', async () => {
getChoicesMock.mockResolvedValue({ getChoicesMock.mockResolvedValue({
status: 'error', status: 'error',
service_message: 'An error occurred', serviceMessage: 'An error occurred',
}); });
const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() => const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>

View file

@ -6,7 +6,7 @@
*/ */
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public'; import { HttpSetup, IToasts } from 'kibana/public';
import { ActionConnector } from '../../../../types'; import { ActionConnector } from '../../../../types';
import { getChoices } from './api'; import { getChoices } from './api';
import { Choice } from './types'; import { Choice } from './types';
@ -14,10 +14,7 @@ import * as i18n from './translations';
export interface UseGetChoicesProps { export interface UseGetChoicesProps {
http: HttpSetup; http: HttpSetup;
toastNotifications: Pick< toastNotifications: IToasts;
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionConnector?: ActionConnector; actionConnector?: ActionConnector;
fields: string[]; fields: string[];
onSuccess?: (choices: Choice[]) => void; onSuccess?: (choices: Choice[]) => void;
@ -66,7 +63,7 @@ export const useGetChoices = ({
if (res.status && res.status === 'error') { if (res.status && res.status === 'error') {
toastNotifications.addDanger({ toastNotifications.addDanger({
title: i18n.CHOICES_API_ERROR, title: i18n.CHOICES_API_ERROR,
text: `${res.service_message ?? res.message}`, text: `${res.serviceMessage ?? res.message}`,
}); });
} else if (onSuccess) { } else if (onSuccess) {
onSuccess(data); onSuccess(data);