From 944b7dd4581331e35bb26e65ba364b1a285f8c59 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 28 Jul 2020 16:02:26 +0300 Subject: [PATCH 01/67] Create schema --- .../builtin_action_types/jira/case_schema.ts | 36 ++++++++++ .../builtin_action_types/jira/schema.ts | 72 +++++++++++++++++-- 2 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts new file mode 100644 index 0000000000000..2df8c8156cde8 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts @@ -0,0 +1,36 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const MappingActionType = schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), +]); + +export const MapRecordSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: MappingActionType, +}); + +export const IncidentConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapRecordSchema), +}); + +export const EntityInformation = { + createdAt: schema.maybe(schema.string()), + createdBy: schema.maybe(schema.any()), + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(schema.any()), +}; + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + ...EntityInformation, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 9c831e75d91c1..9056affe7f5d2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -5,18 +5,78 @@ */ import { schema } from '@kbn/config-schema'; -import { ExternalIncidentServiceConfiguration } from '../case/schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_schema'; -export const JiraPublicConfiguration = { +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), projectKey: schema.string(), - ...ExternalIncidentServiceConfiguration, + // TODO: to remove - set it optional for the current stage to support Case ServiceNow implementation + incidentConfiguration: schema.nullable(IncidentConfigurationSchema), + isCaseOwned: schema.maybe(schema.boolean()), }; -export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration); +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); -export const JiraSecretConfiguration = { +export const ExternalIncidentServiceSecretConfiguration = { email: schema.string(), apiToken: schema.string(), }; -export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration); +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), + schema.literal('getCreateIssueMetadata'), + schema.literal('getCapabilities'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + savedObjectId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + issueType: schema.nullable(schema.string()), + priority: schema.nullable(schema.string()), + labels: schema.nullable(schema.arrayOf(schema.string())), + // TODO: remove later - need for support Case push multiple comments + comments: schema.maybe(schema.arrayOf(CommentSchema)), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); +export const ExecutorSubActionCreateIssueMetadataParamsSchema = schema.object({}); +export const ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), + schema.object({ + subAction: schema.literal('getCreateIssueMetadata'), + subActionParams: ExecutorSubActionCreateIssueMetadataParamsSchema, + }), + schema.object({ + subAction: schema.literal('getCapabilities'), + subActionParams: ExecutorSubActionGetCapabilitiesParamsSchema, + }), +]); From 7dad0fb1cfe69187501e3898ff7887fe3bd93cf7 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 29 Jul 2020 18:48:54 +0300 Subject: [PATCH 02/67] Create types --- .../builtin_action_types/jira/case_types.ts | 65 ++++++++++ .../server/builtin_action_types/jira/types.ts | 121 +++++++++++++++--- 2 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts new file mode 100644 index 0000000000000..2a535764ae3b9 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts @@ -0,0 +1,65 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeOf } from '@kbn/config-schema'; +import { + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; +import { IncidentConfigurationSchema, MapRecordSchema, CommentSchema } from './case_schema'; +import { + PushToServiceApiParams, + ExternalServiceIncidentResponse, + ExternalServiceParams, +} from './types'; + +export interface CreateCommentRequest { + [key: string]: string; +} + +export type IncidentConfiguration = TypeOf; +export type MapRecord = TypeOf; +export type Comment = TypeOf; + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface TransformFieldsArgs { + params: PushToServiceApiParams; + fields: PipedField[]; + currentIncident?: ExternalServiceParams; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 8d9c6b92abb3b..da0c4ee36198f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -4,29 +4,120 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { TypeOf } from '@kbn/config-schema'; -import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionCreateIssueMetadataParamsSchema, + ExecutorSubActionGetCapabilitiesParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { IncidentConfigurationSchema } from './case_schema'; +import { PushToServiceResponse } from './case_types'; +import { Logger } from '../../../../../../src/core/server'; + +export type JiraPublicConfigurationType = TypeOf; +export type JiraSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type IncidentConfiguration = TypeOf; -export type JiraPublicConfigurationType = TypeOf; -export type JiraSecretConfigurationType = TypeOf; +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} -interface CreateIncidentBasicRequestArgs { - summary: string; - description: string; +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; } -interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { - project: { key: string }; - issuetype: { name: string }; + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export type ExternalServiceParams = Record; + +export interface ExternalService { + getIncident: (id: string) => Promise; + findIncidents: (params?: Record) => Promise; + createIncident: (params: ExternalServiceParams) => Promise; + updateIncident: (params: ExternalServiceParams) => Promise; + createComment: (params: ExternalServiceParams) => Promise; + getCreateIssueMetadata: () => Promise; + getCapabilities: () => Promise; +} + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalObject: Record; +} + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export type ExecutorSubActionCreateIssueMetadataParams = TypeOf< + typeof ExecutorSubActionCreateIssueMetadataParamsSchema +>; + +export type ExecutorSubActionGetCapabilitiesParams = TypeOf< + typeof ExecutorSubActionGetCapabilitiesParamsSchema +>; + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + secrets: Record; + logger: Logger; +} + +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; +} + +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; } -export interface CreateIncidentRequest { - fields: CreateIncidentRequestArgs; +export interface CreateIssueMetadataHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionCreateIssueMetadataParams; } -export interface UpdateIncidentRequest { - fields: Partial; +export interface GetCapabilitiesHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetCapabilitiesParams; } -export interface CreateCommentRequest { - body: string; +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + getCreateIssueMetadata: (args: CreateIssueMetadataHandlerArgs) => Promise; + getCapabilities: (args: GetCapabilitiesHandlerArgs) => Promise; } From 0fa0a3e9c730d3236c35e4a0b8c10775ae7272de Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 11:57:25 +0300 Subject: [PATCH 03/67] Transform comments on ServiceNow --- .../builtin_action_types/servicenow/api.ts | 23 +++++++++++++++++-- .../servicenow/case_types.ts | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 3281832941558..06b92233627cc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -14,7 +14,7 @@ import { // TODO: to remove, need to support Case import { transformers } from '../case/transformers'; -import { PushToServiceResponse, TransformFieldsArgs } from './case_types'; +import { PushToServiceResponse, TransformFieldsArgs, Comment } from './case_types'; import { prepareFieldsForTransformation } from '../case/utils'; const handshakeHandler = async ({ @@ -92,9 +92,10 @@ const pushToServiceHandler = async ({ mapping.get('comments')?.actionType !== 'nothing' ) { res.comments = []; + const commentsTransformed = transformComments(comments, ['informationAdded']); const fieldsKey = mapping.get('comments')?.target ?? 'comments'; - for (const currentComment of comments) { + for (const currentComment of commentsTransformed) { await externalService.updateIncident({ incidentId: res.id, incident: { @@ -140,6 +141,24 @@ export const transformFields = ({ }, {}); }; +export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { + return comments.map((c) => ({ + ...c, + comment: flow(...pipes.map((p) => transformers[p]))({ + value: c.comment, + date: c.updatedAt ?? c.createdAt, + user: + (c.updatedBy != null + ? c.updatedBy.fullName + ? c.updatedBy.fullName + : c.updatedBy.username + : c.createdBy.fullName + ? c.createdBy.fullName + : c.createdBy.username) ?? '', + }).value, + })); +}; + export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts index 49b85f9254af9..23a72ad4a894b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts @@ -9,7 +9,7 @@ import { ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, } from './schema'; -import { IncidentConfigurationSchema, MapRecordSchema } from './case_shema'; +import { IncidentConfigurationSchema, MapRecordSchema, CommentSchema } from './case_shema'; import { PushToServiceApiParams, ExternalServiceIncidentResponse, @@ -22,6 +22,7 @@ export interface CreateCommentRequest { export type IncidentConfiguration = TypeOf; export type MapRecord = TypeOf; +export type Comment = TypeOf; export interface ExternalServiceCommentResponse { commentId: string; From aa21313ed82d0e3242b313365a98ac24f347ec28 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 11:59:14 +0300 Subject: [PATCH 04/67] Fix service --- .../builtin_action_types/jira/service.ts | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index aec73cfb375ed..1412b93c14c55 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -6,15 +6,10 @@ import axios from 'axios'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import { Logger } from '../../../../../../src/core/server'; -import { - JiraPublicConfigurationType, - JiraSecretConfigurationType, - CreateIncidentRequest, - UpdateIncidentRequest, - CreateCommentRequest, -} from './types'; +import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; +import { Comment } from './case_types'; import * as i18n from './translations'; import { request, getErrorMessage } from '../lib/axios_utils'; @@ -72,18 +67,26 @@ export const createExternalService = ( } }; + const findIncidents = async (params?: Record) => { + return undefined; + }; + const createIncident = async ({ incident }: ExternalServiceParams) => { // The response from Jira when creating an issue contains only the key and the id. // The function makes two calls when creating an issue. One to create the issue and one to get // the created issue with all the necessary fields. try { - const res = await request({ + const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, logger, method: 'post', data: { - fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, + fields: { + ...(incident as Record), + project: { key: projectKey }, + issuetype: { name: 'Task' }, + }, }, proxySettings, }); @@ -105,16 +108,16 @@ export const createExternalService = ( const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { try { - await request({ + await request({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, logger, - data: { fields: { ...incident } }, + data: { fields: { ...(incident as Record) } }, proxySettings, }); - const updatedIncident = await getIncident(incidentId); + const updatedIncident = await getIncident(incidentId as string); return { title: updatedIncident.key, @@ -132,19 +135,21 @@ export const createExternalService = ( } }; - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + const createComment = async ({ incidentId, comment }: ExternalServiceParams) => { + const commentTyped = comment as Comment; + try { - const res = await request({ + const res = await request({ axios: axiosInstance, method: 'post', - url: getCommentsURL(incidentId), + url: getCommentsURL(incidentId as string), logger, - data: { body: comment.comment }, + data: { body: commentTyped.comment }, proxySettings, }); return { - commentId: comment.commentId, + commentId: commentTyped.commentId, externalCommentId: res.data.id, pushedDate: new Date(res.data.created).toISOString(), }; @@ -158,10 +163,21 @@ export const createExternalService = ( } }; + const getCreateIssueMetadata = async () => { + return undefined; + }; + + const getCapabilities = async () => { + return undefined; + }; + return { getIncident, createIncident, updateIncident, createComment, + findIncidents, + getCreateIssueMetadata, + getCapabilities, }; }; From d466e33c4cbdff3b3a1b34372452d1484fa0ffab Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:00:13 +0300 Subject: [PATCH 05/67] Fix validators --- .../builtin_action_types/jira/translations.ts | 16 +++++++++ .../builtin_action_types/jira/validators.ts | 34 +++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts index dae0d75952e11..2e70bc3a71a6d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', { defaultMessage: 'Jira', }); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.jira.configuration.apiWhitelistError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); + +// TODO: remove when Case mappings will be removed +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.jira.configuration.emptyMapping', + { + defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index 7226071392bc6..7f4bdac3b56e9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -4,8 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ExternalServiceValidation } from '../case/types'; +import { isEmpty } from 'lodash'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: JiraPublicConfigurationType +) => { + if ( + configObject.incidentConfiguration !== null && + isEmpty(configObject.incidentConfiguration.mapping) + ) { + return i18n.MAPPING_EMPTY; + } + + try { + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: JiraSecretConfigurationType +) => {}; export const validate: ExternalServiceValidation = { config: validateCommonConfig, From c7a63bd420e970e3a4c0f3a55242a93ac232c2af Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:01:05 +0300 Subject: [PATCH 06/67] Fix api --- .../server/builtin_action_types/jira/api.ts | 177 +++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 3db66e5884af4..e0c675d090e25 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -4,4 +4,179 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../case/api'; +import { flow } from 'lodash'; +import { + ExternalServiceParams, + PushToServiceApiHandlerArgs, + HandshakeApiHandlerArgs, + GetIncidentApiHandlerArgs, + CreateIssueMetadataHandlerArgs, + GetCapabilitiesHandlerArgs, + ExternalServiceApi, +} from './types'; + +// TODO: to remove, need to support Case +import { transformers } from '../case/transformers'; +import { PushToServiceResponse, TransformFieldsArgs, Comment } from './case_types'; +import { prepareFieldsForTransformation } from '../case/utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; + +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const getCreateIssueMetadataHandler = async ({ + externalService, + mapping, + params, +}: CreateIssueMetadataHandlerArgs) => {}; + +const getCapabilitiesHandler = async ({ + externalService, + mapping, + params, +}: GetCapabilitiesHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, + secrets, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + try { + currentIncident = await externalService.getIncident(externalId); + } catch (ex) { + logger.debug( + `Retrieving Incident by id ${externalId} from ServiceNow was failed with exception: ${ex}` + ); + } + } + + let incident = {}; + // TODO: should be removed later but currently keep it for the Case implementation support + if (mapping && Array.isArray(params.comments)) { + const fields = prepareFieldsForTransformation({ + externalCase: params.externalObject, + mapping, + defaultPipes, + }); + + incident = transformFields({ + params, + fields, + currentIncident, + }); + } else { + incident = { ...params, summary: params.title }; + } + + if (updateIncident) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident: { + ...incident, + caller_id: secrets.username, + }, + }); + } + + // TODO: should temporary keep comments for a Case usage + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping && + mapping.get('comments')?.actionType !== 'nothing' + ) { + const commentsTransformed = transformComments(comments, ['informationAdded']); + + res.comments = []; + for (const currentComment of commentsTransformed) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + field: mapping.get('comments')?.target ?? 'comments', + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Record => { + return fields.reduce((prev, cur) => { + const transform = flow(...cur.pipes.map((p) => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + (params.updatedBy != null + ? params.updatedBy.fullName + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username) ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {}); +}; + +export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { + return comments.map((c) => ({ + ...c, + comment: flow(...pipes.map((p) => transformers[p]))({ + value: c.comment, + date: c.updatedAt ?? c.createdAt, + user: + (c.updatedBy != null + ? c.updatedBy.fullName + ? c.updatedBy.fullName + : c.updatedBy.username + : c.createdBy.fullName + ? c.createdBy.fullName + : c.createdBy.username) ?? '', + }).value, + })); +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, + getCreateIssueMetadata: getCreateIssueMetadataHandler, + getCapabilities: getCapabilitiesHandler, +}; From 2dbe4e834b1acdf0d1f38a46c3caf608a1eec015 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:01:44 +0300 Subject: [PATCH 07/67] Fix init --- .../builtin_action_types/jira/config.ts | 14 --- .../server/builtin_action_types/jira/index.ts | 109 ++++++++++++++---- 2 files changed, 87 insertions(+), 36 deletions(-) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/config.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts deleted file mode 100644 index 54f28e447010a..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { ExternalServiceConfiguration } from '../case/types'; -import * as i18n from './translations'; - -export const config: ExternalServiceConfiguration = { - id: '.jira', - name: i18n.NAME, - minimumLicenseRequired: 'gold', -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 66be0bad02d7b..00e2efb7a7cc7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,33 +4,98 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../../src/core/server'; -import { createConnector } from '../case/utils'; -import { ActionType } from '../../types'; +import { curry } from 'lodash'; +import { schema } from '@kbn/config-schema'; -import { api } from './api'; -import { config } from './config'; import { validate } from './validators'; -import { createExternalService } from './service'; -import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParamsSchema, +} from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { createExternalService } from './service'; +import { api } from './api'; +import { ExecutorParams, ExecutorSubActionPushParams } from './types'; +import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; -export function getActionType({ - logger, - configurationUtilities, -}: { +// TODO: to remove, need to support Case +import { buildMap, mapParams } from '../case/utils'; +import { PushToServiceResponse } from './case_types'; + +interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { - return createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: JiraPublicConfiguration, - secrets: JiraSecretConfiguration, +} + +// action type definition +export function getActionType(params: GetActionTypeParams): ActionType { + const { logger, configurationUtilities } = params; + return { + id: '.jira', + minimumLicenseRequired: 'gold', + name: i18n.NAME, + validate: { + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, }, - logger, - })({ configurationUtilities }); + executor: curry(executor)({ logger }), + }; +} + +// action executor +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: PushToServiceResponse | null = null; + + const externalService = createExternalService({ + config, + secrets, + }); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction !== 'pushToService') { + const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + const { comments, externalId, ...restParams } = pushToServiceParams; + const mapping = config.incidentConfiguration + ? buildMap(config.incidentConfiguration.mapping) + : null; + const externalObject = + config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalObject }, + secrets, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + return { status: 'ok', data: data ?? {}, actionId }; } From ae3d43a1cfadf94352b747ec724a30839763efb6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:42:44 +0300 Subject: [PATCH 08/67] Init jira ui --- .../components/builtin_action_types/jira/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts new file mode 100644 index 0000000000000..a0170f9d84e9b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { getActionType as getJiraActionType } from './jira'; From d4895bfbddb143f253fb7901af6fc99d6b59447d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:43:25 +0300 Subject: [PATCH 09/67] Add cases mappings ui --- .../jira/case_mappings/field_mapping.tsx | 141 +++++++++++++ .../jira/case_mappings/field_mapping_row.tsx | 78 +++++++ .../jira/case_mappings/translations.ts | 190 ++++++++++++++++++ .../jira/case_mappings/types.ts | 16 ++ .../jira/case_mappings/utils.ts | 38 ++++ 5 files changed, 463 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping_row.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/translations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx new file mode 100644 index 0000000000000..ddab6c5b31a4f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx @@ -0,0 +1,141 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FieldMappingRow } from './field_mapping_row'; +import * as i18n from './translations'; + +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types'; +import { CasesConfigurationMapping } from '../types'; +import { connectorConfiguration } from '../config'; +import { createDefaultMapping } from '../jira_connectors'; + +const FieldRowWrapper = styled.div` + margin-top: 8px; + font-size: 14px; +`; + +const actionTypeOptions: Array> = [ + { + value: 'nothing', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}, + 'data-test-subj': 'edit-update-option-nothing', + }, + { + value: 'overwrite', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}, + 'data-test-subj': 'edit-update-option-overwrite', + }, + { + value: 'append', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}, + 'data-test-subj': 'edit-update-option-append', + }, +]; + +const getThirdPartyOptions = ( + caseField: string, + thirdPartyFields: Record +): Array> => + (Object.keys(thirdPartyFields) as string[]).reduce>>( + (acc, key) => { + if (thirdPartyFields[key].validSourceFields.includes(caseField)) { + return [ + ...acc, + { + value: key, + inputDisplay: {thirdPartyFields[key].label}, + 'data-test-subj': `dropdown-mapping-${key}`, + }, + ]; + } + return acc; + }, + [ + { + value: 'not_mapped', + inputDisplay: i18n.MAPPING_FIELD_NOT_MAPPED, + 'data-test-subj': 'dropdown-mapping-not_mapped', + }, + ] + ); + +export interface FieldMappingProps { + disabled: boolean; + mapping: CasesConfigurationMapping[] | null; + connectorActionTypeId: string; + onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; +} + +const FieldMappingComponent: React.FC = ({ + disabled, + mapping, + onChangeMapping, + connectorActionTypeId, +}) => { + const onChangeActionType = useCallback( + (caseField: string, newActionType: string) => { + const myMapping = mapping ?? defaultMapping; + onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [mapping] + ); + + const onChangeThirdParty = useCallback( + (caseField: string, newThirdPartyField: string) => { + const myMapping = mapping ?? defaultMapping; + onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [mapping] + ); + + const selectedConnector = connectorConfiguration ?? { fields: {} }; + const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ + selectedConnector.fields, + ]); + + return ( + <> + + + + {i18n.FIELD_MAPPING_FIRST_COL} + + + {i18n.FIELD_MAPPING_SECOND_COL} + + + {i18n.FIELD_MAPPING_THIRD_COL} + + + + + {(mapping ?? defaultMapping).map((item) => ( + + ))} + + + ); +}; + +export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping_row.tsx new file mode 100644 index 0000000000000..beca8f1fbbc77 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping_row.tsx @@ -0,0 +1,78 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiSuperSelect, + EuiIcon, + EuiSuperSelectOption, +} from '@elastic/eui'; + +import { capitalize } from 'lodash'; + +export interface RowProps { + id: string; + disabled: boolean; + securitySolutionField: string; + thirdPartyOptions: Array>; + actionTypeOptions: Array>; + onChangeActionType: (caseField: string, newActionType: string) => void; + onChangeThirdParty: (caseField: string, newThirdPartyField: string) => void; + selectedActionType: string; + selectedThirdParty: string; +} + +const FieldMappingRowComponent: React.FC = ({ + id, + disabled, + securitySolutionField, + thirdPartyOptions, + actionTypeOptions, + onChangeActionType, + onChangeThirdParty, + selectedActionType, + selectedThirdParty, +}) => { + const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ + securitySolutionField, + ]); + return ( + + + + + {securitySolutionFieldCapitalized} + + + + + + + + + + + + + + ); +}; + +export const FieldMappingRow = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/translations.ts new file mode 100644 index 0000000000000..665ccbcfa114d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/translations.ts @@ -0,0 +1,190 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to external incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const NO_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.noConnector', + { + defaultMessage: 'No connector selected', + } +); + +export const ADD_NEW_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector', + { + defaultMessage: 'Add new connector', + } +); + +export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle', + { + defaultMessage: 'Case Closures', + } +); + +export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc', + { + defaultMessage: + 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', + } +); + +export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel', + { + defaultMessage: 'Case closure options', + } +); + +export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual', + { + defaultMessage: 'Manually close Security cases', + } +); + +export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident', + { + defaultMessage: + 'Automatically close Security cases when pushing new incident to external system', + } +); + +export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident', + { + defaultMessage: 'Automatically close Security cases when incident is closed in external system', + } +); + +export const FIELD_MAPPING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle', + { + defaultMessage: 'Field mappings', + } +); + +export const FIELD_MAPPING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc', + { + defaultMessage: + 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', + } +); + +export const FIELD_MAPPING_FIRST_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol', + { + defaultMessage: 'Security case field', + } +); + +export const FIELD_MAPPING_SECOND_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol', + { + defaultMessage: 'External incident field', + } +); + +export const FIELD_MAPPING_THIRD_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol', + { + defaultMessage: 'On edit and update', + } +); + +export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing', + { + defaultMessage: 'Nothing', + } +); + +export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite', + { + defaultMessage: 'Overwrite', + } +); + +export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend', + { + defaultMessage: 'Append', + } +); + +export const CANCEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.cancelButton', + { + defaultMessage: 'Cancel', + } +); + +export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.warningTitle', + { + defaultMessage: 'Warning', + } +); + +export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.warningMessage', + { + defaultMessage: + 'The selected connector has been deleted. Either select a different connector or create a new one.', + } +); + +export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped', + { + defaultMessage: 'Not mapped', + } +); + +export const UPDATE_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.updateConnector', + { + defaultMessage: 'Update connector', + } +); + +export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { + return i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector', + { + values: { connectorName }, + defaultMessage: 'Update { connectorName }', + } + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts new file mode 100644 index 0000000000000..6cd2200e1dc74 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts @@ -0,0 +1,16 @@ +/* + * 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 { ActionType } from '../../../../../types'; + +export { ActionType }; + +export interface ThirdPartyField { + label: string; + validSourceFields: string[]; + defaultSourceField: string; + defaultActionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts new file mode 100644 index 0000000000000..a173d90515302 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts @@ -0,0 +1,38 @@ +/* + * 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 { CasesConfigurationMapping } from '../types'; + +export const setActionTypeToMapping = ( + caseField: string, + newActionType: string, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => { + const findItemIndex = mapping.findIndex((item) => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: string, + newThirdPartyField: string, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => + mapping.map((item) => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + }); From 66babbd9ef228f4c4c83e22d0e925bfe6059059a Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:44:38 +0300 Subject: [PATCH 10/67] Add types (ui) --- .../builtin_action_types/jira/types.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts new file mode 100644 index 0000000000000..def44e603c67c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts @@ -0,0 +1,47 @@ +/* + * 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. + */ + +export interface JiraActionConnector { + config: JiraConfig; + secrets: JiraSecrets; +} + +export interface JiraActionParams { + subAction: string; + subActionParams: { + savedObjectId: string; + title: string; + description: string; + comments: string; + externalId: string | null; + issueType: string; + priority: string; + labels: string[]; + }; +} + +interface IncidentConfiguration { + mapping: CasesConfigurationMapping[]; +} + +interface JiraConfig { + apiUrl: string; + projectKey: string; + incidentConfiguration?: IncidentConfiguration; + isCaseOwned?: boolean; +} + +interface JiraSecrets { + email: string; + apiToken: string; +} + +// to remove +export interface CasesConfigurationMapping { + source: string; + target: string; + actionType: string; +} From 9f73f58a5682bbf4b7cf17e3f1103a53c47d9426 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:45:05 +0300 Subject: [PATCH 11/67] Add logo --- .../components/builtin_action_types/jira/logo.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg new file mode 100644 index 0000000000000..8560cf7e270c8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 312306f76d0a2a62afc6d6c0fd858c5b4dcd4d77 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:45:46 +0300 Subject: [PATCH 12/67] Add translations (ui) --- .../builtin_action_types/jira/translations.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts new file mode 100644 index 0000000000000..106057e4511dd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -0,0 +1,119 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const JIRA_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', + { + defaultMessage: 'Push or update Security case data to a new issue in Jira', + } +); + +export const JIRA_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle', + { + defaultMessage: 'Jira', + } +); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const JIRA_PROJECT_KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey', + { + defaultMessage: 'Project key', + } +); + +export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField', + { + defaultMessage: 'Project key is required', + } +); + +export const JIRA_EMAIL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', + { + defaultMessage: 'Email or Username', + } +); + +export const JIRA_EMAIL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', + { + defaultMessage: 'Email or Username is required', + } +); + +export const JIRA_API_TOKEN_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', + { + defaultMessage: 'API token or Password', + } +); + +export const JIRA_API_TOKEN_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', + { + defaultMessage: 'API token or Password is required', + } +); + +export const MAPPING_FIELD_SUMMARY = i18n.translate( + 'xpack.securitySolution.case.configureCases.mappingFieldSummary', + { + defaultMessage: 'Summary', + } +); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField', + { + defaultMessage: 'Description is required.', + } +); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField', + { + defaultMessage: 'Title is required.', + } +); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); From c2d97126428c96e50e7f96d3464456763c6de180 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:46:23 +0300 Subject: [PATCH 13/67] Add config (ui) --- .../builtin_action_types/jira/config.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts new file mode 100644 index 0000000000000..94a95a8d22629 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts @@ -0,0 +1,38 @@ +/* + * 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 * as i18n from './translations'; +import logo from './logo.svg'; + +export const connectorConfiguration = { + id: '.jira', + name: i18n.JIRA_TITLE, + logo, + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + fields: { + summary: { + label: i18n.MAPPING_FIELD_SUMMARY, + validSourceFields: ['title', 'description'], + defaultSourceField: 'title', + defaultActionType: 'overwrite', + }, + description: { + label: i18n.MAPPING_FIELD_DESC, + validSourceFields: ['title', 'description'], + defaultSourceField: 'description', + defaultActionType: 'overwrite', + }, + comments: { + label: i18n.MAPPING_FIELD_COMMENTS, + validSourceFields: ['comments'], + defaultSourceField: 'comments', + defaultActionType: 'append', + }, + }, +}; From 89f5ce5faf7a6db8cef31943d703ef170175abb3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:53:56 +0300 Subject: [PATCH 14/67] Create jira connector flyout --- .../jira/jira_connectors.tsx | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx new file mode 100644 index 0000000000000..0789821b30a05 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -0,0 +1,218 @@ +/* + * 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, { useCallback } from 'react'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import { JiraActionConnector, CasesConfigurationMapping } from './types'; +import { connectorConfiguration } from './config'; +import { FieldMapping } from './case_mappings/field_mapping'; + +const ServiceNowConnectorFields: React.FC> = ({ + action, + editActionSecrets, + editActionConfig, + errors, + consumer, + readOnly, + docLinks, +}) => { + // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution + const { apiUrl, projectKey, incidentConfiguration, isCaseOwned } = action.config; + const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + const { email, apiToken } = action.secrets; + + const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; + const isEmailInvalid: boolean = errors.email.length > 0 && email != null; + const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; + + // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + if (consumer === 'case') { + if (isEmpty(mapping)) { + editActionConfig('incidentConfiguration', { + mapping: createDefaultMapping(connectorConfiguration.fields as any), + }); + } + if (!isCaseOwned) { + editActionConfig('isCaseOwned', true); + } + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('incidentConfiguration', { + ...action.config.incidentConfiguration, + mapping: newMapping, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + + handleOnChangeActionConfig('projectKey', evt.target.value)} + onBlur={() => { + if (!projectKey) { + editActionConfig('projectKey', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('email', evt.target.value)} + onBlur={() => { + if (!email) { + editActionSecrets('email', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('apiToken', evt.target.value)} + onBlur={() => { + if (!apiToken) { + editActionSecrets('apiToken', ''); + } + }} + /> + + + + {consumer === 'case' && ( // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + <> + + + + + + + + )} + + ); +}; + +export const createDefaultMapping = (fields: Record): CasesConfigurationMapping[] => + Object.keys(fields).map( + (key) => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); + +// eslint-disable-next-line import/no-default-export +export { ServiceNowConnectorFields as default }; From 7550f465f6734ad0b368c5baef97f069e16bd157 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:54:30 +0300 Subject: [PATCH 15/67] Create jira alerts flyout --- .../builtin_action_types/jira/jira_params.tsx | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx new file mode 100644 index 0000000000000..82061e9233194 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -0,0 +1,208 @@ +/* + * 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, useEffect } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { ActionParamsProps } from '../../../../types'; +import { JiraActionParams } from './types'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; + +const JiraParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, +}) => { + const { title, description, comments, issueType, priority, labels, savedObjectId } = + actionParams.subActionParams || {}; + + const selectOptions = [ + { + value: '1', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectHighOptionLabel', + { + defaultMessage: 'High', + } + ), + }, + { + value: '2', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectMediumOptionLabel', + { + defaultMessage: 'Medium', + } + ), + }, + { + value: '3', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectLawOptionLabel', + { + defaultMessage: 'Low', + } + ), + }, + ]; + + const editSubActionProperty = (key: string, value: {}) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }; + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!savedObjectId && messageVariables?.find((variable) => variable.name === 'alertId')) { + editSubActionProperty('savedObjectId', '{{alertId}}'); + } + if (!issueType) { + editSubActionProperty('issueType', '3'); + } + if (!priority) { + editSubActionProperty('priority', '3'); + } + if (!labels) { + editSubActionProperty('labels', '3'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [title, description, comments, issueType, priority, labels]); + + return ( + + +

Incident

+
+ + + { + editSubActionProperty('issueType', e.target.value); + }} + /> + + + + + + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + + { + editSubActionProperty('labels', e.target.value); + }} + /> + + + + + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', + { + defaultMessage: 'Summary', + } + )} + > + + + + +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { JiraParamsFields as default }; From 992cf52b64563df616a46f59c0bef04acd472332 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:55:29 +0300 Subject: [PATCH 16/67] Unregister jira connector from security solutions --- x-pack/plugins/security_solution/public/plugin.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 1017cbb6a2c61..10bbbbfa72719 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -21,7 +21,7 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { jiraActionType, resilientActionType } from './common/lib/connectors'; +import { resilientActionType } from './common/lib/connectors'; import { PluginSetup, PluginStart, @@ -96,7 +96,6 @@ export class Plugin implements IPlugin { From cc0f84b34bd92c6e00df7d4f3256bdf33fdf15da Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 12:55:57 +0300 Subject: [PATCH 17/67] Register jira (ui) --- .../components/builtin_action_types/index.ts | 2 + .../builtin_action_types/jira/jira.tsx | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index c241997e99dd7..130e4496c4d04 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -13,6 +13,7 @@ import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; import { getServiceNowActionType } from './servicenow'; +import { getJiraActionType } from './jira'; export function registerBuiltInActionTypes({ actionTypeRegistry, @@ -26,4 +27,5 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getPagerDutyActionType()); actionTypeRegistry.register(getWebhookActionType()); actionTypeRegistry.register(getServiceNowActionType()); + actionTypeRegistry.register(getJiraActionType()); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx new file mode 100644 index 0000000000000..fd36bd6aeab0a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -0,0 +1,69 @@ +/* + * 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 { lazy } from 'react'; +import { ValidationResult, ActionTypeModel } from '../../../../types'; +import { connectorConfiguration } from './config'; +import logo from './logo.svg'; +import { JiraActionConnector, JiraActionParams } from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +const validateConnector = (action: JiraActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: new Array(), + projectKey: new Array(), + email: new Array(), + apiToken: new Array(), + }; + validationResult.errors = errors; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + if (!action.config.projectKey) { + errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + } + + if (!action.secrets.email) { + errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED]; + } + + if (!action.secrets.apiToken) { + errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED]; + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel { + return { + id: connectorConfiguration.id, + iconClass: logo, + selectMessage: i18n.JIRA_DESC, + actionTypeTitle: connectorConfiguration.name, + validateConnector, + actionConnectorFields: lazy(() => import('./jira_connectors')), + validateParams: (actionParams: JiraActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + title: new Array(), + }; + validationResult.errors = errors; + if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + errors.title.push(i18n.TITLE_REQUIRED); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./jira_params')), + }; +} From 5f2a7d7b4d69c1e4d4594a5d5f98666144c72f21 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 13:42:44 +0300 Subject: [PATCH 18/67] Filter out non jira case connectors --- x-pack/plugins/case/common/constants.ts | 8 +++++++- .../server/routes/api/cases/configure/get_connectors.ts | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index bd12c258a5388..15a318002390f 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -28,5 +28,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; export const SERVICENOW_ACTION_TYPE_ID = '.servicenow'; +export const JIRA_ACTION_TYPE_ID = '.jira'; +export const RESILIENT_ACTION_TYPE_ID = '.resilient'; -export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient']; +export const SUPPORTED_CONNECTORS = [ + SERVICENOW_ACTION_TYPE_ID, + JIRA_ACTION_TYPE_ID, + RESILIENT_ACTION_TYPE_ID, +]; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 28e75dd2f8c32..381d5a8da4970 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -12,6 +12,7 @@ import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS, SERVICENOW_ACTION_TYPE_ID, + JIRA_ACTION_TYPE_ID, } from '../../../../../common/constants'; /* @@ -36,8 +37,9 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou (action) => SUPPORTED_CONNECTORS.includes(action.actionTypeId) && // Need this filtering temporary to display only Case owned ServiceNow connectors - (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID || - (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned)) + (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) || + ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) && + action.config!.isCaseOwned)) ); return response.ok({ body: results }); } catch (error) { From 5267b9fd842817034da0c30a9cc09e0855e91dc1 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 19:33:21 +0300 Subject: [PATCH 19/67] Implement getCreateIssueMetadata subAction --- .../server/builtin_action_types/jira/api.ts | 14 ++--- .../server/builtin_action_types/jira/index.ts | 21 ++++++- .../builtin_action_types/jira/schema.ts | 5 -- .../builtin_action_types/jira/service.ts | 61 ++++++++++++++++++- .../server/builtin_action_types/jira/types.ts | 17 ++++-- 5 files changed, 92 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index e0c675d090e25..0fe6a087f8ff6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -11,7 +11,6 @@ import { HandshakeApiHandlerArgs, GetIncidentApiHandlerArgs, CreateIssueMetadataHandlerArgs, - GetCapabilitiesHandlerArgs, ExternalServiceApi, } from './types'; @@ -34,15 +33,11 @@ const getIncidentHandler = async ({ const getCreateIssueMetadataHandler = async ({ externalService, - mapping, params, -}: CreateIssueMetadataHandlerArgs) => {}; - -const getCapabilitiesHandler = async ({ - externalService, - mapping, - params, -}: GetCapabilitiesHandlerArgs) => {}; +}: CreateIssueMetadataHandlerArgs) => { + const res = await externalService.getCreateIssueMetadata(); + return res; +}; const pushToServiceHandler = async ({ externalService, @@ -178,5 +173,4 @@ export const api: ExternalServiceApi = { pushToService: pushToServiceHandler, getIncident: getIncidentHandler, getCreateIssueMetadata: getCreateIssueMetadataHandler, - getCapabilities: getCapabilitiesHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 00e2efb7a7cc7..b96641f817190 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -17,7 +17,12 @@ import { ActionsConfigurationUtilities } from '../../actions_config'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { createExternalService } from './service'; import { api } from './api'; -import { ExecutorParams, ExecutorSubActionPushParams } from './types'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + ExecutorSubActionCreateIssueMetadataParams, + GetCreateIssueMetadataResponse, +} from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; @@ -30,6 +35,8 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } +const supportedSubActions: string[] = ['pushToService', 'getCreateIssueMetadata']; + // action type definition export function getActionType(params: GetActionTypeParams): ActionType { const { logger, configurationUtilities } = params; @@ -57,7 +64,7 @@ async function executor( ): Promise { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params as ExecutorParams; - let data: PushToServiceResponse | null = null; + let data: PushToServiceResponse | GetCreateIssueMetadataResponse | null = null; const externalService = createExternalService({ config, @@ -70,7 +77,7 @@ async function executor( throw new Error(errorMessage); } - if (subAction !== 'pushToService') { + if (!supportedSubActions.includes(subAction)) { const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; logger.error(errorMessage); throw new Error(errorMessage); @@ -97,5 +104,13 @@ async function executor( logger.debug(`response push to service for incident id: ${data.id}`); } + if (subAction === 'getCreateIssueMetadata') { + const getCreateIssueMetadataParams = subActionParams as ExecutorSubActionCreateIssueMetadataParams; + data = await api.getCreateIssueMetadata({ + externalService, + params: getCreateIssueMetadataParams, + }); + } + return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 9056affe7f5d2..533d837de9118 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -33,7 +33,6 @@ export const ExecutorSubActionSchema = schema.oneOf([ schema.literal('pushToService'), schema.literal('handshake'), schema.literal('getCreateIssueMetadata'), - schema.literal('getCapabilities'), ]); export const ExecutorSubActionPushParamsSchema = schema.object({ @@ -75,8 +74,4 @@ export const ExecutorParamsSchema = schema.oneOf([ subAction: schema.literal('getCreateIssueMetadata'), subActionParams: ExecutorSubActionCreateIssueMetadataParamsSchema, }), - schema.object({ - subAction: schema.literal('getCapabilities'), - subActionParams: ExecutorSubActionGetCapabilitiesParamsSchema, - }), ]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 1412b93c14c55..7e40eb26e4886 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -17,6 +17,7 @@ import { ProxySettings } from '../../types'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; +const CAPABILITIES_URL = `rest/capabilities`; const INCIDENT_URL = `issue`; const COMMENT_URL = `comment`; @@ -35,7 +36,9 @@ export const createExternalService = ( } const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; + const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`; const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; + const createIssueMetadataUrl = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; const axiosInstance = axios.create({ auth: { username: email, password: apiToken }, }); @@ -164,11 +167,65 @@ export const createExternalService = ( }; const getCreateIssueMetadata = async () => { - return undefined; + try { + const capabilitiesResponse = await getCapabilities(); + + const capabilities = Object.keys(capabilitiesResponse?.capabilities ?? []); + const supportsNewAPI = createMetaCapabilities.every((c) => capabilities.includes(c)); + + if (!supportsNewAPI) { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createIssueMetadataUrl, + }); + + const issueTypes = res.data.projects[0].issuetypes; + const metadata = issueTypes.reduce((acc, currentIssueType) => { + const fields = Object.keys(currentIssueType.fields).reduce((fieldsAcc, fieldKey) => { + return { + ...fieldsAcc, + [fieldKey]: { + allowedValues: currentIssueType.fields[fieldKey].allowedValues ?? [], + defaultValue: currentIssueType.fields[fieldKey].defaultValue ?? {}, + }, + }; + }, {}); + + return { + ...acc, + [currentIssueType.id]: { + name: currentIssueType.name, + fields, + }, + }; + }, {}); + + return { issueTypes: metadata }; + } + + return { issueTypes: [] }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get create issue metadata. Error: ${error.message}`) + ); + } }; const getCapabilities = async () => { - return undefined; + try { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: capabilitiesUrl, + }); + + return { ...res.data }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get capabilities. Error: ${error.message}`) + ); + } }; return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index da0c4ee36198f..1d369ffc83e76 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -63,8 +63,8 @@ export interface ExternalService { createIncident: (params: ExternalServiceParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; createComment: (params: ExternalServiceParams) => Promise; - getCreateIssueMetadata: () => Promise; - getCapabilities: () => Promise; + getCreateIssueMetadata: () => Promise; + getCapabilities: () => Promise; } export interface PushToServiceApiParams extends ExecutorSubActionPushParams { @@ -106,18 +106,23 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: ExecutorSubActionHandshakeParams; } -export interface CreateIssueMetadataHandlerArgs extends ExternalServiceApiHandlerArgs { +export interface CreateIssueMetadataHandlerArgs { + externalService: ExternalService; params: ExecutorSubActionCreateIssueMetadataParams; } -export interface GetCapabilitiesHandlerArgs extends ExternalServiceApiHandlerArgs { +export interface GetCapabilitiesHandlerArgs { + externalService: ExternalService; params: ExecutorSubActionGetCapabilitiesParams; } +export type GetCreateIssueMetadataResponse = Record; + export interface ExternalServiceApi { handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; - getCreateIssueMetadata: (args: CreateIssueMetadataHandlerArgs) => Promise; - getCapabilities: (args: GetCapabilitiesHandlerArgs) => Promise; + getCreateIssueMetadata: ( + args: CreateIssueMetadataHandlerArgs + ) => Promise; } From 155986ed381bafbc3781bfc703cecf3ff64c4694 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 30 Jul 2020 19:34:38 +0300 Subject: [PATCH 20/67] Get create issue metadata (ui) --- .../builtin_action_types/jira/api.ts | 22 +++++++++++++++++++ .../builtin_action_types/jira/jira_params.tsx | 22 ++++++++++++++++++- .../action_connector_form/action_form.tsx | 1 + .../triggers_actions_ui/public/types.ts | 1 + 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts new file mode 100644 index 0000000000000..620d7dca5c018 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from 'kibana/public'; +import { BASE_ACTION_API_PATH } from '../../../constants'; + +export async function getCreateIssueMetadata({ + http, + connectorId, +}: { + http: HttpSetup; + connectorId: string; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'getCreateIssueMetadata', subActionParams: {} }, + }), + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 82061e9233194..717179b2280b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -12,10 +12,13 @@ import { EuiFlexGroup } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; import { EuiTitle } from '@elastic/eui'; + +import { useAppDependencies } from '../../../app_context'; import { ActionParamsProps } from '../../../../types'; -import { JiraActionParams } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +import { JiraActionParams } from './types'; +import { getCreateIssueMetadata } from './api'; const JiraParamsFields: React.FunctionComponent> = ({ actionParams, @@ -23,7 +26,9 @@ const JiraParamsFields: React.FunctionComponent { + const { http } = useAppDependencies(); const { title, description, comments, issueType, priority, labels, savedObjectId } = actionParams.subActionParams || {}; @@ -62,6 +67,21 @@ const JiraParamsFields: React.FunctionComponent { + let cancel = false; + const fetchData = async () => { + const createIssueMetadata = await getCreateIssueMetadata({ + http, + connectorId: actionConnector.id, + }); + console.log('createIssueMetadata', createIssueMetadata); + }; + fetchData(); + return () => { + cancel = true; + }; + }, [http, actionConnector]); + useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 2d4507ca93078..ac5b2a2187c2f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -311,6 +311,7 @@ export const ActionForm = ({ messageVariables={messageVariables} defaultMessage={defaultActionMessage ?? undefined} docLinks={docLinks} + actionConnector={actionConnector} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 762f41ba3691c..cc4eb5aba3eaa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -54,6 +54,7 @@ export interface ActionParamsProps { messageVariables?: ActionVariable[]; defaultMessage?: string; docLinks: DocLinksStart; + actionConnector: ActionConnector; } export interface Pagination { From 9d954833de2f95ad09772fd58ca4fddb797894a6 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jul 2020 16:54:28 +0300 Subject: [PATCH 21/67] Return issue types by name --- .../actions/server/builtin_action_types/jira/service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 7e40eb26e4886..405c6bf93048e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -194,7 +194,8 @@ export const createExternalService = ( return { ...acc, - [currentIssueType.id]: { + [currentIssueType.name]: { + id: currentIssueType.id, name: currentIssueType.name, fields, }, From 94c8908dff9112a53f824b78beaef0d143be8c19 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 31 Jul 2020 16:55:37 +0300 Subject: [PATCH 22/67] Fetch and populate create issue fields --- .../builtin_action_types/jira/api.ts | 2 +- .../builtin_action_types/jira/jira_params.tsx | 233 +++++++++--------- .../builtin_action_types/jira/types.ts | 2 +- .../jira/use_create_issue_metadata.tsx | 117 +++++++++ 4 files changed, 239 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts index 620d7dca5c018..fc655a85e4ac7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts @@ -13,7 +13,7 @@ export async function getCreateIssueMetadata({ }: { http: HttpSetup; connectorId: string; -}): Promise> { +}): Promise> { return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'getCreateIssueMetadata', subActionParams: {} }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 717179b2280b2..d6a8d07581e0a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment, useEffect } from 'react'; -import { EuiFormRow } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; @@ -18,7 +18,7 @@ import { ActionParamsProps } from '../../../../types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { JiraActionParams } from './types'; -import { getCreateIssueMetadata } from './api'; +import { useCreateIssueMetadata } from './use_create_issue_metadata'; const JiraParamsFields: React.FunctionComponent> = ({ actionParams, @@ -31,57 +31,25 @@ const JiraParamsFields: React.FunctionComponent ({ label })) : []; const editSubActionProperty = (key: string, value: {}) => { const newProps = { ...actionParams.subActionParams, [key]: value }; editAction('subActionParams', newProps, index); }; - useEffect(() => { - let cancel = false; - const fetchData = async () => { - const createIssueMetadata = await getCreateIssueMetadata({ - http, - connectorId: actionConnector.id, - }); - console.log('createIssueMetadata', createIssueMetadata); - }; - fetchData(); - return () => { - cancel = true; - }; - }, [http, actionConnector]); - useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); @@ -89,17 +57,27 @@ const JiraParamsFields: React.FunctionComponent variable.name === 'alertId')) { editSubActionProperty('savedObjectId', '{{alertId}}'); } - if (!issueType) { - editSubActionProperty('issueType', '3'); + if (!issueType && issueTypesSelectOptions.length > 0) { + editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); } - if (!priority) { - editSubActionProperty('priority', '3'); + if (!priority && prioritiesSelectOptions.length > 0) { + editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); } - if (!labels) { - editSubActionProperty('labels', '3'); + if (!labels && hasLabels) { + editSubActionProperty('labels', []); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [title, description, comments, issueType, priority, labels]); + }, [ + title, + description, + comments, + issueType, + priority, + labels, + issueTypes, + issueTypesSelectOptions, + prioritiesSelectOptions, + ]); return ( @@ -119,7 +97,7 @@ const JiraParamsFields: React.FunctionComponent { editSubActionProperty('issueType', e.target.value); @@ -127,51 +105,76 @@ const JiraParamsFields: React.FunctionComponent - - - - 0 && ( + + + { - editSubActionProperty('priority', e.target.value); - }} - /> - - - - - { - editSubActionProperty('labels', e.target.value); - }} - /> - - - + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel', + { + defaultMessage: 'Priority', + } + )} + > + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + )} + {hasLabels && ( + <> + + + + { + const newOptions = [...labelOptions, { label: searchValue }]; + editSubActionProperty( + 'labels', + newOptions.map((newOption) => newOption.label) + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editSubActionProperty( + 'labels', + selectedOptions.map((selectedOption) => selectedOption.label) + ); + }} + onBlur={() => { + if (!labels) { + editSubActionProperty('labels', []); + } + }} + isClearable={true} + data-test-subj="labelsComboBox" + /> + + + + + + )} + {hasDescription && ( + + )} - { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + }} messageVariables={messageVariables} paramsProperty={'comments'} - inputTargetValue={comments} + inputTargetValue={comments[0].comment} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts index def44e603c67c..45f3777522e14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts @@ -15,7 +15,7 @@ export interface JiraActionParams { savedObjectId: string; title: string; description: string; - comments: string; + comments: Array<{ commentId: string; comment: string }>; externalId: string | null; issueType: string; priority: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx new file mode 100644 index 0000000000000..e59428f2ef922 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx @@ -0,0 +1,117 @@ +/* + * 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 { useState, useEffect, useCallback } from 'react'; +import { EuiSelectOption } from '@elastic/eui'; +import { HttpSetup } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getCreateIssueMetadata } from './api'; + +interface IssueTypes { + [key: string]: { + name: string; + fields: { + [key: string]: { + allowedValues: Array<{ name: string; id: string }> | []; + defaultValue: { name: string; id: string } | {}; + }; + }; + }; +} + +interface Props { + http: HttpSetup; + actionConnector: ActionConnector; + selectedIssueType?: string; +} + +export interface UseCreateIssueMetadata { + issueTypes: IssueTypes; + issueTypesSelectOptions: EuiSelectOption[]; + prioritiesSelectOptions: EuiSelectOption[]; + hasDescription: boolean; + hasLabels: boolean; +} + +export const useCreateIssueMetadata = ({ + http, + actionConnector, + selectedIssueType, +}: Props): UseCreateIssueMetadata => { + const [issueTypes, setIssueTypes] = useState({}); + const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]); + const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); + const [hasDescription, setHasDescription] = useState(false); + const [hasLabels, setHasLabels] = useState(false); + + const hasField = useCallback( + (key) => { + if (selectedIssueType != null) { + const fields = issueTypes[selectedIssueType]?.fields ?? {}; + return Object.prototype.hasOwnProperty.call(fields, key); + } + + return false; + }, + [selectedIssueType, issueTypes] + ); + + useEffect(() => { + let cancel = false; + const fetchData = async () => { + const res = await getCreateIssueMetadata({ + http, + connectorId: actionConnector.id, + }); + + if (!cancel) { + setIssueTypes(res.data.issueTypes); + } + }; + fetchData(); + return () => { + cancel = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, actionConnector]); + + useEffect(() => { + const options = Object.keys(issueTypes).map((key) => ({ + value: key, + text: issueTypes[key].name ?? '', + })); + + setIssueTypesSelectOptions(options); + }, [issueTypes]); + + useEffect(() => { + if (selectedIssueType != null) { + const fields = issueTypes[selectedIssueType]?.fields ?? {}; + const priorities = fields.priority?.allowedValues ?? []; + const options = priorities.map((priority) => ({ + value: priority.name, + text: priority.name, + })); + setPrioritiesSelectOptions(options); + } + }, [selectedIssueType, issueTypes]); + + useEffect(() => { + setHasDescription(hasField('description')); + }, [hasField]); + + useEffect(() => { + setHasLabels(hasField('labels')); + }, [hasField]); + + return { + issueTypes, + issueTypesSelectOptions, + prioritiesSelectOptions, + hasDescription, + hasLabels, + }; +}; From a00c63a4f714428c6194943eef9c9d7a55f92c9b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 2 Aug 2020 15:36:30 +0300 Subject: [PATCH 23/67] Push to jira from alerts --- .../server/builtin_action_types/jira/api.ts | 21 +++++------ .../builtin_action_types/jira/service.ts | 36 ++++++++++++++----- .../server/builtin_action_types/jira/types.ts | 9 ++++- .../builtin_action_types/jira/jira_params.tsx | 2 +- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 0fe6a087f8ff6..57589688d1159 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -62,9 +62,9 @@ const pushToServiceHandler = async ({ } } - let incident = {}; + let incident; // TODO: should be removed later but currently keep it for the Case implementation support - if (mapping && Array.isArray(params.comments)) { + if (mapping) { const fields = prepareFieldsForTransformation({ externalCase: params.externalObject, mapping, @@ -77,7 +77,8 @@ const pushToServiceHandler = async ({ currentIncident, }); } else { - incident = { ...params, summary: params.title }; + const { title, description, priority, labels, issueType } = params; + incident = { summary: title, description, priority, labels, issueType }; } if (updateIncident) { @@ -89,27 +90,21 @@ const pushToServiceHandler = async ({ res = await externalService.createIncident({ incident: { ...incident, - caller_id: secrets.username, }, }); } // TODO: should temporary keep comments for a Case usage - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping && - mapping.get('comments')?.actionType !== 'nothing' - ) { - const commentsTransformed = transformComments(comments, ['informationAdded']); + if (comments && Array.isArray(comments) && comments.length > 0) { + const commentsTransformed = mapping + ? transformComments(comments, ['informationAdded']) + : comments; res.comments = []; for (const currentComment of commentsTransformed) { const comment = await externalService.createComment({ incidentId: res.id, comment: currentComment, - field: mapping.get('comments')?.target ?? 'comments', }); res.comments = [ ...(res.comments ?? []), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 405c6bf93048e..40a492828a93b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -6,9 +6,15 @@ import axios from 'axios'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import { Logger } from '../../../../../../src/core/server'; -import { JiraPublicConfigurationType, JiraSecretConfigurationType } from './types'; +import { + ExternalServiceCredentials, + ExternalService, + ExternalServiceParams, + CreateIncidentParams, + JiraPublicConfigurationType, + JiraSecretConfigurationType, +} from './types'; import { Comment } from './case_types'; import * as i18n from './translations'; @@ -74,10 +80,28 @@ export const createExternalService = ( return undefined; }; - const createIncident = async ({ incident }: ExternalServiceParams) => { + const createIncident = async ({ incident }: CreateIncidentParams) => { // The response from Jira when creating an issue contains only the key and the id. // The function makes two calls when creating an issue. One to create the issue and one to get // the created issue with all the necessary fields. + let fields: { [key: string]: string | string[] | { name: string } | { key: string } } = { + summary: incident.summary, + project: { key: projectKey }, + issuetype: { name: incident.issueType ?? 'Task' }, + }; + + if (incident.description) { + fields = { ...fields, description: incident.description }; + } + + if (incident.labels) { + fields = { ...fields, labels: incident.labels }; + } + + if (incident.priority) { + fields = { ...fields, priority: { name: incident.priority } }; + } + try { const res = await request({ axios: axiosInstance, @@ -85,11 +109,7 @@ export const createExternalService = ( logger, method: 'post', data: { - fields: { - ...(incident as Record), - project: { key: projectKey }, - issuetype: { name: 'Task' }, - }, + fields, }, proxySettings, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 1d369ffc83e76..ded7d4a18f27f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -57,10 +57,17 @@ export interface ExternalServiceCommentResponse { export type ExternalServiceParams = Record; +export interface CreateIncidentParams { + incident: Pick< + ExecutorSubActionPushParams, + 'description' | 'priority' | 'labels' | 'issueType' + > & { summary: string }; +} + export interface ExternalService { getIncident: (id: string) => Promise; findIncidents: (params?: Record) => Promise; - createIncident: (params: ExternalServiceParams) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; createComment: (params: ExternalServiceParams) => Promise; getCreateIssueMetadata: () => Promise; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index d6a8d07581e0a..e08e9d553cb90 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -218,7 +218,7 @@ const JiraParamsFields: React.FunctionComponent 0 ? comments[0].comment : ''} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', { From 3241645b32e62e7b15a80080d9af1f252c80f6a8 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 13:46:59 +0300 Subject: [PATCH 24/67] Improve types --- .../server/builtin_action_types/jira/api.ts | 9 ++- .../server/builtin_action_types/jira/index.ts | 1 - .../builtin_action_types/jira/schema.ts | 2 +- .../builtin_action_types/jira/service.ts | 67 +++++++++++-------- .../server/builtin_action_types/jira/types.ts | 39 ++++++----- .../email/email_params.test.tsx | 11 +++ .../es_index/es_index_params.test.tsx | 9 +++ .../jira/use_create_issue_metadata.tsx | 12 ++-- .../pagerduty/pagerduty_params.test.tsx | 9 +++ .../server_log/server_log_params.test.tsx | 11 +++ .../servicenow/servicenow_params.test.tsx | 9 +++ .../slack/slack_params.test.tsx | 9 +++ .../webhook/webhook_params.test.tsx | 9 +++ 13 files changed, 142 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 57589688d1159..a8b31042dae76 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -12,6 +12,7 @@ import { GetIncidentApiHandlerArgs, CreateIssueMetadataHandlerArgs, ExternalServiceApi, + Incident, } from './types'; // TODO: to remove, need to support Case @@ -43,7 +44,6 @@ const pushToServiceHandler = async ({ externalService, mapping, params, - secrets, logger, }: PushToServiceApiHandlerArgs): Promise => { const { externalId, comments } = params; @@ -62,7 +62,7 @@ const pushToServiceHandler = async ({ } } - let incident; + let incident: Incident; // TODO: should be removed later but currently keep it for the Case implementation support if (mapping) { const fields = prepareFieldsForTransformation({ @@ -94,7 +94,6 @@ const pushToServiceHandler = async ({ }); } - // TODO: should temporary keep comments for a Case usage if (comments && Array.isArray(comments) && comments.length > 0) { const commentsTransformed = mapping ? transformComments(comments, ['informationAdded']) @@ -123,7 +122,7 @@ export const transformFields = ({ params, fields, currentIncident, -}: TransformFieldsArgs): Record => { +}: TransformFieldsArgs): Incident => { return fields.reduce((prev, cur) => { const transform = flow(...cur.pipes.map((p) => transformers[p])); return { @@ -142,7 +141,7 @@ export const transformFields = ({ previousValue: currentIncident ? currentIncident[cur.key] : '', }).value, }; - }, {}); + }, {} as Incident); }; export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index b96641f817190..ca5419e1ac738 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -97,7 +97,6 @@ async function executor( externalService, mapping, params: { ...pushToServiceParams, externalObject }, - secrets, logger, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 533d837de9118..561bcaf9e36cc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -43,7 +43,7 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ issueType: schema.nullable(schema.string()), priority: schema.nullable(schema.string()), labels: schema.nullable(schema.arrayOf(schema.string())), - // TODO: remove later - need for support Case push multiple comments + // TODO: modify later to string[] - need for support Case schema comments: schema.maybe(schema.arrayOf(CommentSchema)), ...EntityInformation, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 40a492828a93b..98831cd1dc548 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -14,8 +14,9 @@ import { CreateIncidentParams, JiraPublicConfigurationType, JiraSecretConfigurationType, + Fields, + CreateCommentParams, } from './types'; -import { Comment } from './case_types'; import * as i18n from './translations'; import { request, getErrorMessage } from '../lib/axios_utils'; @@ -84,7 +85,7 @@ export const createExternalService = ( // The response from Jira when creating an issue contains only the key and the id. // The function makes two calls when creating an issue. One to create the issue and one to get // the created issue with all the necessary fields. - let fields: { [key: string]: string | string[] | { name: string } | { key: string } } = { + let fields: Fields = { summary: incident.summary, project: { key: projectKey }, issuetype: { name: incident.issueType ?? 'Task' }, @@ -158,21 +159,19 @@ export const createExternalService = ( } }; - const createComment = async ({ incidentId, comment }: ExternalServiceParams) => { - const commentTyped = comment as Comment; - + const createComment = async ({ incidentId, comment }: CreateCommentParams) => { try { const res = await request({ axios: axiosInstance, method: 'post', - url: getCommentsURL(incidentId as string), + url: getCommentsURL(incidentId), logger, - data: { body: commentTyped.comment }, + data: { body: comment.comment }, proxySettings, }); return { - commentId: commentTyped.commentId, + commentId: comment.commentId, externalCommentId: res.data.id, pushedDate: new Date(res.data.created).toISOString(), }; @@ -190,7 +189,7 @@ export const createExternalService = ( try { const capabilitiesResponse = await getCapabilities(); - const capabilities = Object.keys(capabilitiesResponse?.capabilities ?? []); + const capabilities = Object.keys(capabilitiesResponse?.capabilities ?? {}); const supportsNewAPI = createMetaCapabilities.every((c) => capabilities.includes(c)); if (!supportsNewAPI) { @@ -200,27 +199,41 @@ export const createExternalService = ( url: createIssueMetadataUrl, }); - const issueTypes = res.data.projects[0].issuetypes; - const metadata = issueTypes.reduce((acc, currentIssueType) => { - const fields = Object.keys(currentIssueType.fields).reduce((fieldsAcc, fieldKey) => { + const issueTypes = res.data.projects[0]?.issuetypes ?? []; + const metadata = issueTypes.reduce( + ( + acc: Record, + currentIssueType: { + name: string; + id: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fields: { [k: string]: { allowedValues?: any; defaultValue?: any } }; + } + ) => { + const fields = Object.keys(currentIssueType.fields ?? {}).reduce( + (fieldsAcc, fieldKey) => { + return { + ...fieldsAcc, + [fieldKey]: { + allowedValues: currentIssueType.fields[fieldKey]?.allowedValues ?? [], + defaultValue: currentIssueType.fields[fieldKey]?.defaultValue ?? {}, + }, + }; + }, + {} + ); + return { - ...fieldsAcc, - [fieldKey]: { - allowedValues: currentIssueType.fields[fieldKey].allowedValues ?? [], - defaultValue: currentIssueType.fields[fieldKey].defaultValue ?? {}, + ...acc, + [currentIssueType.name]: { + id: currentIssueType.id, + name: currentIssueType.name, + fields, }, }; - }, {}); - - return { - ...acc, - [currentIssueType.name]: { - id: currentIssueType.id, - name: currentIssueType.name, - fields, - }, - }; - }, {}); + }, + {} + ); return { issueTypes: metadata }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index ded7d4a18f27f..5172f136a8802 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -19,7 +19,7 @@ import { } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { IncidentConfigurationSchema } from './case_schema'; -import { PushToServiceResponse } from './case_types'; +import { PushToServiceResponse, Comment } from './case_types'; import { Logger } from '../../../../../../src/core/server'; export type JiraPublicConfigurationType = TypeOf; @@ -57,11 +57,18 @@ export interface ExternalServiceCommentResponse { export type ExternalServiceParams = Record; +export type Incident = Pick< + ExecutorSubActionPushParams, + 'description' | 'priority' | 'labels' | 'issueType' +> & { summary: string }; + export interface CreateIncidentParams { - incident: Pick< - ExecutorSubActionPushParams, - 'description' | 'priority' | 'labels' | 'issueType' - > & { summary: string }; + incident: Incident; +} + +export interface CreateCommentParams { + incidentId: string; + comment: Comment; } export interface ExternalService { @@ -69,7 +76,7 @@ export interface ExternalService { findIncidents: (params?: Record) => Promise; createIncident: (params: CreateIncidentParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; - createComment: (params: ExternalServiceParams) => Promise; + createComment: (params: CreateCommentParams) => Promise; getCreateIssueMetadata: () => Promise; getCapabilities: () => Promise; } @@ -78,11 +85,6 @@ export interface PushToServiceApiParams extends ExecutorSubActionPushParams { externalObject: Record; } -export interface ExternalServiceApiHandlerArgs { - externalService: ExternalService; - mapping: Map | null; -} - export type ExecutorSubActionGetIncidentParams = TypeOf< typeof ExecutorSubActionGetIncidentParamsSchema >; @@ -99,9 +101,13 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf< typeof ExecutorSubActionGetCapabilitiesParamsSchema >; +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; +} + export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; - secrets: Record; logger: Logger; } @@ -118,11 +124,6 @@ export interface CreateIssueMetadataHandlerArgs { params: ExecutorSubActionCreateIssueMetadataParams; } -export interface GetCapabilitiesHandlerArgs { - externalService: ExternalService; - params: ExecutorSubActionGetCapabilitiesParams; -} - export type GetCreateIssueMetadataResponse = Record; export interface ExternalServiceApi { @@ -133,3 +134,7 @@ export interface ExternalServiceApi { args: CreateIssueMetadataHandlerArgs ) => Promise; } + +export interface Fields { + [key: string]: string | string[] | { name: string } | { key: string }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx index ecdfefa109f58..b1322367d497c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -17,6 +17,16 @@ describe('EmailParamsFields renders', () => { subject: 'test', message: 'test message', }; + + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; + const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 25c04bda3f536..8819606b8c89e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -13,6 +13,14 @@ describe('IndexParamsFields renders', () => { const actionParams = { documents: [{ test: 123 }], }; + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(`{ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx index e59428f2ef922..f083caaa2f1e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx @@ -7,6 +7,7 @@ import { useState, useEffect, useCallback } from 'react'; import { EuiSelectOption } from '@elastic/eui'; import { HttpSetup } from 'kibana/public'; +import { map } from 'lodash/fp'; import { ActionConnector } from '../../../../types'; import { getCreateIssueMetadata } from './api'; @@ -91,10 +92,13 @@ export const useCreateIssueMetadata = ({ if (selectedIssueType != null) { const fields = issueTypes[selectedIssueType]?.fields ?? {}; const priorities = fields.priority?.allowedValues ?? []; - const options = priorities.map((priority) => ({ - value: priority.name, - text: priority.name, - })); + const options = map( + (priority) => ({ + value: priority.name, + text: priority.name, + }), + priorities + ); setPrioritiesSelectOptions(options); } }, [selectedIssueType, issueTypes]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index 9e37047ccda50..a03f6a0ae6927 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -22,6 +22,14 @@ describe('PagerDutyParamsFields renders', () => { group: 'group', class: 'test class', }; + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx index 3a015cddcd335..95d77cab48dd1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx @@ -10,6 +10,15 @@ import ServerLogParamsFields from './server_log_params'; import { DocLinksStart } from 'kibana/public'; describe('ServerLogParamsFields renders', () => { + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; + test('all params fields is rendered', () => { const actionParams = { level: ServerLogLevelOptions.TRACE, @@ -23,6 +32,7 @@ describe('ServerLogParamsFields renders', () => { index={0} defaultMessage={'test default message'} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); @@ -44,6 +54,7 @@ describe('ServerLogParamsFields renders', () => { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx index 1fc856b1e1ab2..6cf0733b1f84f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -23,6 +23,14 @@ describe('ServiceNowParamsFields renders', () => { externalId: null, }, }; + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; const wrapper = mountWithIntl( { index={0} messageVariables={[]} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="urgencySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx index 7649d2dcb62c7..362f63ad4b6cf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx @@ -13,6 +13,14 @@ describe('SlackParamsFields renders', () => { const actionParams = { message: 'test message', }; + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx index 825c1372dfaf7..4f01ad3aa98b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -13,6 +13,14 @@ describe('WebhookParamsFields renders', () => { const actionParams = { body: 'test message', }; + const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); From cc2af4c86fd2d43a33f188b657cac2ac3f580ee5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 13:56:58 +0300 Subject: [PATCH 25/67] Remove jira from lib --- .../public/common/lib/connectors/config.ts | 11 +- .../public/common/lib/connectors/index.ts | 1 - .../common/lib/connectors/jira/config.ts | 40 ------ .../common/lib/connectors/jira/flyout.tsx | 114 ------------------ .../common/lib/connectors/jira/index.tsx | 54 --------- .../common/lib/connectors/jira/logo.svg | 9 -- .../lib/connectors/jira/translations.ts | 72 ----------- .../common/lib/connectors/jira/types.ts | 22 ---- .../public/common/index.ts | 1 + 9 files changed, 8 insertions(+), 316 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts index 833f85712b5fa..9e6982ea20301 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common'; -import { connector as jiraConnectorConfig } from './jira/config'; +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + ServiceNowConnectorConfiguration, + JiraConnectorConfiguration, +} from '../../../../../triggers_actions_ui/public/common'; import { connector as resilientConnectorConfig } from './resilient/config'; import { ConnectorConfiguration } from './types'; export const connectorsConfiguration: Record = { '.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration, - '.jira': jiraConnectorConfig, + '.jira': JiraConnectorConfiguration as ConnectorConfiguration, '.resilient': resilientConnectorConfig, }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts index f32e1e0df184e..33afa82c84f34 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getActionType as jiraActionType } from './jira'; export { getActionType as resilientActionType } from './resilient'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts deleted file mode 100644 index e6151a54bff74..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { ConnectorConfiguration } from './types'; - -import * as i18n from './translations'; -import logo from './logo.svg'; - -export const connector: ConnectorConfiguration = { - id: '.jira', - name: i18n.JIRA_TITLE, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - fields: { - summary: { - label: i18n.MAPPING_FIELD_SUMMARY, - validSourceFields: ['title', 'description'], - defaultSourceField: 'title', - defaultActionType: 'overwrite', - }, - description: { - label: i18n.MAPPING_FIELD_DESC, - validSourceFields: ['title', 'description'], - defaultSourceField: 'description', - defaultActionType: 'overwrite', - }, - comments: { - label: i18n.MAPPING_FIELD_COMMENTS, - validSourceFields: ['comments'], - defaultSourceField: 'comments', - defaultActionType: 'append', - }, - }, -}; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx deleted file mode 100644 index 0737db3cd08eb..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import * as i18n from './translations'; -import { ConnectorFlyoutFormProps } from '../types'; -import { JiraActionConnector } from './types'; -import { withConnectorFlyout } from '../components/connector_flyout'; - -const JiraConnectorForm: React.FC> = ({ - errors, - action, - onChangeSecret, - onBlurSecret, - onChangeConfig, - onBlurConfig, -}) => { - const { projectKey } = action.config; - const { email, apiToken } = action.secrets; - const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; - const isEmailInvalid: boolean = errors.email.length > 0 && email != null; - const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; - - return ( - <> - - - - onChangeConfig('projectKey', evt.target.value)} - onBlur={() => onBlurConfig('projectKey')} - /> - - - - - - - - onChangeSecret('email', evt.target.value)} - onBlur={() => onBlurSecret('email')} - /> - - - - - - - - onChangeSecret('apiToken', evt.target.value)} - onBlur={() => onBlurSecret('apiToken')} - /> - - - - - ); -}; - -export const JiraConnectorFlyout = withConnectorFlyout({ - ConnectorFormComponent: JiraConnectorForm, - secretKeys: ['email', 'apiToken'], - configKeys: ['projectKey'], - connectorActionTypeId: '.jira', -}); - -// eslint-disable-next-line import/no-default-export -export { JiraConnectorFlyout as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx deleted file mode 100644 index cead392010dc7..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../triggers_actions_ui/public/types'; - -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { JiraActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - projectKey: string[]; - email: string[]; - apiToken: string[]; -} - -const validateConnector = (action: JiraActionConnector): ValidationResult => { - const errors: Errors = { - projectKey: [], - email: [], - apiToken: [], - }; - - if (!action.config.projectKey) { - errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; - } - - if (!action.secrets.email) { - errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED]; - } - - if (!action.secrets.apiToken) { - errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.JIRA_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg deleted file mode 100644 index 8560cf7e270c8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts deleted file mode 100644 index d7abf77a58d4c..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export * from '../translations'; - -export const JIRA_DESC = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.selectMessageText', - { - defaultMessage: 'Push or update Security case data to a new issue in Jira', - } -); - -export const JIRA_TITLE = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.actionTypeTitle', - { - defaultMessage: 'Jira', - } -); - -export const JIRA_PROJECT_KEY_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.projectKey', - { - defaultMessage: 'Project key', - } -); - -export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField', - { - defaultMessage: 'Project key is required', - } -); - -export const JIRA_EMAIL_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.emailTextFieldLabel', - { - defaultMessage: 'Email or Username', - } -); - -export const JIRA_EMAIL_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.requiredEmailTextField', - { - defaultMessage: 'Email or Username is required', - } -); - -export const JIRA_API_TOKEN_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel', - { - defaultMessage: 'API token or Password', - } -); - -export const JIRA_API_TOKEN_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField', - { - defaultMessage: 'API token or Password is required', - } -); - -export const MAPPING_FIELD_SUMMARY = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldSummary', - { - defaultMessage: 'Summary', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts deleted file mode 100644 index fafb4a0d41fb3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - JiraPublicConfigurationType, - JiraSecretConfigurationType, -} from '../../../../../../actions/server/builtin_action_types/jira/types'; - -export { JiraFieldsType } from '../../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface JiraActionConnector { - config: JiraPublicConfigurationType; - secrets: JiraSecretConfigurationType; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 9dd3fd787f860..8b728b5e178b5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -7,3 +7,4 @@ export * from './expression_items'; export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; +export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config'; From e37d3e74ad4cc16cbdb6f5416b83a80e6834bca4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 15:36:36 +0300 Subject: [PATCH 26/67] Improve error reporting --- .../builtin_action_types/jira/service.ts | 44 ++++++++++++++++--- .../server/builtin_action_types/jira/types.ts | 3 ++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 98831cd1dc548..8a65cdc7b4853 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -16,6 +16,7 @@ import { JiraSecretConfigurationType, Fields, CreateCommentParams, + ResponseError, } from './types'; import * as i18n from './translations'; @@ -58,6 +59,13 @@ export const createExternalService = ( return commentUrl.replace('{issueId}', issueId); }; + const createErrorMessage = (errors: ResponseError) => { + return Object.entries(errors).reduce((errorMessage, [, value]) => { + const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value; + return msg; + }, ''); + }; + const getIncident = async (id: string) => { try { const res = await request({ @@ -72,7 +80,12 @@ export const createExternalService = ( return { ...rest, ...fields }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to get incident with id ${id}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response.data.errors)}` + ) ); } }; @@ -125,7 +138,12 @@ export const createExternalService = ( }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage( + error.response.data.errors + )}` + ) ); } }; @@ -153,7 +171,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to update incident with id ${incidentId}. Error: ${error.message}` + `Unable to update incident with id ${incidentId}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response.data.errors)}` ) ); } @@ -179,7 +199,9 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + `Unable to create comment at incident with id ${incidentId}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response.data.errors)}` ) ); } @@ -241,7 +263,12 @@ export const createExternalService = ( return { issueTypes: [] }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to get create issue metadata. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to get create issue metadata. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response.data.errors)}` + ) ); } }; @@ -257,7 +284,12 @@ export const createExternalService = ( return { ...res.data }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to get capabilities. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to get capabilities. Error: ${error.message} Reason: ${createErrorMessage( + error.response.data.errors + )}` + ) ); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 5172f136a8802..8ef97f8c0278c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -138,3 +138,6 @@ export interface ExternalServiceApi { export interface Fields { [key: string]: string | string[] | { name: string } | { key: string }; } +export interface ResponseError { + [k: string]: string; +} From b90acfe8bf0efdcfe68983d391bdf2cff8486331 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 16:46:11 +0300 Subject: [PATCH 27/67] Get default issueType from metadata --- .../builtin_action_types/jira/service.ts | 60 ++++++++++++------- .../server/builtin_action_types/jira/types.ts | 20 +++++-- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 8a65cdc7b4853..6a12b868409e6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -16,7 +16,9 @@ import { JiraSecretConfigurationType, Fields, CreateCommentParams, + Incident, ResponseError, + ExternalServiceIncidentResponse, } from './types'; import * as i18n from './translations'; @@ -59,6 +61,31 @@ export const createExternalService = ( return commentUrl.replace('{issueId}', issueId); }; + const createFields = (key: string, incident: Incident): Fields => { + let fields: Fields = { + summary: incident.summary, + project: { key }, + }; + + if (incident.issueType) { + fields = { ...fields, issuetype: { name: incident.issueType } }; + } + + if (incident.description) { + fields = { ...fields, description: incident.description }; + } + + if (incident.labels) { + fields = { ...fields, labels: incident.labels }; + } + + if (incident.priority) { + fields = { ...fields, priority: { name: incident.priority } }; + } + + return fields; + }; + const createErrorMessage = (errors: ResponseError) => { return Object.entries(errors).reduce((errorMessage, [, value]) => { const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value; @@ -94,27 +121,20 @@ export const createExternalService = ( return undefined; }; - const createIncident = async ({ incident }: CreateIncidentParams) => { + const createIncident = async ({ + incident, + }: CreateIncidentParams): Promise => { // The response from Jira when creating an issue contains only the key and the id. // The function makes two calls when creating an issue. One to create the issue and one to get // the created issue with all the necessary fields. - let fields: Fields = { - summary: incident.summary, - project: { key: projectKey }, - issuetype: { name: incident.issueType ?? 'Task' }, - }; - - if (incident.description) { - fields = { ...fields, description: incident.description }; - } - - if (incident.labels) { - fields = { ...fields, labels: incident.labels }; - } - - if (incident.priority) { - fields = { ...fields, priority: { name: incident.priority } }; - } + const createIssueMetadata = await getCreateIssueMetadata(); + const fields = createFields(projectKey, { + ...incident, + issueType: incident.issueType + ? incident.issueType + : createIssueMetadata.issueTypes[Object.keys(createIssueMetadata.issueTypes)[0]]?.name ?? + '', + }); try { const res = await request({ @@ -207,7 +227,7 @@ export const createExternalService = ( } }; - const getCreateIssueMetadata = async () => { + const getCreateIssueMetadata = async (): Promise => { try { const capabilitiesResponse = await getCapabilities(); @@ -260,7 +280,7 @@ export const createExternalService = ( return { issueTypes: metadata }; } - return { issueTypes: [] }; + return { issueTypes: {} }; } catch (error) { throw new Error( getErrorMessage( diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 8ef97f8c0278c..f3bd792ca3066 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -71,13 +71,28 @@ export interface CreateCommentParams { comment: Comment; } +export interface IssueTypes { + [key: string]: { + name: string; + fields: { + [key: string]: { + allowedValues: Array<{ name: string; id: string }> | []; + defaultValue: { name: string; id: string } | {}; + }; + }; + }; +} +export interface GetCreateIssueMetadataResponse { + issueTypes: IssueTypes; +} + export interface ExternalService { getIncident: (id: string) => Promise; findIncidents: (params?: Record) => Promise; createIncident: (params: CreateIncidentParams) => Promise; updateIncident: (params: ExternalServiceParams) => Promise; createComment: (params: CreateCommentParams) => Promise; - getCreateIssueMetadata: () => Promise; + getCreateIssueMetadata: () => Promise; getCapabilities: () => Promise; } @@ -118,14 +133,11 @@ export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: ExecutorSubActionHandshakeParams; } - export interface CreateIssueMetadataHandlerArgs { externalService: ExternalService; params: ExecutorSubActionCreateIssueMetadataParams; } -export type GetCreateIssueMetadataResponse = Record; - export interface ExternalServiceApi { handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; From e73bc82e8f3bc7837e28951f7ee459e0f791d3cb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 16:46:43 +0300 Subject: [PATCH 28/67] Update Jira incident --- .../server/builtin_action_types/jira/api.ts | 3 +- .../builtin_action_types/jira/service.ts | 32 +++++++++++++++---- .../server/builtin_action_types/jira/types.ts | 7 +++- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index a8b31042dae76..5df1d5fb84a6e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -34,7 +34,6 @@ const getIncidentHandler = async ({ const getCreateIssueMetadataHandler = async ({ externalService, - params, }: CreateIssueMetadataHandlerArgs) => { const res = await externalService.getCreateIssueMetadata(); return res; @@ -81,7 +80,7 @@ const pushToServiceHandler = async ({ incident = { summary: title, description, priority, labels, issueType }; } - if (updateIncident) { + if (externalId != null) { res = await externalService.updateIncident({ incidentId: externalId, incident, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 6a12b868409e6..812cec9fbe548 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -10,14 +10,16 @@ import { Logger } from '../../../../../../src/core/server'; import { ExternalServiceCredentials, ExternalService, - ExternalServiceParams, CreateIncidentParams, + UpdateIncidentParams, JiraPublicConfigurationType, JiraSecretConfigurationType, Fields, CreateCommentParams, Incident, ResponseError, + GetCreateIssueMetadataResponse, + ExternalServiceCommentResponse, ExternalServiceIncidentResponse, } from './types'; @@ -124,9 +126,12 @@ export const createExternalService = ( const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { - // The response from Jira when creating an issue contains only the key and the id. - // The function makes two calls when creating an issue. One to create the issue and one to get - // the created issue with all the necessary fields. + /* The response from Jira when creating an issue contains only the key and the id. + The function makes three calls when creating an issue: + 1. Get issueTypes to set a default in case incident.issueType is missing + 2. Create the issue. + 3. Get the created issue with all the necessary fields. + */ const createIssueMetadata = await getCreateIssueMetadata(); const fields = createFields(projectKey, { ...incident, @@ -168,14 +173,24 @@ export const createExternalService = ( } }; - const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + const updateIncident = async ({ + incidentId, + incident, + }: UpdateIncidentParams): Promise => { + const incidentWithoutNullValues = Object.entries(incident).reduce( + (obj, [key, value]) => (value != null ? { ...obj, [key]: value } : obj), + {} as Incident + ); + + const fields = createFields(projectKey, incidentWithoutNullValues); + try { await request({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, logger, - data: { fields: { ...(incident as Record) } }, + data: { fields }, proxySettings, }); @@ -199,7 +214,10 @@ export const createExternalService = ( } }; - const createComment = async ({ incidentId, comment }: CreateCommentParams) => { + const createComment = async ({ + incidentId, + comment, + }: CreateCommentParams): Promise => { try { const res = await request({ axios: axiosInstance, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index f3bd792ca3066..fbdf941e32b32 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -66,6 +66,11 @@ export interface CreateIncidentParams { incident: Incident; } +export interface UpdateIncidentParams { + incidentId: string; + incident: Incident; +} + export interface CreateCommentParams { incidentId: string; comment: Comment; @@ -90,7 +95,7 @@ export interface ExternalService { getIncident: (id: string) => Promise; findIncidents: (params?: Record) => Promise; createIncident: (params: CreateIncidentParams) => Promise; - updateIncident: (params: ExternalServiceParams) => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; createComment: (params: CreateCommentParams) => Promise; getCreateIssueMetadata: () => Promise; getCapabilities: () => Promise; From a1841f226d9933f6a4a4415b3463044dc3081437 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 17:29:04 +0300 Subject: [PATCH 29/67] Support the new API for getting create issue metadata --- .../builtin_action_types/jira/service.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 812cec9fbe548..c2672a24078f6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -51,6 +51,8 @@ export const createExternalService = ( const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`; const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; const createIssueMetadataUrl = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; + const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`; + const getIssueTypeFieldsUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`; const axiosInstance = axios.create({ auth: { username: email, password: apiToken }, }); @@ -62,6 +64,9 @@ export const createExternalService = ( const getCommentsURL = (issueId: string) => { return commentUrl.replace('{issueId}', issueId); }; + const createGetIssueTypeFieldsUrl = (issueTypeId: string) => { + return getIssueTypeFieldsUrl.replace('{issueTypeId}', issueTypeId); + }; const createFields = (key: string, incident: Incident): Fields => { let fields: Fields = { @@ -296,9 +301,44 @@ export const createExternalService = ( ); return { issueTypes: metadata }; - } + } else { + let metadata = {}; + const issueTypeRes = await request({ + axios: axiosInstance, + method: 'get', + url: getIssueTypesUrl, + }); + + const issueTypes = issueTypeRes.data.values; - return { issueTypes: {} }; + for (const issueType of issueTypes) { + const fieldsRes = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(issueType.id), + }); + const fields = fieldsRes.data.values; + + metadata = { + ...metadata, + name: issueType.name, + [issueType.name]: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fields: fields.reduce((fieldsAcc: {}, field: Record) => { + return { + ...fieldsAcc, + [field.fieldId]: { + allowedValues: field.allowedValues ?? [], + defaultValue: field.defaultValue ?? {}, + }, + }; + }, {}), + }, + }; + } + + return { issueTypes: metadata }; + } } catch (error) { throw new Error( getErrorMessage( From ecf52b9d11682230863818d3c74370374f1dccd4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 4 Aug 2020 17:49:46 +0300 Subject: [PATCH 30/67] Show loading spinner when fetching issue metadata --- .../builtin_action_types/jira/jira_params.tsx | 279 +++++++++--------- .../jira/use_create_issue_metadata.tsx | 6 + 2 files changed, 149 insertions(+), 136 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index e08e9d553cb90..e0dc857c5af80 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment, useEffect } from 'react'; -import { EuiFormRow, EuiComboBox } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; @@ -37,6 +37,7 @@ const JiraParamsFields: React.FunctionComponentIncident - - { - editSubActionProperty('issueType', e.target.value); - }} - /> - - - {prioritiesSelectOptions.length > 0 && ( - - - } + {!isLoading && ( + <> + + { + editSubActionProperty('issueType', e.target.value); + }} + /> + + + {prioritiesSelectOptions.length > 0 && ( + + + + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + )} + + {hasLabels && ( + <> + + + + { + const newOptions = [...labelOptions, { label: searchValue }]; + editSubActionProperty( + 'labels', + newOptions.map((newOption) => newOption.label) + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editSubActionProperty( + 'labels', + selectedOptions.map((selectedOption) => selectedOption.label) + ); + }} + onBlur={() => { + if (!labels) { + editSubActionProperty('labels', []); + } + }} + isClearable={true} + data-test-subj="labelsComboBox" + /> + + + + + + )} + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', + { + defaultMessage: 'Summary', + } + )} + > + + + {hasDescription && ( + - { - editSubActionProperty('priority', e.target.value); - }} - /> - - - - )} - - {hasLabels && ( - <> - - - - { - const newOptions = [...labelOptions, { label: searchValue }]; - editSubActionProperty( - 'labels', - newOptions.map((newOption) => newOption.label) - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editSubActionProperty( - 'labels', - selectedOptions.map((selectedOption) => selectedOption.label) - ); - }} - onBlur={() => { - if (!labels) { - editSubActionProperty('labels', []); - } - }} - isClearable={true} - data-test-subj="labelsComboBox" - /> - - - - - - )} - 0 && title !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', - { - defaultMessage: 'Summary', - } - )} - > - - - {hasDescription && ( - )} - errors={errors.description as string[]} - /> + { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + }} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments (optional)', + } + )} + errors={errors.comments as string[]} + /> + )} - { - editSubActionProperty(key, [{ commentId: '1', comment: value }]); - }} - messageVariables={messageVariables} - paramsProperty={'comments'} - inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', - { - defaultMessage: 'Additional comments (optional)', - } - )} - errors={errors.comments as string[]} - /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx index f083caaa2f1e8..105e95d71f2a5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx @@ -35,6 +35,7 @@ export interface UseCreateIssueMetadata { prioritiesSelectOptions: EuiSelectOption[]; hasDescription: boolean; hasLabels: boolean; + isLoading: boolean; } export const useCreateIssueMetadata = ({ @@ -42,6 +43,7 @@ export const useCreateIssueMetadata = ({ actionConnector, selectedIssueType, }: Props): UseCreateIssueMetadata => { + const [isLoading, setIsLoading] = useState(true); const [issueTypes, setIssueTypes] = useState({}); const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]); const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); @@ -63,18 +65,21 @@ export const useCreateIssueMetadata = ({ useEffect(() => { let cancel = false; const fetchData = async () => { + setIsLoading(true); const res = await getCreateIssueMetadata({ http, connectorId: actionConnector.id, }); if (!cancel) { + setIsLoading(false); setIssueTypes(res.data.issueTypes); } }; fetchData(); return () => { cancel = true; + setIsLoading(false); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [http, actionConnector]); @@ -117,5 +122,6 @@ export const useCreateIssueMetadata = ({ prioritiesSelectOptions, hasDescription, hasLabels, + isLoading, }; }; From 355acde3bf1d6d1c30b9b44557f29f122f1f8cdf Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 Aug 2020 13:56:37 +0300 Subject: [PATCH 31/67] Support generics --- .../server/builtin_action_types/jira/index.ts | 41 +++++++++++++------ .../server/builtin_action_types/jira/types.ts | 2 + 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index ca5419e1ac738..401a950fc7718 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -21,14 +21,15 @@ import { ExecutorParams, ExecutorSubActionPushParams, ExecutorSubActionCreateIssueMetadataParams, - GetCreateIssueMetadataResponse, + JiraPublicConfigurationType, + JiraSecretConfigurationType, + JiraExecutorResultData, } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; // TODO: to remove, need to support Case import { buildMap, mapParams } from '../case/utils'; -import { PushToServiceResponse } from './case_types'; interface GetActionTypeParams { logger: Logger; @@ -38,7 +39,14 @@ interface GetActionTypeParams { const supportedSubActions: string[] = ['pushToService', 'getCreateIssueMetadata']; // action type definition -export function getActionType(params: GetActionTypeParams): ActionType { +export function getActionType( + params: GetActionTypeParams +): ActionType< + JiraPublicConfigurationType, + JiraSecretConfigurationType, + ExecutorParams, + JiraExecutorResultData | {} +> { const { logger, configurationUtilities } = params; return { id: '.jira', @@ -60,16 +68,24 @@ export function getActionType(params: GetActionTypeParams): ActionType { // action executor async function executor( { logger }: { logger: Logger }, - execOptions: ActionTypeExecutorOptions -): Promise { + execOptions: ActionTypeExecutorOptions< + JiraPublicConfigurationType, + JiraSecretConfigurationType, + ExecutorParams + > +): Promise> { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params as ExecutorParams; - let data: PushToServiceResponse | GetCreateIssueMetadataResponse | null = null; + let data: JiraExecutorResultData | null = null; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; @@ -87,9 +103,8 @@ async function executor( const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; const { comments, externalId, ...restParams } = pushToServiceParams; - const mapping = config.incidentConfiguration - ? buildMap(config.incidentConfiguration.mapping) - : null; + const incidentConfiguration = config.incidentConfiguration; + const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; const externalObject = config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index fbdf941e32b32..0b9fecc1bab98 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -152,6 +152,8 @@ export interface ExternalServiceApi { ) => Promise; } +export type JiraExecutorResultData = PushToServiceResponse | GetCreateIssueMetadataResponse; + export interface Fields { [key: string]: string | string[] | { name: string } | { key: string }; } From 3a348fc0addf6712234b42713dec84e58702c01d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 Aug 2020 14:26:55 +0300 Subject: [PATCH 32/67] Fix types --- .../builtin_action_types/jira/service.test.ts | 59 +++++++++++++++---- .../builtin_action_types/jira/service.ts | 10 ++++ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 547595b4c183f..f1af8e2b547f1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -8,7 +8,7 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; @@ -138,7 +138,13 @@ describe('Jira service', () => { })); const res = await service.createIncident({ - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: 'Task', + priority: 'High', + }, }); expect(res).toEqual({ @@ -159,7 +165,13 @@ describe('Jira service', () => { })); await service.createIncident({ - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: 'Task', + priority: 'High', + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -185,7 +197,13 @@ describe('Jira service', () => { expect( service.createIncident({ - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: 'Task', + priority: 'High', + }, }) ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred'); }); @@ -203,7 +221,13 @@ describe('Jira service', () => { const res = await service.updateIncident({ incidentId: '1', - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: 'Task', + priority: 'High', + }, }); expect(res).toEqual({ @@ -225,7 +249,13 @@ describe('Jira service', () => { await service.updateIncident({ incidentId: '1', - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: 'Task', + priority: 'High', + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -245,7 +275,13 @@ describe('Jira service', () => { expect( service.updateIncident({ incidentId: '1', - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: 'Task', + priority: 'High', + }, }) ).rejects.toThrow( '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred' @@ -265,8 +301,7 @@ describe('Jira service', () => { const res = await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, }); expect(res).toEqual({ @@ -287,8 +322,7 @@ describe('Jira service', () => { await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'my_field', + comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -308,8 +342,7 @@ describe('Jira service', () => { expect( service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, }) ).rejects.toThrow( '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred' diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index c2672a24078f6..cdf2e740105e1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -35,6 +35,8 @@ const COMMENT_URL = `comment`; const VIEW_INCIDENT_URL = `browse`; +const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-fields']; + export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, @@ -262,6 +264,8 @@ export const createExternalService = ( axios: axiosInstance, method: 'get', url: createIssueMetadataUrl, + logger, + proxySettings, }); const issueTypes = res.data.projects[0]?.issuetypes ?? []; @@ -307,6 +311,8 @@ export const createExternalService = ( axios: axiosInstance, method: 'get', url: getIssueTypesUrl, + logger, + proxySettings, }); const issueTypes = issueTypeRes.data.values; @@ -316,6 +322,8 @@ export const createExternalService = ( axios: axiosInstance, method: 'get', url: createGetIssueTypeFieldsUrl(issueType.id), + logger, + proxySettings, }); const fields = fieldsRes.data.values; @@ -357,6 +365,8 @@ export const createExternalService = ( axios: axiosInstance, method: 'get', url: capabilitiesUrl, + logger, + proxySettings, }); return { ...res.data }; From 349b6d5ea534b20c98b9513cce6a3594c92b8f58 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 24 Aug 2020 14:48:31 +0300 Subject: [PATCH 33/67] Change desc --- .../components/builtin_action_types/jira/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 106057e4511dd..0dadea96b8bb0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const JIRA_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', { - defaultMessage: 'Push or update Security case data to a new issue in Jira', + defaultMessage: 'Push or update data to a new issue in Jira', } ); From 77cc4880ddb5d3cf05cb8427911ef603c50df324 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 25 Aug 2020 13:25:28 +0300 Subject: [PATCH 34/67] Create issue type and fields sub actions --- .../server/builtin_action_types/jira/api.ts | 18 ++++ .../server/builtin_action_types/jira/index.ts | 25 ++++- .../builtin_action_types/jira/schema.ts | 12 +++ .../builtin_action_types/jira/service.ts | 91 +++++++++++++++++-- .../server/builtin_action_types/jira/types.ts | 38 +++++++- 5 files changed, 176 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 5df1d5fb84a6e..2cb1ab042dfc5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -13,6 +13,8 @@ import { CreateIssueMetadataHandlerArgs, ExternalServiceApi, Incident, + GetFieldsByIssueTypeHandlerArgs, + GetIssueTypesHandlerArgs, } from './types'; // TODO: to remove, need to support Case @@ -39,6 +41,20 @@ const getCreateIssueMetadataHandler = async ({ return res; }; +const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => { + const res = await externalService.getIssueTypes(); + return res; +}; + +const getFieldsByIssueTypeHandler = async ({ + externalService, + params, +}: GetFieldsByIssueTypeHandlerArgs) => { + const { id } = params; + const res = await externalService.getFieldsByIssueType(id); + return res; +}; + const pushToServiceHandler = async ({ externalService, mapping, @@ -166,4 +182,6 @@ export const api: ExternalServiceApi = { pushToService: pushToServiceHandler, getIncident: getIncidentHandler, getCreateIssueMetadata: getCreateIssueMetadataHandler, + getIssueTypes: getIssueTypesHandler, + getFieldsByIssueType: getFieldsByIssueTypeHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 401a950fc7718..0b0dddf016878 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -24,6 +24,8 @@ import { JiraPublicConfigurationType, JiraSecretConfigurationType, JiraExecutorResultData, + ExecutorSubActionGetFieldsByIssueTypeParams, + ExecutorSubActionGetIssueTypesParams, } from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; @@ -36,7 +38,12 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['pushToService', 'getCreateIssueMetadata']; +const supportedSubActions: string[] = [ + 'pushToService', + 'getCreateIssueMetadata', + 'getIssueTypes', + 'getFieldsByIssueType', +]; // action type definition export function getActionType( @@ -126,5 +133,21 @@ async function executor( }); } + if (subAction === 'getIssueTypes') { + const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; + data = await api.getIssueTypes({ + externalService, + params: getIssueTypesParams, + }); + } + + if (subAction === 'getFieldsByIssueType') { + const getFieldsByIssueTypeParams = subActionParams as ExecutorSubActionGetFieldsByIssueTypeParams; + data = await api.getFieldsByIssueType({ + externalService, + params: getFieldsByIssueTypeParams, + }); + } + return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 561bcaf9e36cc..071e95798e012 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -56,6 +56,10 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); export const ExecutorSubActionCreateIssueMetadataParamsSchema = schema.object({}); export const ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({}); +export const ExecutorSubActionGetIssueTypesParamsSchema = schema.object({}); +export const ExecutorSubActionGetFieldsByIssueTypeParamsSchema = schema.object({ + id: schema.string(), +}); export const ExecutorParamsSchema = schema.oneOf([ schema.object({ @@ -74,4 +78,12 @@ export const ExecutorParamsSchema = schema.oneOf([ subAction: schema.literal('getCreateIssueMetadata'), subActionParams: ExecutorSubActionCreateIssueMetadataParamsSchema, }), + schema.object({ + subAction: schema.literal('getIssueTypes'), + subActionParams: ExecutorSubActionGetIssueTypesParamsSchema, + }), + schema.object({ + subAction: schema.literal('getFieldsByIssueType'), + subActionParams: ExecutorSubActionGetFieldsByIssueTypeParamsSchema, + }), ]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index cdf2e740105e1..98409716b8812 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -53,6 +53,8 @@ export const createExternalService = ( const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`; const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; const createIssueMetadataUrl = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; + const getIssueTypesOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; + const getIssueTypeFieldsOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`; const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`; const getIssueTypeFieldsUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`; const axiosInstance = axios.create({ @@ -66,8 +68,8 @@ export const createExternalService = ( const getCommentsURL = (issueId: string) => { return commentUrl.replace('{issueId}', issueId); }; - const createGetIssueTypeFieldsUrl = (issueTypeId: string) => { - return getIssueTypeFieldsUrl.replace('{issueTypeId}', issueTypeId); + const createGetIssueTypeFieldsUrl = (uri: string, issueTypeId: string) => { + return uri.replace('{issueTypeId}', issueTypeId); }; const createFields = (key: string, incident: Incident): Fields => { @@ -102,6 +104,25 @@ export const createExternalService = ( }, ''); }; + const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) => + createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c)); + + const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) => + issueTypes.map((type) => ({ id: type.id, name: type.name })); + + const normalizeFields = (fields: { + [key: string]: { allowedValues?: Array<{}>; defaultValue?: {} }; + }) => + Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => { + return { + ...fieldsAcc, + [fieldKey]: { + allowedValues: fields[fieldKey]?.allowedValues ?? [], + defaultValue: fields[fieldKey]?.defaultValue ?? {}, + }, + }; + }, {}); + const getIncident = async (id: string) => { try { const res = await request({ @@ -255,9 +276,7 @@ export const createExternalService = ( const getCreateIssueMetadata = async (): Promise => { try { const capabilitiesResponse = await getCapabilities(); - - const capabilities = Object.keys(capabilitiesResponse?.capabilities ?? {}); - const supportsNewAPI = createMetaCapabilities.every((c) => capabilities.includes(c)); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); if (!supportsNewAPI) { const res = await request({ @@ -321,7 +340,7 @@ export const createExternalService = ( const fieldsRes = await request({ axios: axiosInstance, method: 'get', - url: createGetIssueTypeFieldsUrl(issueType.id), + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueType.id), logger, proxySettings, }); @@ -382,6 +401,64 @@ export const createExternalService = ( } }; + const getIssueTypes = async () => { + const capabilitiesResponse = await getCapabilities(); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + + if (!supportsNewAPI) { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: getIssueTypesOldAPIURL, + logger, + proxySettings, + }); + + const issueTypes = res.data.projects[0]?.issuetypes ?? []; + return normalizeIssueTypes(issueTypes); + } else { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: getIssueTypesUrl, + logger, + proxySettings, + }); + + const issueTypes = res.data.values; + return issueTypes.map((type: { name: string }) => ({ name: type.name })); + } + }; + + const getFieldsByIssueType = async (issueTypeId: string) => { + const capabilitiesResponse = await getCapabilities(); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + + if (!supportsNewAPI) { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), + logger, + proxySettings, + }); + + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + return normalizeFields(fields); + } else { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), + logger, + proxySettings, + }); + + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + return normalizeFields(fields); + } + }; + return { getIncident, createIncident, @@ -390,5 +467,7 @@ export const createExternalService = ( findIncidents, getCreateIssueMetadata, getCapabilities, + getIssueTypes, + getFieldsByIssueType, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 0b9fecc1bab98..6c564007ba91b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -16,6 +16,8 @@ import { ExecutorSubActionHandshakeParamsSchema, ExecutorSubActionCreateIssueMetadataParamsSchema, ExecutorSubActionGetCapabilitiesParamsSchema, + ExecutorSubActionGetIssueTypesParamsSchema, + ExecutorSubActionGetFieldsByIssueTypeParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { IncidentConfigurationSchema } from './case_schema'; @@ -91,6 +93,12 @@ export interface GetCreateIssueMetadataResponse { issueTypes: IssueTypes; } +export type GetIssueTypesResponse = Array<{ id: string; name: string }>; +export type GetFieldsByIssueTypeResponse = Record< + string, + { allowedValues: Array<{}>; defaultValue: {} } +>; + export interface ExternalService { getIncident: (id: string) => Promise; findIncidents: (params?: Record) => Promise; @@ -99,6 +107,8 @@ export interface ExternalService { createComment: (params: CreateCommentParams) => Promise; getCreateIssueMetadata: () => Promise; getCapabilities: () => Promise; + getIssueTypes: () => Promise; + getFieldsByIssueType: (issueTypeId: string) => Promise; } export interface PushToServiceApiParams extends ExecutorSubActionPushParams { @@ -121,6 +131,14 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf< typeof ExecutorSubActionGetCapabilitiesParamsSchema >; +export type ExecutorSubActionGetIssueTypesParams = TypeOf< + typeof ExecutorSubActionGetIssueTypesParamsSchema +>; + +export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf< + typeof ExecutorSubActionGetFieldsByIssueTypeParamsSchema +>; + export interface ExternalServiceApiHandlerArgs { externalService: ExternalService; mapping: Map | null; @@ -143,6 +161,16 @@ export interface CreateIssueMetadataHandlerArgs { params: ExecutorSubActionCreateIssueMetadataParams; } +export interface GetIssueTypesHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetIssueTypesParams; +} + +export interface GetFieldsByIssueTypeHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetFieldsByIssueTypeParams; +} + export interface ExternalServiceApi { handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; @@ -150,9 +178,17 @@ export interface ExternalServiceApi { getCreateIssueMetadata: ( args: CreateIssueMetadataHandlerArgs ) => Promise; + getIssueTypes: (args: GetIssueTypesHandlerArgs) => Promise; + getFieldsByIssueType: ( + args: GetFieldsByIssueTypeHandlerArgs + ) => Promise; } -export type JiraExecutorResultData = PushToServiceResponse | GetCreateIssueMetadataResponse; +export type JiraExecutorResultData = + | PushToServiceResponse + | GetCreateIssueMetadataResponse + | GetIssueTypesResponse + | GetFieldsByIssueTypeResponse; export interface Fields { [key: string]: string | string[] | { name: string } | { key: string }; From cf74bd01c0f39a0b6b55a20c4049d3299596b73f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 25 Aug 2020 14:09:18 +0300 Subject: [PATCH 35/67] Use new api --- .../builtin_action_types/jira/api.ts | 30 ++ .../builtin_action_types/jira/jira_params.tsx | 322 ++++++++++-------- .../jira/use_get_fields_by_issue_type.tsx | 71 ++++ .../jira/use_get_issue_types.tsx | 53 +++ 4 files changed, 327 insertions(+), 149 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts index fc655a85e4ac7..6351503244378 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts @@ -20,3 +20,33 @@ export async function getCreateIssueMetadata({ }), }); } + +export async function getIssueTypes({ + http, + connectorId, +}: { + http: HttpSetup; + connectorId: string; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'getIssueTypes', subActionParams: {} }, + }), + }); +} + +export async function getFieldsByIssueType({ + http, + connectorId, + id, +}: { + http: HttpSetup; + connectorId: string; + id: string; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'getFieldsByIssueType', subActionParams: { id } }, + }), + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index e0dc857c5af80..fd730e41b67fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useEffect } from 'react'; -import { EuiFormRow, EuiComboBox, EuiLoadingSpinner } from '@elastic/eui'; +import React, { Fragment, useEffect, useState, useMemo } from 'react'; +import { map } from 'lodash/fp'; +import { EuiFormRow, EuiComboBox, EuiLoadingSpinner, EuiSelectOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; @@ -18,7 +19,8 @@ import { ActionParamsProps } from '../../../../types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { JiraActionParams } from './types'; -import { useCreateIssueMetadata } from './use_create_issue_metadata'; +import { useGetIssueTypes } from './use_get_issue_types'; +import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; const JiraParamsFields: React.FunctionComponent> = ({ actionParams, @@ -28,25 +30,54 @@ const JiraParamsFields: React.FunctionComponent { - const { http } = useAppDependencies(); const { title, description, comments, issueType, priority, labels, savedObjectId } = actionParams.subActionParams || {}; - const { - issueTypes, - issueTypesSelectOptions, - prioritiesSelectOptions, - hasDescription, - hasLabels, - isLoading, - } = useCreateIssueMetadata({ + + const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]); + const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); + const { http } = useAppDependencies(); + const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({ http, actionConnector, - selectedIssueType: issueType, + }); + const { isLoading: isLoadingFields, fields } = useGetFieldsByIssueType({ + http, + actionConnector, + issueType, }); + const hasLabels = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'labels'), [fields]); + const hasDescription = useMemo( + () => Object.prototype.hasOwnProperty.call(fields, 'description'), + [fields] + ); + + useEffect(() => { + const options = issueTypes.map((type) => ({ + value: type.id ?? '', + text: type.name ?? '', + })); + + setIssueTypesSelectOptions(options); + }, [issueTypes]); + + useEffect(() => { + if (issueType != null && fields != null) { + const priorities = fields.priority?.allowedValues ?? []; + const options = map( + (p) => ({ + value: p.name, + text: p.name, + }), + priorities + ); + setPrioritiesSelectOptions(options); + } + }, [fields, issueType]); + const labelOptions = labels ? labels.map((label: string) => ({ label })) : []; - const editSubActionProperty = (key: string, value: {}) => { + const editSubActionProperty = (key: string, value: any) => { const newProps = { ...actionParams.subActionParams, [key]: value }; editAction('subActionParams', newProps, index); }; @@ -58,28 +89,16 @@ const JiraParamsFields: React.FunctionComponent variable.name === 'alertId')) { editSubActionProperty('savedObjectId', '{{alertId}}'); } - if (!issueType && issueTypesSelectOptions.length > 0) { + + if (issueTypesSelectOptions.length > 0) { editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); } - if (!priority && prioritiesSelectOptions.length > 0) { + + if (prioritiesSelectOptions.length > 0) { editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); } - if (!labels && hasLabels) { - editSubActionProperty('labels', []); - } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - actionConnector, - title, - description, - comments, - issueType, - priority, - labels, - issueTypes, - issueTypesSelectOptions, - prioritiesSelectOptions, - ]); + }, [actionConnector, issueType, fields]); return ( @@ -87,8 +106,8 @@ const JiraParamsFields: React.FunctionComponentIncident - {isLoading && } - {!isLoading && ( + {isLoadingIssueTypes && } + {!isLoadingIssueTypes && ( <> - {prioritiesSelectOptions.length > 0 && ( - - - - { - editSubActionProperty('priority', e.target.value); - }} - /> - - - - )} - - {hasLabels && ( + {isLoadingFields && } + {!isLoadingFields && ( <> - - - - 0 && ( + + + { - const newOptions = [...labelOptions, { label: searchValue }]; - editSubActionProperty( - 'labels', - newOptions.map((newOption) => newOption.label) - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editSubActionProperty( - 'labels', - selectedOptions.map((selectedOption) => selectedOption.label) - ); - }} - onBlur={() => { - if (!labels) { - editSubActionProperty('labels', []); + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel', + { + defaultMessage: 'Priority', } - }} - isClearable={true} - data-test-subj="labelsComboBox" - /> - - - + )} + > + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + )} - - )} - 0 && title !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', - { - defaultMessage: 'Summary', - } - )} - > - - - {hasDescription && ( - + + + + { + const newOptions = [...labelOptions, { label: searchValue }]; + editSubActionProperty( + 'labels', + newOptions.map((newOption) => newOption.label) + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editSubActionProperty( + 'labels', + selectedOptions.map((selectedOption) => selectedOption.label) + ); + }} + onBlur={() => { + if (!labels) { + editSubActionProperty('labels', []); + } + }} + isClearable={true} + data-test-subj="labelsComboBox" + /> + + + + + )} - errors={errors.description as string[]} - /> + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', + { + defaultMessage: 'Summary', + } + )} + > + + + {hasDescription && ( + + )} + { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + }} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments (optional)', + } + )} + errors={errors.comments as string[]} + /> + )} - { - editSubActionProperty(key, [{ commentId: '1', comment: value }]); - }} - messageVariables={messageVariables} - paramsProperty={'comments'} - inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', - { - defaultMessage: 'Additional comments (optional)', - } - )} - errors={errors.comments as string[]} - /> )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx new file mode 100644 index 0000000000000..e594e3a962db9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -0,0 +1,71 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { HttpSetup } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getFieldsByIssueType } from './api'; + +interface Fields { + [key: string]: { + allowedValues: Array<{ name: string; id: string }> | []; + defaultValue: { name: string; id: string } | {}; + }; +} + +interface Props { + http: HttpSetup; + actionConnector: ActionConnector; + issueType: string; +} + +export interface UseCreateIssueMetadata { + fields: Fields; + isLoading: boolean; +} + +export const useGetFieldsByIssueType = ({ + http, + actionConnector, + issueType, +}: Props): UseCreateIssueMetadata => { + const [isLoading, setIsLoading] = useState(true); + const [fields, setFields] = useState({}); + + useEffect(() => { + let cancel = false; + const fetchData = async () => { + if (!issueType) { + setIsLoading(false); + return; + } + + setIsLoading(true); + const res = await getFieldsByIssueType({ + http, + connectorId: actionConnector.id, + id: issueType, + }); + + if (!cancel) { + setIsLoading(false); + setFields(res.data); + } + }; + + fetchData(); + + return () => { + cancel = true; + setIsLoading(false); + }; + }, [http, actionConnector, issueType]); + + return { + isLoading, + fields, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx new file mode 100644 index 0000000000000..316f5692c4146 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx @@ -0,0 +1,53 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { HttpSetup } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getIssueTypes } from './api'; + +type IssueTypes = Array<{ id: string; name: string }>; + +interface Props { + http: HttpSetup; + actionConnector: ActionConnector; +} + +export interface UseCreateIssueMetadata { + issueTypes: IssueTypes; + isLoading: boolean; +} + +export const useGetIssueTypes = ({ http, actionConnector }: Props): UseCreateIssueMetadata => { + const [isLoading, setIsLoading] = useState(true); + const [issueTypes, setIssueTypes] = useState([]); + + useEffect(() => { + let cancel = false; + const fetchData = async () => { + setIsLoading(true); + const res = await getIssueTypes({ + http, + connectorId: actionConnector.id, + }); + + if (!cancel) { + setIsLoading(false); + setIssueTypes(res.data); + } + }; + fetchData(); + return () => { + cancel = true; + setIsLoading(false); + }; + }, [http, actionConnector]); + + return { + issueTypes, + isLoading, + }; +}; From 8214217055199f79c8ed0723294e60e44959e0ca Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 25 Aug 2020 14:17:46 +0300 Subject: [PATCH 36/67] Remove create issue metadata endpoint --- .../server/builtin_action_types/jira/api.ts | 9 - .../server/builtin_action_types/jira/index.ts | 16 +- .../builtin_action_types/jira/schema.ts | 5 - .../builtin_action_types/jira/service.ts | 226 ++++++------------ .../server/builtin_action_types/jira/types.ts | 29 --- .../jira/use_create_issue_metadata.tsx | 127 ---------- 6 files changed, 73 insertions(+), 339 deletions(-) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 2cb1ab042dfc5..c6dd33c780ccc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -10,7 +10,6 @@ import { PushToServiceApiHandlerArgs, HandshakeApiHandlerArgs, GetIncidentApiHandlerArgs, - CreateIssueMetadataHandlerArgs, ExternalServiceApi, Incident, GetFieldsByIssueTypeHandlerArgs, @@ -34,13 +33,6 @@ const getIncidentHandler = async ({ params, }: GetIncidentApiHandlerArgs) => {}; -const getCreateIssueMetadataHandler = async ({ - externalService, -}: CreateIssueMetadataHandlerArgs) => { - const res = await externalService.getCreateIssueMetadata(); - return res; -}; - const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => { const res = await externalService.getIssueTypes(); return res; @@ -181,7 +173,6 @@ export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, getIncident: getIncidentHandler, - getCreateIssueMetadata: getCreateIssueMetadataHandler, getIssueTypes: getIssueTypesHandler, getFieldsByIssueType: getFieldsByIssueTypeHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 0b0dddf016878..60fe0cb69f7a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -20,7 +20,6 @@ import { api } from './api'; import { ExecutorParams, ExecutorSubActionPushParams, - ExecutorSubActionCreateIssueMetadataParams, JiraPublicConfigurationType, JiraSecretConfigurationType, JiraExecutorResultData, @@ -38,12 +37,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = [ - 'pushToService', - 'getCreateIssueMetadata', - 'getIssueTypes', - 'getFieldsByIssueType', -]; +const supportedSubActions: string[] = ['pushToService', 'getIssueTypes', 'getFieldsByIssueType']; // action type definition export function getActionType( @@ -125,14 +119,6 @@ async function executor( logger.debug(`response push to service for incident id: ${data.id}`); } - if (subAction === 'getCreateIssueMetadata') { - const getCreateIssueMetadataParams = subActionParams as ExecutorSubActionCreateIssueMetadataParams; - data = await api.getCreateIssueMetadata({ - externalService, - params: getCreateIssueMetadataParams, - }); - } - if (subAction === 'getIssueTypes') { const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; data = await api.getIssueTypes({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 071e95798e012..147b753ef10c0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -54,7 +54,6 @@ export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ // Reserved for future implementation export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); -export const ExecutorSubActionCreateIssueMetadataParamsSchema = schema.object({}); export const ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({}); export const ExecutorSubActionGetIssueTypesParamsSchema = schema.object({}); export const ExecutorSubActionGetFieldsByIssueTypeParamsSchema = schema.object({ @@ -74,10 +73,6 @@ export const ExecutorParamsSchema = schema.oneOf([ subAction: schema.literal('pushToService'), subActionParams: ExecutorSubActionPushParamsSchema, }), - schema.object({ - subAction: schema.literal('getCreateIssueMetadata'), - subActionParams: ExecutorSubActionCreateIssueMetadataParamsSchema, - }), schema.object({ subAction: schema.literal('getIssueTypes'), subActionParams: ExecutorSubActionGetIssueTypesParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 98409716b8812..af95d702db0db 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -18,7 +18,6 @@ import { CreateCommentParams, Incident, ResponseError, - GetCreateIssueMetadataResponse, ExternalServiceCommentResponse, ExternalServiceIncidentResponse, } from './types'; @@ -52,7 +51,6 @@ export const createExternalService = ( const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`; const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; - const createIssueMetadataUrl = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; const getIssueTypesOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; const getIssueTypeFieldsOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`; const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`; @@ -155,18 +153,22 @@ export const createExternalService = ( incident, }: CreateIncidentParams): Promise => { /* The response from Jira when creating an issue contains only the key and the id. - The function makes three calls when creating an issue: - 1. Get issueTypes to set a default in case incident.issueType is missing + The function makes the following calls when creating an issue: + 1. Get issueTypes to set a default ONLY when incident.issueType is missing 2. Create the issue. 3. Get the created issue with all the necessary fields. */ - const createIssueMetadata = await getCreateIssueMetadata(); + + let issueType = incident.issueType; + + if (!incident.issueType) { + const issueTypes = await getIssueTypes(); + issueType = issueTypes[0]?.name ?? 'Task'; + } + const fields = createFields(projectKey, { ...incident, - issueType: incident.issueType - ? incident.issueType - : createIssueMetadata.issueTypes[Object.keys(createIssueMetadata.issueTypes)[0]]?.name ?? - '', + issueType, }); try { @@ -273,60 +275,47 @@ export const createExternalService = ( } }; - const getCreateIssueMetadata = async (): Promise => { + const getCapabilities = async () => { try { - const capabilitiesResponse = await getCapabilities(); - const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + const res = await request({ + axios: axiosInstance, + method: 'get', + url: capabilitiesUrl, + logger, + proxySettings, + }); + + return { ...res.data }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to get capabilities. Error: ${error.message} Reason: ${createErrorMessage( + error.response.data.errors + )}` + ) + ); + } + }; + + const getIssueTypes = async () => { + const capabilitiesResponse = await getCapabilities(); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + try { if (!supportsNewAPI) { const res = await request({ axios: axiosInstance, method: 'get', - url: createIssueMetadataUrl, + url: getIssueTypesOldAPIURL, logger, proxySettings, }); const issueTypes = res.data.projects[0]?.issuetypes ?? []; - const metadata = issueTypes.reduce( - ( - acc: Record, - currentIssueType: { - name: string; - id: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fields: { [k: string]: { allowedValues?: any; defaultValue?: any } }; - } - ) => { - const fields = Object.keys(currentIssueType.fields ?? {}).reduce( - (fieldsAcc, fieldKey) => { - return { - ...fieldsAcc, - [fieldKey]: { - allowedValues: currentIssueType.fields[fieldKey]?.allowedValues ?? [], - defaultValue: currentIssueType.fields[fieldKey]?.defaultValue ?? {}, - }, - }; - }, - {} - ); - - return { - ...acc, - [currentIssueType.name]: { - id: currentIssueType.id, - name: currentIssueType.name, - fields, - }, - }; - }, - {} - ); - - return { issueTypes: metadata }; + return normalizeIssueTypes(issueTypes); } else { - let metadata = {}; - const issueTypeRes = await request({ + const res = await request({ axios: axiosInstance, method: 'get', url: getIssueTypesUrl, @@ -334,66 +323,54 @@ export const createExternalService = ( proxySettings, }); - const issueTypes = issueTypeRes.data.values; - - for (const issueType of issueTypes) { - const fieldsRes = await request({ - axios: axiosInstance, - method: 'get', - url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueType.id), - logger, - proxySettings, - }); - const fields = fieldsRes.data.values; - - metadata = { - ...metadata, - name: issueType.name, - [issueType.name]: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fields: fields.reduce((fieldsAcc: {}, field: Record) => { - return { - ...fieldsAcc, - [field.fieldId]: { - allowedValues: field.allowedValues ?? [], - defaultValue: field.defaultValue ?? {}, - }, - }; - }, {}), - }, - }; - } - - return { issueTypes: metadata }; + const issueTypes = res.data.values; + return issueTypes.map((type: { name: string }) => ({ name: type.name })); } } catch (error) { throw new Error( getErrorMessage( i18n.NAME, - `Unable to get create issue metadata. Error: ${ - error.message - } Reason: ${createErrorMessage(error.response.data.errors)}` + `Unable to get issue types. Error: ${error.message} Reason: ${createErrorMessage( + error.response.data.errors + )}` ) ); } }; - const getCapabilities = async () => { + const getFieldsByIssueType = async (issueTypeId: string) => { + const capabilitiesResponse = await getCapabilities(); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + try { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: capabilitiesUrl, - logger, - proxySettings, - }); + if (!supportsNewAPI) { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), + logger, + proxySettings, + }); - return { ...res.data }; + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + return normalizeFields(fields); + } else { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), + logger, + proxySettings, + }); + + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + return normalizeFields(fields); + } } catch (error) { throw new Error( getErrorMessage( i18n.NAME, - `Unable to get capabilities. Error: ${error.message} Reason: ${createErrorMessage( + `Unable to get fields. Error: ${error.message} Reason: ${createErrorMessage( error.response.data.errors )}` ) @@ -401,71 +378,12 @@ export const createExternalService = ( } }; - const getIssueTypes = async () => { - const capabilitiesResponse = await getCapabilities(); - const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - - if (!supportsNewAPI) { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: getIssueTypesOldAPIURL, - logger, - proxySettings, - }); - - const issueTypes = res.data.projects[0]?.issuetypes ?? []; - return normalizeIssueTypes(issueTypes); - } else { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: getIssueTypesUrl, - logger, - proxySettings, - }); - - const issueTypes = res.data.values; - return issueTypes.map((type: { name: string }) => ({ name: type.name })); - } - }; - - const getFieldsByIssueType = async (issueTypeId: string) => { - const capabilitiesResponse = await getCapabilities(); - const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); - - if (!supportsNewAPI) { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), - logger, - proxySettings, - }); - - const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; - return normalizeFields(fields); - } else { - const res = await request({ - axios: axiosInstance, - method: 'get', - url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), - logger, - proxySettings, - }); - - const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; - return normalizeFields(fields); - } - }; - return { getIncident, createIncident, updateIncident, createComment, findIncidents, - getCreateIssueMetadata, getCapabilities, getIssueTypes, getFieldsByIssueType, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 6c564007ba91b..f98ffee6f51ef 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -14,7 +14,6 @@ import { ExecutorSubActionPushParamsSchema, ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, - ExecutorSubActionCreateIssueMetadataParamsSchema, ExecutorSubActionGetCapabilitiesParamsSchema, ExecutorSubActionGetIssueTypesParamsSchema, ExecutorSubActionGetFieldsByIssueTypeParamsSchema, @@ -78,21 +77,6 @@ export interface CreateCommentParams { comment: Comment; } -export interface IssueTypes { - [key: string]: { - name: string; - fields: { - [key: string]: { - allowedValues: Array<{ name: string; id: string }> | []; - defaultValue: { name: string; id: string } | {}; - }; - }; - }; -} -export interface GetCreateIssueMetadataResponse { - issueTypes: IssueTypes; -} - export type GetIssueTypesResponse = Array<{ id: string; name: string }>; export type GetFieldsByIssueTypeResponse = Record< string, @@ -105,7 +89,6 @@ export interface ExternalService { createIncident: (params: CreateIncidentParams) => Promise; updateIncident: (params: UpdateIncidentParams) => Promise; createComment: (params: CreateCommentParams) => Promise; - getCreateIssueMetadata: () => Promise; getCapabilities: () => Promise; getIssueTypes: () => Promise; getFieldsByIssueType: (issueTypeId: string) => Promise; @@ -123,10 +106,6 @@ export type ExecutorSubActionHandshakeParams = TypeOf< typeof ExecutorSubActionHandshakeParamsSchema >; -export type ExecutorSubActionCreateIssueMetadataParams = TypeOf< - typeof ExecutorSubActionCreateIssueMetadataParamsSchema ->; - export type ExecutorSubActionGetCapabilitiesParams = TypeOf< typeof ExecutorSubActionGetCapabilitiesParamsSchema >; @@ -156,10 +135,6 @@ export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: ExecutorSubActionHandshakeParams; } -export interface CreateIssueMetadataHandlerArgs { - externalService: ExternalService; - params: ExecutorSubActionCreateIssueMetadataParams; -} export interface GetIssueTypesHandlerArgs { externalService: ExternalService; @@ -175,9 +150,6 @@ export interface ExternalServiceApi { handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; - getCreateIssueMetadata: ( - args: CreateIssueMetadataHandlerArgs - ) => Promise; getIssueTypes: (args: GetIssueTypesHandlerArgs) => Promise; getFieldsByIssueType: ( args: GetFieldsByIssueTypeHandlerArgs @@ -186,7 +158,6 @@ export interface ExternalServiceApi { export type JiraExecutorResultData = | PushToServiceResponse - | GetCreateIssueMetadataResponse | GetIssueTypesResponse | GetFieldsByIssueTypeResponse; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx deleted file mode 100644 index 105e95d71f2a5..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_create_issue_metadata.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 { useState, useEffect, useCallback } from 'react'; -import { EuiSelectOption } from '@elastic/eui'; -import { HttpSetup } from 'kibana/public'; -import { map } from 'lodash/fp'; -import { ActionConnector } from '../../../../types'; -import { getCreateIssueMetadata } from './api'; - -interface IssueTypes { - [key: string]: { - name: string; - fields: { - [key: string]: { - allowedValues: Array<{ name: string; id: string }> | []; - defaultValue: { name: string; id: string } | {}; - }; - }; - }; -} - -interface Props { - http: HttpSetup; - actionConnector: ActionConnector; - selectedIssueType?: string; -} - -export interface UseCreateIssueMetadata { - issueTypes: IssueTypes; - issueTypesSelectOptions: EuiSelectOption[]; - prioritiesSelectOptions: EuiSelectOption[]; - hasDescription: boolean; - hasLabels: boolean; - isLoading: boolean; -} - -export const useCreateIssueMetadata = ({ - http, - actionConnector, - selectedIssueType, -}: Props): UseCreateIssueMetadata => { - const [isLoading, setIsLoading] = useState(true); - const [issueTypes, setIssueTypes] = useState({}); - const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]); - const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); - const [hasDescription, setHasDescription] = useState(false); - const [hasLabels, setHasLabels] = useState(false); - - const hasField = useCallback( - (key) => { - if (selectedIssueType != null) { - const fields = issueTypes[selectedIssueType]?.fields ?? {}; - return Object.prototype.hasOwnProperty.call(fields, key); - } - - return false; - }, - [selectedIssueType, issueTypes] - ); - - useEffect(() => { - let cancel = false; - const fetchData = async () => { - setIsLoading(true); - const res = await getCreateIssueMetadata({ - http, - connectorId: actionConnector.id, - }); - - if (!cancel) { - setIsLoading(false); - setIssueTypes(res.data.issueTypes); - } - }; - fetchData(); - return () => { - cancel = true; - setIsLoading(false); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [http, actionConnector]); - - useEffect(() => { - const options = Object.keys(issueTypes).map((key) => ({ - value: key, - text: issueTypes[key].name ?? '', - })); - - setIssueTypesSelectOptions(options); - }, [issueTypes]); - - useEffect(() => { - if (selectedIssueType != null) { - const fields = issueTypes[selectedIssueType]?.fields ?? {}; - const priorities = fields.priority?.allowedValues ?? []; - const options = map( - (priority) => ({ - value: priority.name, - text: priority.name, - }), - priorities - ); - setPrioritiesSelectOptions(options); - } - }, [selectedIssueType, issueTypes]); - - useEffect(() => { - setHasDescription(hasField('description')); - }, [hasField]); - - useEffect(() => { - setHasLabels(hasField('labels')); - }, [hasField]); - - return { - issueTypes, - issueTypesSelectOptions, - prioritiesSelectOptions, - hasDescription, - hasLabels, - isLoading, - }; -}; From f802d4eb77463fa78024845161d3cbc40bee8722 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 25 Aug 2020 15:56:47 +0300 Subject: [PATCH 37/67] Reset fields when changing connector or issueType --- .../builtin_action_types/jira/service.ts | 4 ++-- .../server/builtin_action_types/jira/types.ts | 2 +- .../builtin_action_types/jira/jira_params.tsx | 22 +++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index af95d702db0db..b63cb647f6f7b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -77,7 +77,7 @@ export const createExternalService = ( }; if (incident.issueType) { - fields = { ...fields, issuetype: { name: incident.issueType } }; + fields = { ...fields, issuetype: { id: incident.issueType } }; } if (incident.description) { @@ -163,7 +163,7 @@ export const createExternalService = ( if (!incident.issueType) { const issueTypes = await getIssueTypes(); - issueType = issueTypes[0]?.name ?? 'Task'; + issueType = issueTypes[0]?.id ?? ''; } const fields = createFields(projectKey, { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index f98ffee6f51ef..4b04df309eb0b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -162,7 +162,7 @@ export type JiraExecutorResultData = | GetFieldsByIssueTypeResponse; export interface Fields { - [key: string]: string | string[] | { name: string } | { key: string }; + [key: string]: string | string[] | { name: string } | { key: string } | { id: string }; } export interface ResponseError { [k: string]: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index fd730e41b67fb..2c6d12c2e6a44 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -90,15 +90,29 @@ const JiraParamsFields: React.FunctionComponent 0) { + if (!issueType && issueTypesSelectOptions.length > 0) { editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); } - if (prioritiesSelectOptions.length > 0) { + if (!priority && prioritiesSelectOptions.length > 0) { editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionConnector, issueType, fields]); + }, [ + actionConnector, + issueType, + fields, + actionParams.subAction, + index, + savedObjectId, + issueTypesSelectOptions, + prioritiesSelectOptions, + ]); + + useEffect(() => { + editAction('subActionParams', { issueType }, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector, issueType]); return ( @@ -232,7 +246,7 @@ const JiraParamsFields: React.FunctionComponent Date: Tue, 25 Aug 2020 17:55:01 +0300 Subject: [PATCH 38/67] Improve layout --- .../builtin_action_types/jira/jira_params.tsx | 51 ++++++++++--------- .../servicenow/servicenow_params.tsx | 9 +++- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 2c6d12c2e6a44..97dcdae066bea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -117,7 +117,11 @@ const JiraParamsFields: React.FunctionComponent -

Incident

+

+ {i18n.translate('xpack.triggersActionsUI.components.builtinActionTypes.jira.title', { + defaultMessage: 'Issue', + })} +

{isLoadingIssueTypes && } @@ -172,6 +176,27 @@ const JiraParamsFields: React.FunctionComponent )} + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', + { + defaultMessage: 'Summary', + } + )} + > + + + {hasLabels && ( <> @@ -181,7 +206,7 @@ const JiraParamsFields: React.FunctionComponent @@ -216,26 +241,6 @@ const JiraParamsFields: React.FunctionComponent )} - 0 && title !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', - { - defaultMessage: 'Summary', - } - )} - > - - {hasDescription && ( -

Incident

+

+ {i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title', + { + defaultMessage: 'Incident', + } + )} +

Date: Tue, 25 Aug 2020 19:04:28 +0300 Subject: [PATCH 39/67] Improve service tests --- .../builtin_action_types/jira/service.test.ts | 584 +++++++++++++++++- .../builtin_action_types/jira/service.ts | 30 +- 2 files changed, 583 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index f1af8e2b547f1..fda57d4c9c0dc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -13,6 +13,10 @@ import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; +interface ResponseError extends Error { + response?: { data: { errors: Record } }; +} + jest.mock('axios'); jest.mock('../lib/axios_utils', () => { const originalUtils = jest.requireActual('../lib/axios_utils'); @@ -25,6 +29,72 @@ jest.mock('../lib/axios_utils', () => { axios.create = jest.fn(() => axios); const requestMock = utils.request as jest.Mock; +const issueTypesResponse = { + data: { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }, + ], + }, +}; + +const fieldsResponse = { + data: { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + fields: { + summary: { fieldId: 'summary' }, + priority: { + fieldId: 'priority', + allowedValues: [ + { + name: 'Highest', + id: '1', + }, + { + name: 'High', + id: '2', + }, + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '4', + }, + { + name: 'Lowest', + id: '5', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + }, + }, + ], + }, + ], + }, +}; + describe('Jira service', () => { let service: ExternalService; @@ -116,19 +186,24 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect(service.getIncident('1')).rejects.toThrow( - 'Unable to get incident with id 1. Error: An error has occurred' + '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field' ); }); }); describe('createIncident', () => { test('it creates the incident correctly', async () => { - // The response from Jira when creating an issue contains only the key and the id. - // The service makes two calls when creating an issue. One to create and one to get - // the created incident with all the necessary fields. + /* The response from Jira when creating an issue contains only the key and the id. + The function makes the following calls when creating an issue: + 1. Get issueTypes to set a default ONLY when incident.issueType is missing + 2. Create the issue. + 3. Get the created issue with all the necessary fields. + */ requestMock.mockImplementationOnce(() => ({ data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, })); @@ -142,7 +217,7 @@ describe('Jira service', () => { summary: 'title', description: 'desc', labels: [], - issueType: 'Task', + issueType: '10006', priority: 'High', }, }); @@ -155,6 +230,68 @@ describe('Jira service', () => { }); }); + test('it creates the incident correctly without issue type', async () => { + /* The response from Jira when creating an issue contains only the key and the id. + The function makes the following calls when creating an issue: + 1. Get issueTypes to set a default ONLY when incident.issueType is missing + 2. Create the issue. + 3. Get the created issue with all the necessary fields. + */ + // getIssueType mocks + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + // getIssueType mocks + requestMock.mockImplementationOnce(() => issueTypesResponse); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + })); + + const res = await service.createIncident({ + incident: { + summary: 'title', + description: 'desc', + labels: [], + priority: 'High', + issueType: null, + }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + logger, + method: 'post', + data: { + fields: { + summary: 'title', + description: 'desc', + project: { key: 'CK' }, + issuetype: { id: '10006' }, + labels: [], + priority: { name: 'High' }, + }, + }, + }); + }); + test('it should call request with correct arguments', async () => { requestMock.mockImplementation(() => ({ data: { @@ -169,7 +306,7 @@ describe('Jira service', () => { summary: 'title', description: 'desc', labels: [], - issueType: 'Task', + issueType: '10006', priority: 'High', }, }); @@ -184,7 +321,9 @@ describe('Jira service', () => { summary: 'title', description: 'desc', project: { key: 'CK' }, - issuetype: { name: 'Task' }, + issuetype: { id: '10006' }, + labels: [], + priority: { name: 'High' }, }, }, }); @@ -192,7 +331,9 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect( @@ -201,11 +342,13 @@ describe('Jira service', () => { summary: 'title', description: 'desc', labels: [], - issueType: 'Task', + issueType: '10006', priority: 'High', }, }) - ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred'); + ).rejects.toThrow( + '[Action][Jira]: Unable to create incident. Error: An error has occurred. Reason: Required field' + ); }); }); @@ -225,7 +368,7 @@ describe('Jira service', () => { summary: 'title', description: 'desc', labels: [], - issueType: 'Task', + issueType: '10006', priority: 'High', }, }); @@ -253,7 +396,7 @@ describe('Jira service', () => { summary: 'title', description: 'desc', labels: [], - issueType: 'Task', + issueType: '10006', priority: 'High', }, }); @@ -263,13 +406,24 @@ describe('Jira service', () => { logger, method: 'put', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', - data: { fields: { summary: 'title', description: 'desc' } }, + data: { + fields: { + summary: 'title', + description: 'desc', + labels: [], + priority: { name: 'High' }, + issuetype: { id: '10006' }, + project: { key: 'CK' }, + }, + }, }); }); test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect( @@ -279,12 +433,12 @@ describe('Jira service', () => { summary: 'title', description: 'desc', labels: [], - issueType: 'Task', + issueType: '10006', priority: 'High', }, }) ).rejects.toThrow( - '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred' + '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field' ); }); }); @@ -336,7 +490,9 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect( @@ -345,8 +501,398 @@ describe('Jira service', () => { comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, }) ).rejects.toThrow( - '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred' + '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' ); }); }); + + describe('getCapabilities', () => { + test('it should return the capabilities', async () => { + requestMock.mockImplementation(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + const res = await service.getCapabilities(); + expect(res).toEqual({ + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + await service.getCapabilities(); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + method: 'get', + url: 'https://siem-kibana.atlassian.net/rest/capabilities', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { capabilities: 'Could not get capabilities' } } }; + throw error; + }); + + expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities' + ); + }); + }); + + describe('getIssueTypes', () => { + describe('Old API', () => { + test('it should return the issue types', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => issueTypesResponse); + + const res = await service.getIssueTypes(); + + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => issueTypesResponse); + + await service.getIssueTypes(); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; + }); + + expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' + ); + }); + }); + describe('New API', () => { + test('it should return the issue types', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + })); + + const res = await service.getIssueTypes(); + + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + })); + + await service.getIssueTypes(); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; + }); + + expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' + ); + }); + }); + }); + + describe('getFieldsByIssueType', () => { + describe('Old API', () => { + test('it should return the fields', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => fieldsResponse); + + const res = await service.getFieldsByIssueType('10006'); + + expect(res).toEqual({ + priority: { + allowedValues: [ + { id: '1', name: 'Highest' }, + { id: '2', name: 'High' }, + { id: '3', name: 'Medium' }, + { id: '4', name: 'Low' }, + { id: '5', name: 'Lowest' }, + ], + defaultValue: { id: '3', name: 'Medium' }, + }, + summary: { allowedValues: [], defaultValue: {} }, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => fieldsResponse); + + await service.getFieldsByIssueType('10006'); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { fields: 'Could not get fields' } } }; + throw error; + }); + + expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields' + ); + }); + }); + + describe('New API', () => { + test('it should return the fields', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: [ + { fieldId: 'summary' }, + { + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + ], + }, + })); + + const res = await service.getFieldsByIssueType('10006'); + + expect(res).toEqual({ + priority: { + allowedValues: [{ id: '3', name: 'Medium' }], + defaultValue: { id: '3', name: 'Medium' }, + }, + summary: { allowedValues: [], defaultValue: {} }, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: [ + { fieldId: 'summary' }, + { + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + ], + }, + })); + + await service.getFieldsByIssueType('10006'); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; + }); + + expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' + ); + }); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index b63cb647f6f7b..c349023fa9592 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -139,7 +139,7 @@ export const createExternalService = ( i18n.NAME, `Unable to get incident with id ${id}. Error: ${ error.message - } Reason: ${createErrorMessage(error.response.data.errors)}` + } Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}` ) ); } @@ -196,7 +196,7 @@ export const createExternalService = ( getErrorMessage( i18n.NAME, `Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage( - error.response.data.errors + error.response?.data?.errors ?? {} )}` ) ); @@ -238,7 +238,7 @@ export const createExternalService = ( i18n.NAME, `Unable to update incident with id ${incidentId}. Error: ${ error.message - } Reason: ${createErrorMessage(error.response.data.errors)}` + }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}` ) ); } @@ -269,7 +269,7 @@ export const createExternalService = ( i18n.NAME, `Unable to create comment at incident with id ${incidentId}. Error: ${ error.message - } Reason: ${createErrorMessage(error.response.data.errors)}` + }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}` ) ); } @@ -290,8 +290,8 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to get capabilities. Error: ${error.message} Reason: ${createErrorMessage( - error.response.data.errors + `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} )}` ) ); @@ -324,14 +324,14 @@ export const createExternalService = ( }); const issueTypes = res.data.values; - return issueTypes.map((type: { name: string }) => ({ name: type.name })); + return normalizeIssueTypes(issueTypes); } } catch (error) { throw new Error( getErrorMessage( i18n.NAME, - `Unable to get issue types. Error: ${error.message} Reason: ${createErrorMessage( - error.response.data.errors + `Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} )}` ) ); @@ -363,15 +363,21 @@ export const createExternalService = ( proxySettings, }); - const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + const fields = res.data.values.reduce( + (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ + ...acc, + [value.fieldId]: { ...value }, + }), + {} + ); return normalizeFields(fields); } } catch (error) { throw new Error( getErrorMessage( i18n.NAME, - `Unable to get fields. Error: ${error.message} Reason: ${createErrorMessage( - error.response.data.errors + `Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} )}` ) ); From 4ec6e260c49d0e703a81b5e49f2ff025025956e9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Aug 2020 13:57:07 +0300 Subject: [PATCH 40/67] Improve api tests --- .../builtin_action_types/jira/api.test.ts | 274 ++++++++++++++++-- .../server/builtin_action_types/jira/api.ts | 4 + .../server/builtin_action_types/jira/mocks.ts | 38 ++- 3 files changed, 284 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index bcfb82077d286..596f0818db449 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../case/api'; +import { Logger } from '../../../../../../src/core/server'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; +import { api } from './api'; +let mockedLogger: jest.Mocked; describe('api', () => { let externalService: jest.Mocked; beforeEach(() => { externalService = externalServiceMock.create(); + jest.clearAllMocks(); }); afterEach(() => { @@ -20,10 +23,15 @@ describe('api', () => { }); describe('pushToService', () => { - describe('create incident', () => { + describe('create incident - cases', () => { test('it creates an incident', async () => { const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -45,7 +53,12 @@ describe('api', () => { test('it creates an incident without comments', async () => { const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -57,7 +70,7 @@ describe('api', () => { test('it calls createIncident correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -69,9 +82,25 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); + test('it calls createIncident correctly without mapping', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: 'Incident description', + summary: 'Incident title', + issueType: '10006', + labels: ['kibana', 'elastic'], + priority: 'High', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + test('it calls createComment correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', @@ -89,7 +118,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -108,14 +136,59 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, }); }); }); describe('update incident', () => { test('it updates an incident', async () => { - const res = await api.pushToService({ externalService, mapping, params: apiParams }); + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -137,7 +210,12 @@ describe('api', () => { test('it updates an incident without comments', async () => { const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -149,7 +227,7 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -162,9 +240,26 @@ describe('api', () => { expect(externalService.createIncident).not.toHaveBeenCalled(); }); + test('it calls updateIncident correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: 'Incident description', + summary: 'Incident title', + issueType: '10006', + labels: ['kibana', 'elastic'], + priority: 'High', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + test('it calls createComment correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', @@ -182,7 +277,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -201,7 +295,87 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + }); + }); + }); + + describe('getIssueTypes', () => { + test('it returns the issue types correctly', async () => { + const res = await api.getIssueTypes({ + externalService, + params: {}, + }); + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + }); + + describe('getFieldsByIssueType', () => { + test('it returns the issue types correctly', async () => { + const res = await api.getFieldsByIssueType({ + externalService, + params: { id: '10006' }, + }); + expect(res).toEqual({ + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, }); }); }); @@ -228,7 +402,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -260,7 +439,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -291,7 +475,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -324,7 +513,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: {}, @@ -352,7 +546,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -382,7 +581,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -414,7 +618,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -445,7 +654,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -478,7 +692,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -509,7 +728,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.createComment).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index c6dd33c780ccc..c9b49b64d2d09 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -102,6 +102,10 @@ const pushToServiceHandler = async ({ } if (comments && Array.isArray(comments) && comments.length > 0) { + if (mapping && mapping.get('comments')?.actionType === 'nothing') { + return res; + } + const commentsTransformed = mapping ? transformComments(comments, ['informationAdded']) : comments; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index 709d490a5227f..53f8d43ebc2d8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExternalService, - PushToServiceApiParams, - ExecutorSubActionPushParams, - MapRecord, -} from '../case/types'; +import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; + +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { @@ -40,6 +37,30 @@ const createMock = (): jest.Mocked => { }) ), createComment: jest.fn(), + findIncidents: jest.fn(), + getCapabilities: jest.fn(), + getIssueTypes: jest.fn().mockImplementation(() => [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]), + getFieldsByIssueType: jest.fn().mockImplementation(() => ({ + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + })), }; service.createComment.mockImplementationOnce(() => @@ -96,6 +117,9 @@ const executorParams: ExecutorSubActionPushParams = { updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', comments: [ { commentId: 'case-comment-1', @@ -118,7 +142,7 @@ const executorParams: ExecutorSubActionPushParams = { const apiParams: PushToServiceApiParams = { ...executorParams, - externalCase: { summary: 'Incident title', description: 'Incident description' }, + externalObject: { summary: 'Incident title', description: 'Incident description' }, }; export { externalServiceMock, mapping, executorParams, apiParams }; From 675451be51ba19b5292cb91ccc5a5b1d09fcabbb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Aug 2020 14:25:57 +0300 Subject: [PATCH 41/67] Create jira_connectors test --- .../jira/jira_connectors.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx new file mode 100644 index 0000000000000..ce6f25f54dcbc --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -0,0 +1,99 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocLinksStart } from 'kibana/public'; +import JiraConnectorFields from './Jira_connectors'; +import { JiraActionConnector } from './types'; + +describe('JiraActionConnectorFields renders', () => { + test('alerting Jira connector fields is rendered', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'token', + }, + id: 'test', + actionTypeId: '.jira', + isPreconfigured: false, + name: 'jira', + config: { + apiUrl: 'https://test/', + projectKey: 'CK', + }, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 + ).toBeTruthy(); + }); + + test('case specific Jira connector fields is rendered', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'token', + }, + id: 'test', + actionTypeId: '.jira', + isPreconfigured: false, + name: 'jira', + config: { + apiUrl: 'https://test/', + projectKey: 'CK', + }, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + consumer={'case'} + /> + ); + expect(wrapper.find('[data-test-subj="case-jira-mappings"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 + ).toBeTruthy(); + }); +}); From 140a0512854e6434dbb4ebf248b3ea156b617dfa Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Aug 2020 14:35:41 +0300 Subject: [PATCH 42/67] Create jira (ui) test --- .../builtin_action_types/jira/jira.test.tsx | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx new file mode 100644 index 0000000000000..61923d8f78b51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { JiraActionConnector } from './types'; + +const ACTION_TYPE_ID = '.jira'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('jira connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'apiToken', + }, + id: 'test', + actionTypeId: '.jira', + name: 'jira', + isPreconfigured: false, + config: { + apiUrl: 'https://siem-kibana.atlassian.net', + projectKey: 'CK', + }, + } as JiraActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: [], + email: [], + apiToken: [], + projectKey: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = ({ + secrets: { + email: 'user', + }, + id: '.jira', + actionTypeId: '.jira', + name: 'jira', + config: {}, + } as unknown) as JiraActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: ['URL is required.'], + email: [], + apiToken: ['API token or Password is required'], + projectKey: ['Project key is required'], + }, + }); + }); +}); + +describe('jira action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { title: 'some title {{test}}' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { title: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + subActionParams: { title: '' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + title: ['Title is required.'], + }, + }); + }); +}); From 7afdc5e7fce3ca00a54e036a039e14ab9d028601 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Aug 2020 19:56:45 +0300 Subject: [PATCH 43/67] Create jira_params test --- .../jira/jira_params.test.tsx | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx new file mode 100644 index 0000000000000..230fa09c659b5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -0,0 +1,184 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import JiraParamsFields from './jira_params'; +import { DocLinksStart } from 'kibana/public'; + +import { useGetIssueTypes } from './use_get_issue_types'; +import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; + +jest.mock('../../../app_context', () => { + const post = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ http: { post } })), + }; +}); + +jest.mock('./use_get_issue_types'); +jest.mock('./use_get_fields_by_issue_type'); + +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; + +const actionParams = { + subAction: 'pushToService', + subActionParams: { + title: 'sn title', + description: 'some description', + comments: [{ commentId: '1', comment: 'comment for jira' }], + issueType: '10006', + labels: ['kibana'], + priority: 'High', + savedObjectId: '123', + externalId: null, + }, +}; +const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, +}; + +describe('JiraParamsFields renders', () => { + const useGetIssueTypesResponse = { + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }; + + const useGetFieldsByIssueTypeResponse = { + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, + }; + + beforeEach(() => { + // jest.resetAllMocks(); + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + }); + + test('all params fields is rendered', () => { + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('value')).toStrictEqual( + '10006' + ); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual( + 'High' + ); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="labelsComboBox"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); + }); + + test('hide issue types and fields when loading issue types', () => { + useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeFalsy(); + }); + + test('hide fields when loading fields', () => { + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + isLoading: true, + }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeFalsy(); + }); + + test('hide unsupported fields', () => { + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + fields: {}, + }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy(); + }); +}); From fef9f154e58c89c65eb243e1942dbcd01b15953e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 26 Aug 2020 20:28:16 +0300 Subject: [PATCH 44/67] Add basic integration test --- .../actions/builtin_action_types/jira.ts | 91 +++++++++++++++++++ .../basic/tests/actions/index.ts | 1 + 2 files changed, 92 insertions(+) create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts new file mode 100644 index 0000000000000..33b4f67e85a9f --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts @@ -0,0 +1,91 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts + +const mapping = [ + { + source: 'title', + target: 'summary', + actionType: 'nothing', + }, + { + source: 'description', + target: 'description', + actionType: 'nothing', + }, + { + source: 'comments', + target: 'comments', + actionType: 'nothing', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function jiraTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockServiceNow = { + config: { + apiUrl: 'www.jiraisinkibanaactions.com', + incidentConfiguration: { mapping: [...mapping] }, + isCaseOwned: true, + }, + secrets: { + email: 'elastic', + apiToken: 'changeme', + }, + params: { + savedObjectId: '123', + title: 'a title', + description: 'a description', + labels: ['kibana'], + issueType: '10006', + priority: 'High', + externalId: null, + comments: [ + { + commentId: '456', + comment: 'first comment', + }, + ], + }, + }; + describe('jira', () => { + let jiraSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + jiraSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); + }); + + it('should return 403 when creating a jira action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: 'CK', + incidentConfiguration: { ...mockServiceNow.config.incidentConfiguration }, + isCaseOwned: true, + }, + secrets: mockServiceNow.secrets, + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts index 1788a12afebf2..8f31e7f96b562 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -11,6 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); + loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); From b93bd9f8b609413ffa7ef06271e3b75253a3dfb3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 30 Aug 2020 18:15:52 +0300 Subject: [PATCH 45/67] Add AbortController --- .../components/builtin_action_types/jira/api.ts | 9 +++++++++ .../jira/use_get_fields_by_issue_type.tsx | 13 +++++++++---- .../jira/use_get_issue_types.tsx | 16 ++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts index 6351503244378..7308b07055f97 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts @@ -9,38 +9,46 @@ import { BASE_ACTION_API_PATH } from '../../../constants'; export async function getCreateIssueMetadata({ http, + signal, connectorId, }: { http: HttpSetup; + signal: AbortSignal; connectorId: string; }): Promise> { return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'getCreateIssueMetadata', subActionParams: {} }, }), + signal, }); } export async function getIssueTypes({ http, + signal, connectorId, }: { http: HttpSetup; + signal: AbortSignal; connectorId: string; }): Promise> { return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { body: JSON.stringify({ params: { subAction: 'getIssueTypes', subActionParams: {} }, }), + signal, }); } export async function getFieldsByIssueType({ http, + signal, connectorId, id, }: { http: HttpSetup; + signal: AbortSignal; connectorId: string; id: string; }): Promise> { @@ -48,5 +56,6 @@ export async function getFieldsByIssueType({ body: JSON.stringify({ params: { subAction: 'getFieldsByIssueType', subActionParams: { id } }, }), + signal, }); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx index e594e3a962db9..5f9aa5554efb5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { HttpSetup } from 'kibana/public'; import { ActionConnector } from '../../../../types'; import { getFieldsByIssueType } from './api'; @@ -34,33 +34,38 @@ export const useGetFieldsByIssueType = ({ }: Props): UseCreateIssueMetadata => { const [isLoading, setIsLoading] = useState(true); const [fields, setFields] = useState({}); + const abortCtrl = useRef(new AbortController()); useEffect(() => { - let cancel = false; + let didCancel = false; const fetchData = async () => { if (!issueType) { setIsLoading(false); return; } + abortCtrl.current = new AbortController(); setIsLoading(true); const res = await getFieldsByIssueType({ http, + signal: abortCtrl.current.signal, connectorId: actionConnector.id, id: issueType, }); - if (!cancel) { + if (!didCancel) { setIsLoading(false); setFields(res.data); } }; + abortCtrl.current.abort(); fetchData(); return () => { - cancel = true; + didCancel = true; setIsLoading(false); + abortCtrl.current.abort(); }; }, [http, actionConnector, issueType]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx index 316f5692c4146..6a046016d0ba6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { HttpSetup } from 'kibana/public'; import { ActionConnector } from '../../../../types'; import { getIssueTypes } from './api'; @@ -24,25 +24,33 @@ export interface UseCreateIssueMetadata { export const useGetIssueTypes = ({ http, actionConnector }: Props): UseCreateIssueMetadata => { const [isLoading, setIsLoading] = useState(true); const [issueTypes, setIssueTypes] = useState([]); + const abortCtrl = useRef(new AbortController()); useEffect(() => { - let cancel = false; + let didCancel = false; const fetchData = async () => { + abortCtrl.current = new AbortController(); setIsLoading(true); + const res = await getIssueTypes({ http, + signal: abortCtrl.current.signal, connectorId: actionConnector.id, }); - if (!cancel) { + if (!didCancel) { setIsLoading(false); setIssueTypes(res.data); } }; + + abortCtrl.current.abort(); fetchData(); + return () => { - cancel = true; + didCancel = true; setIsLoading(false); + abortCtrl.current.abort(); }; }, [http, actionConnector]); From ee04cd8b4712823ea2464dcbc528e4145418b437 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 30 Aug 2020 18:24:27 +0300 Subject: [PATCH 46/67] Rename subactions --- .../builtin_action_types/jira/api.test.ts | 8 +++---- .../server/builtin_action_types/jira/api.ts | 4 ++-- .../server/builtin_action_types/jira/index.ts | 10 ++++----- .../builtin_action_types/jira/schema.ts | 4 ++-- .../server/builtin_action_types/jira/types.ts | 4 ++-- .../builtin_action_types/jira/validators.ts | 2 +- .../builtin_action_types/jira/api.ts | 21 ++----------------- .../jira/jira_connectors.test.tsx | 2 +- 8 files changed, 19 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index 596f0818db449..1aa6b91a11f1c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -340,9 +340,9 @@ describe('api', () => { }); }); - describe('getIssueTypes', () => { + describe('issueTypes', () => { test('it returns the issue types correctly', async () => { - const res = await api.getIssueTypes({ + const res = await api.issueTypes({ externalService, params: {}, }); @@ -359,9 +359,9 @@ describe('api', () => { }); }); - describe('getFieldsByIssueType', () => { + describe('fieldsByIssueType', () => { test('it returns the issue types correctly', async () => { - const res = await api.getFieldsByIssueType({ + const res = await api.fieldsByIssueType({ externalService, params: { id: '10006' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index c9b49b64d2d09..dd38ac852ebfb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -177,6 +177,6 @@ export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, getIncident: getIncidentHandler, - getIssueTypes: getIssueTypesHandler, - getFieldsByIssueType: getFieldsByIssueTypeHandler, + issueTypes: getIssueTypesHandler, + fieldsByIssueType: getFieldsByIssueTypeHandler, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 60fe0cb69f7a5..e2f13fe5d5f9a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -37,7 +37,7 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const supportedSubActions: string[] = ['pushToService', 'getIssueTypes', 'getFieldsByIssueType']; +const supportedSubActions: string[] = ['pushToService', 'issueTypes', 'fieldsByIssueType']; // action type definition export function getActionType( @@ -119,17 +119,17 @@ async function executor( logger.debug(`response push to service for incident id: ${data.id}`); } - if (subAction === 'getIssueTypes') { + if (subAction === 'issueTypes') { const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; - data = await api.getIssueTypes({ + data = await api.issueTypes({ externalService, params: getIssueTypesParams, }); } - if (subAction === 'getFieldsByIssueType') { + if (subAction === 'fieldsByIssueType') { const getFieldsByIssueTypeParams = subActionParams as ExecutorSubActionGetFieldsByIssueTypeParams; - data = await api.getFieldsByIssueType({ + data = await api.fieldsByIssueType({ externalService, params: getFieldsByIssueTypeParams, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 147b753ef10c0..b147f49affc2c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -74,11 +74,11 @@ export const ExecutorParamsSchema = schema.oneOf([ subActionParams: ExecutorSubActionPushParamsSchema, }), schema.object({ - subAction: schema.literal('getIssueTypes'), + subAction: schema.literal('issueTypes'), subActionParams: ExecutorSubActionGetIssueTypesParamsSchema, }), schema.object({ - subAction: schema.literal('getFieldsByIssueType'), + subAction: schema.literal('fieldsByIssueType'), subActionParams: ExecutorSubActionGetFieldsByIssueTypeParamsSchema, }), ]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 4b04df309eb0b..938ec02dd031d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -150,8 +150,8 @@ export interface ExternalServiceApi { handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; getIncident: (args: GetIncidentApiHandlerArgs) => Promise; - getIssueTypes: (args: GetIssueTypesHandlerArgs) => Promise; - getFieldsByIssueType: ( + issueTypes: (args: GetIssueTypesHandlerArgs) => Promise; + fieldsByIssueType: ( args: GetFieldsByIssueTypeHandlerArgs ) => Promise; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index 7f4bdac3b56e9..06a4b3fc68e77 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -26,7 +26,7 @@ export const validateCommonConfig = ( } try { - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + configurationUtilities.ensureUriAllowed(configObject.apiUrl); } catch (whitelistError) { return i18n.WHITE_LISTED_ERROR(whitelistError.message); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts index 7308b07055f97..86893e5b87ddf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/api.ts @@ -7,23 +7,6 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ACTION_API_PATH } from '../../../constants'; -export async function getCreateIssueMetadata({ - http, - signal, - connectorId, -}: { - http: HttpSetup; - signal: AbortSignal; - connectorId: string; -}): Promise> { - return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { - body: JSON.stringify({ - params: { subAction: 'getCreateIssueMetadata', subActionParams: {} }, - }), - signal, - }); -} - export async function getIssueTypes({ http, signal, @@ -35,7 +18,7 @@ export async function getIssueTypes({ }): Promise> { return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { body: JSON.stringify({ - params: { subAction: 'getIssueTypes', subActionParams: {} }, + params: { subAction: 'issueTypes', subActionParams: {} }, }), signal, }); @@ -54,7 +37,7 @@ export async function getFieldsByIssueType({ }): Promise> { return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { body: JSON.stringify({ - params: { subAction: 'getFieldsByIssueType', subActionParams: { id } }, + params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, }), signal, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index ce6f25f54dcbc..2cac1819d552d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DocLinksStart } from 'kibana/public'; -import JiraConnectorFields from './Jira_connectors'; +import JiraConnectorFields from './jira_connectors'; import { JiraActionConnector } from './types'; describe('JiraActionConnectorFields renders', () => { From cf317a4e095625de268657211ef1efa108496616 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 Aug 2020 13:22:03 +0300 Subject: [PATCH 47/67] Add toast notification on error --- .../builtin_action_types/jira/jira_params.tsx | 4 +- .../builtin_action_types/jira/translations.ts | 14 ++++++ .../jira/use_get_fields_by_issue_type.tsx | 43 ++++++++++++----- .../jira/use_get_issue_types.tsx | 46 ++++++++++++++----- 4 files changed, 84 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 97dcdae066bea..2289affd277d2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -35,13 +35,15 @@ const JiraParamsFields: React.FunctionComponent([]); const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); - const { http } = useAppDependencies(); + const { http, toastNotifications } = useAppDependencies(); const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({ http, + toastNotifications, actionConnector, }); const { isLoading: isLoadingFields, fields } = useGetFieldsByIssueType({ http, + toastNotifications, actionConnector, issueType, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 0dadea96b8bb0..988b3e3507035 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -117,3 +117,17 @@ export const MAPPING_FIELD_COMMENTS = i18n.translate( defaultMessage: 'Comments', } ); + +export const ISSUE_TYPES_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage', + { + defaultMessage: 'Unable to get issue types', + } +); + +export const FIELDS_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage', + { + defaultMessage: 'Unable to get fields', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx index 5f9aa5554efb5..7f2e719073ff1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -5,9 +5,10 @@ */ import { useState, useEffect, useRef } from 'react'; -import { HttpSetup } from 'kibana/public'; +import { HttpSetup, ToastsApi } from 'kibana/public'; import { ActionConnector } from '../../../../types'; import { getFieldsByIssueType } from './api'; +import * as i18n from './translations'; interface Fields { [key: string]: { @@ -18,6 +19,10 @@ interface Fields { interface Props { http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; actionConnector: ActionConnector; issueType: string; } @@ -29,6 +34,7 @@ export interface UseCreateIssueMetadata { export const useGetFieldsByIssueType = ({ http, + toastNotifications, actionConnector, issueType, }: Props): UseCreateIssueMetadata => { @@ -46,16 +52,31 @@ export const useGetFieldsByIssueType = ({ abortCtrl.current = new AbortController(); setIsLoading(true); - const res = await getFieldsByIssueType({ - http, - signal: abortCtrl.current.signal, - connectorId: actionConnector.id, - id: issueType, - }); + try { + const res = await getFieldsByIssueType({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + id: issueType, + }); - if (!didCancel) { - setIsLoading(false); - setFields(res.data); + if (!didCancel) { + setIsLoading(false); + setFields(res.data ?? {}); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: error.message, + }); + } } }; @@ -67,7 +88,7 @@ export const useGetFieldsByIssueType = ({ setIsLoading(false); abortCtrl.current.abort(); }; - }, [http, actionConnector, issueType]); + }, [http, actionConnector, issueType, toastNotifications]); return { isLoading, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx index 6a046016d0ba6..5f3f865178fcd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx @@ -5,14 +5,19 @@ */ import { useState, useEffect, useRef } from 'react'; -import { HttpSetup } from 'kibana/public'; +import { HttpSetup, ToastsApi } from 'kibana/public'; import { ActionConnector } from '../../../../types'; import { getIssueTypes } from './api'; +import * as i18n from './translations'; type IssueTypes = Array<{ id: string; name: string }>; interface Props { http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; actionConnector: ActionConnector; } @@ -21,7 +26,11 @@ export interface UseCreateIssueMetadata { isLoading: boolean; } -export const useGetIssueTypes = ({ http, actionConnector }: Props): UseCreateIssueMetadata => { +export const useGetIssueTypes = ({ + http, + actionConnector, + toastNotifications, +}: Props): UseCreateIssueMetadata => { const [isLoading, setIsLoading] = useState(true); const [issueTypes, setIssueTypes] = useState([]); const abortCtrl = useRef(new AbortController()); @@ -32,15 +41,30 @@ export const useGetIssueTypes = ({ http, actionConnector }: Props): UseCreateIss abortCtrl.current = new AbortController(); setIsLoading(true); - const res = await getIssueTypes({ - http, - signal: abortCtrl.current.signal, - connectorId: actionConnector.id, - }); + try { + const res = await getIssueTypes({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + }); - if (!didCancel) { - setIsLoading(false); - setIssueTypes(res.data); + if (!didCancel) { + setIsLoading(false); + setIssueTypes(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: error.message, + }); + } } }; @@ -52,7 +76,7 @@ export const useGetIssueTypes = ({ http, actionConnector }: Props): UseCreateIss setIsLoading(false); abortCtrl.current.abort(); }; - }, [http, actionConnector]); + }, [http, actionConnector, toastNotifications]); return { issueTypes, From fa7125cba9e30f77843e3ca1fac08d4d6160e9cd Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 Aug 2020 18:00:54 +0300 Subject: [PATCH 48/67] Improve UX --- .../builtin_action_types/jira/jira_params.tsx | 316 +++++++++--------- 1 file changed, 167 insertions(+), 149 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 2289affd277d2..95f368a8b4d6d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -6,7 +6,7 @@ import React, { Fragment, useEffect, useState, useMemo } from 'react'; import { map } from 'lodash/fp'; -import { EuiFormRow, EuiComboBox, EuiLoadingSpinner, EuiSelectOption } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiSelectOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; @@ -53,6 +53,9 @@ const JiraParamsFields: React.FunctionComponent Object.prototype.hasOwnProperty.call(fields, 'description'), [fields] ); + const hasPriority = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'priority'), [ + fields, + ]); useEffect(() => { const options = issueTypes.map((type) => ({ @@ -84,6 +87,20 @@ const JiraParamsFields: React.FunctionComponent { + setIssueTypesSelectOptions([]); + editAction('subActionParams', { savedObjectId }, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + // Reset fields when changing connector or issue type + useEffect(() => { + setPrioritiesSelectOptions([]); + editAction('subActionParams', { issueType, savedObjectId }, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [issueType, savedObjectId]); + useEffect(() => { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); @@ -91,30 +108,31 @@ const JiraParamsFields: React.FunctionComponent variable.name === 'alertId')) { editSubActionProperty('savedObjectId', '{{alertId}}'); } - - if (!issueType && issueTypesSelectOptions.length > 0) { - editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); - } - - if (!priority && prioritiesSelectOptions.length > 0) { - editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); - } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ actionConnector, - issueType, - fields, actionParams.subAction, index, savedObjectId, issueTypesSelectOptions, - prioritiesSelectOptions, + issueType, ]); + // Set default issue type + useEffect(() => { + if (issueTypesSelectOptions.length > 0) { + editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [issueTypes, issueTypesSelectOptions]); + + // Set default priority useEffect(() => { - editAction('subActionParams', { issueType }, index); + if (prioritiesSelectOptions.length > 0) { + editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [actionConnector, issueType]); + }, [actionConnector, issueType, prioritiesSelectOptions]); return ( @@ -126,159 +144,159 @@ const JiraParamsFields: React.FunctionComponent - {isLoadingIssueTypes && } - {!isLoadingIssueTypes && ( + <> + + { + editSubActionProperty('issueType', e.target.value); + }} + /> + + <> + {hasPriority && ( + + + + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + )} + 0 && title !== undefined} label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel', + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', { - defaultMessage: 'Issue type', + defaultMessage: 'Summary', } )} > - { - editSubActionProperty('issueType', e.target.value); - }} + - {isLoadingFields && } - {!isLoadingFields && ( + {hasLabels && ( <> - {prioritiesSelectOptions.length > 0 && ( - - - + + + { + const newOptions = [...labelOptions, { label: searchValue }]; + editSubActionProperty( + 'labels', + newOptions.map((newOption) => newOption.label) + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editSubActionProperty( + 'labels', + selectedOptions.map((selectedOption) => selectedOption.label) + ); + }} + onBlur={() => { + if (!labels) { + editSubActionProperty('labels', []); } - )} - > - { - editSubActionProperty('priority', e.target.value); - }} - /> - - - - )} + }} + isClearable={true} + data-test-subj="labelsComboBox" + /> + + +
- 0 && title !== undefined} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', - { - defaultMessage: 'Summary', - } - )} - > - - - - {hasLabels && ( - <> - - - - { - const newOptions = [...labelOptions, { label: searchValue }]; - editSubActionProperty( - 'labels', - newOptions.map((newOption) => newOption.label) - ); - }} - onChange={(selectedOptions: Array<{ label: string }>) => { - editSubActionProperty( - 'labels', - selectedOptions.map((selectedOption) => selectedOption.label) - ); - }} - onBlur={() => { - if (!labels) { - editSubActionProperty('labels', []); - } - }} - isClearable={true} - data-test-subj="labelsComboBox" - /> - - - - - - )} - {hasDescription && ( - - )} - { - editSubActionProperty(key, [{ commentId: '1', comment: value }]); - }} - messageVariables={messageVariables} - paramsProperty={'comments'} - inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', - { - defaultMessage: 'Additional comments (optional)', - } - )} - errors={errors.comments as string[]} - /> )} + {hasDescription && ( + + )} + { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + }} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments (optional)', + } + )} + errors={errors.comments as string[]} + /> - )} +
); }; From 56ebeaab8bd675887496f0fa49600e99947eeb99 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 Aug 2020 18:07:57 +0300 Subject: [PATCH 49/67] Change license --- .../application/components/builtin_action_types/jira/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts index 94a95a8d22629..628600ee91c8e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts @@ -14,7 +14,7 @@ export const connectorConfiguration = { enabled: true, enabledInConfig: true, enabledInLicense: true, - minimumLicenseRequired: 'platinum', + minimumLicenseRequired: 'gold', fields: { summary: { label: i18n.MAPPING_FIELD_SUMMARY, From b4f6a7cf429f4aebc5d35109e98b742e1bf9cefb Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 Aug 2020 18:35:24 +0300 Subject: [PATCH 50/67] Improve README --- x-pack/plugins/actions/README.md | 147 ++++++++++-------- .../builtin_action_types/jira/schema.ts | 3 +- 2 files changed, 88 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 868f6f180cc91..7c7a4aee3a5cc 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -19,7 +19,7 @@ Table of Contents - [Usage](#usage) - [Kibana Actions Configuration](#kibana-actions-configuration) - [Configuration Options](#configuration-options) - - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-hosts-allow-list) + - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) - [Configuration Utilities](#configuration-utilities) - [Action types](#action-types) - [Methods](#methods) @@ -74,13 +74,21 @@ Table of Contents - [`secrets`](#secrets-7) - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) + - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) - [IBM Resilient](#ibm-resilient) - [`config`](#config-8) - [`secrets`](#secrets-8) - [`params`](#params-8) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) + - [licensing](#licensing) + - [plugin location](#plugin-location) + - [documentation](#documentation) + - [tests](#tests) + - [action type config and secrets](#action-type-config-and-secrets) + - [user interface](#user-interface) ## Terminology @@ -103,12 +111,12 @@ Implemented under the [Actions Config](./server/actions_config.ts). Built-In-Actions are configured using the _xpack.actions_ namespoace under _kibana.yml_, and have the following configuration options: -| Namespaced Key | Description | Type | -| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | -| _xpack.actions._**allowedHosts** | Which _hostnames_ are allowed for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | -| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | -| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | +| Namespaced Key | Description | Type | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | +| _xpack.actions._**allowedHosts** | Which _hostnames_ are allowed for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | +| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | +| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | #### Adding Built-in Action Types to allowedHosts @@ -120,14 +128,14 @@ Uniquely, the _PagerDuty Action Type_ has been configured to support the service This module provides a Utilities for interacting with the configuration. -| Method | Arguments | Description | Return Type | -| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| isUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all URI's are allowed (using an "\*") then it will always return `true`. | Boolean | -| isHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will always return `true`. | Boolean | -| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | -| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | -| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | -| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | +| Method | Arguments | Description | Return Type | +| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| isUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all URI's are allowed (using an "\*") then it will always return `true`. | Boolean | +| isHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will always return `true`. | Boolean | +| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | +| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | +| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | +| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | ## Action types @@ -442,7 +450,7 @@ The config and params properties are modelled after the [Watcher Index Action](h | index | The Elasticsearch index to index into. | string _(optional)_ | | doc_id | The optional \_id of the document. | string _(optional)_ | | execution_time_field | The field that will store/index the action execution time. | string _(optional)_ | -| refresh | Setting of the refresh policy for the write request. | boolean _(optional)_ | +| refresh | Setting of the refresh policy for the write request. | boolean _(optional)_ | ### `secrets` @@ -450,9 +458,9 @@ This action type has no `secrets` properties. ### `params` -| Property | Description | Type | -| --------- | ---------------------------------------- | ------------------- | -| documents | JSON object that describes the [document](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index.html#getting-started-batch-processing). | object[] | +| Property | Description | Type | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| documents | JSON object that describes the [document](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index.html#getting-started-batch-processing). | object[] | --- @@ -529,10 +537,10 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `config` -| Property | Description | Type | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| apiUrl | ServiceNow instance URL. | string | -| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| apiUrl | ServiceNow instance URL. | string | +| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object _(optional)_ | ### `secrets` @@ -550,13 +558,17 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a #### `subActionParams (pushToService)` -| Property | Description | Type | -| ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| caseId | The case id | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| externalId | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object. | string | +| title | The title of the case. | string _(optional)_ | +| description | The description of the case. | string _(optional)_ | +| comment | A comment. | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | +| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| severity | The name of the severity in ServiceNow. | string _(optional)_ | +| urgency | The name of the urgency in ServiceNow. | string _(optional)_ | +| impact | The name of the impact in ServiceNow. | string _(optional)_ | --- @@ -568,34 +580,47 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `config` -| Property | Description | Type | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| apiUrl | Jira instance URL. | string | -| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| apiUrl | Jira instance URL. | string | +| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object _(optional)_ | ### `secrets` -| Property | Description | Type | -| -------- | --------------------------------------- | ------ | -| email | email for HTTP Basic authentication | string | -| apiToken | API token for HTTP Basic authentication | string | +| Property | Description | Type | +| -------- | ----------------------------------------------------- | ------ | +| email | email (or username) for HTTP Basic authentication | string | +| apiToken | API token (or password) for HTTP Basic authentication | string | ### `params` -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object | string | +| title | The title of the case | string _(optional)_ | +| description | The description of the case | string _(optional)_ | +| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| issueType | The id of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. | string[] _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | + +#### `subActionParams (issueTypes)` + +No parameters for `issueTypes` sub-action. Provide an empty object `{}`. #### `subActionParams (pushToService)` -| Property | Description | Type | -| ----------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | -| caseId | The case id | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| Property | Description | Type | +| -------- | -------------------------------- | ------ | +| id | The id of the issue type in Jira | string | ## IBM Resilient @@ -603,10 +628,10 @@ ID: `.resilient` ### `config` -| Property | Description | Type | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -| apiUrl | IBM Resilient instance URL. | string | -| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | +| apiUrl | IBM Resilient instance URL. | string | +| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | ### `secrets` @@ -624,13 +649,13 @@ ID: `.resilient` #### `subActionParams (pushToService)` -| Property | Description | Type | -| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| caseId | The case id | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object | string | +| title | The title of the case | string _(optional)_ | +| description | The description of the case | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | # Command Line Utility diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index b147f49affc2c..7821568c957ec 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -32,7 +32,8 @@ export const ExecutorSubActionSchema = schema.oneOf([ schema.literal('getIncident'), schema.literal('pushToService'), schema.literal('handshake'), - schema.literal('getCreateIssueMetadata'), + schema.literal('issueTypes'), + schema.literal('fieldsByIssueType'), ]); export const ExecutorSubActionPushParamsSchema = schema.object({ From dfaadca943a6c32d547a4deb8621fd35acc3c82e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 Aug 2020 18:40:55 +0300 Subject: [PATCH 51/67] Fix translations --- x-pack/plugins/translations/translations/ja-JP.json | 9 --------- x-pack/plugins/translations/translations/zh-CN.json | 9 --------- .../components/builtin_action_types/jira/translations.ts | 2 +- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8b9409f01087c..10483b2c0d58f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15115,7 +15115,6 @@ "xpack.securitySolution.case.configureCases.mappingFieldDescription": "説明", "xpack.securitySolution.case.configureCases.mappingFieldName": "名前", "xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "マップされません", - "xpack.securitySolution.case.configureCases.mappingFieldSummary": "まとめ", "xpack.securitySolution.case.configureCases.noConnector": "コネクターを選択していません", "xpack.securitySolution.case.configureCases.updateConnector": "コネクターを更新", "xpack.securitySolution.case.configureCases.updateSelectedConnector": "{ connectorName }を更新", @@ -15139,14 +15138,6 @@ "xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "パスワードが必要です", "xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "ユーザー名が必要です", "xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "ユーザー名", - "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira", - "xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel": "APIトークンまたはパスワード", - "xpack.securitySolution.case.connectors.jira.emailTextFieldLabel": "電子メールアドレスまたはユーザー名", - "xpack.securitySolution.case.connectors.jira.projectKey": "プロジェクトキー", - "xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField": "APIトークンまたはパスワードが必要です", - "xpack.securitySolution.case.connectors.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です", - "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", - "xpack.securitySolution.case.connectors.jira.selectMessageText": "Jiraでセキュリティケースデータを更新するか、新しいインシデントにプッシュ", "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", "xpack.securitySolution.case.connectors.resilient.apiKeyId": "APIキーID", "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "APIキーシークレット", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b9fb6340e38cf..cfc7885881cb3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15124,7 +15124,6 @@ "xpack.securitySolution.case.configureCases.mappingFieldDescription": "描述", "xpack.securitySolution.case.configureCases.mappingFieldName": "名称", "xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "未映射", - "xpack.securitySolution.case.configureCases.mappingFieldSummary": "摘要", "xpack.securitySolution.case.configureCases.noConnector": "未选择连接器", "xpack.securitySolution.case.configureCases.updateConnector": "更新连接器", "xpack.securitySolution.case.configureCases.updateSelectedConnector": "更新 { connectorName }", @@ -15148,14 +15147,6 @@ "xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "“密码”必填", "xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "“用户名”必填", "xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "用户名", - "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira", - "xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel": "API 令牌或密码", - "xpack.securitySolution.case.connectors.jira.emailTextFieldLabel": "电子邮件或用户名", - "xpack.securitySolution.case.connectors.jira.projectKey": "项目键", - "xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField": "“API 令牌或密码”必填", - "xpack.securitySolution.case.connectors.jira.requiredEmailTextField": "“电子邮件或用户名”必填", - "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "“项目键”必填", - "xpack.securitySolution.case.connectors.jira.selectMessageText": "将 Security 案例数据推送或更新到 Jira 中的新问题", "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", "xpack.securitySolution.case.connectors.resilient.apiKeyId": "API 密钥 ID", "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "API 密钥密码", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 988b3e3507035..bfcb72d1cb977 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -84,7 +84,7 @@ export const JIRA_API_TOKEN_REQUIRED = i18n.translate( ); export const MAPPING_FIELD_SUMMARY = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldSummary', + 'xpack.triggersActionsUI.case.configureCases.mappingFieldSummary', { defaultMessage: 'Summary', } From 7610b83e8f4db5d1b8e8a9624a30fa05c3e9c0fa Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 31 Aug 2020 19:07:40 +0300 Subject: [PATCH 52/67] Improvements --- .../actions/server/builtin_action_types/jira/service.ts | 6 ++---- .../components/builtin_action_types/jira/jira_params.tsx | 4 +++- .../jira/use_get_fields_by_issue_type.tsx | 4 ++-- .../builtin_action_types/jira/use_get_issue_types.tsx | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index c349023fa9592..8d4b674e08a79 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -29,8 +29,6 @@ import { ProxySettings } from '../../types'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; const CAPABILITIES_URL = `rest/capabilities`; -const INCIDENT_URL = `issue`; -const COMMENT_URL = `comment`; const VIEW_INCIDENT_URL = `browse`; @@ -48,9 +46,9 @@ export const createExternalService = ( throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } - const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; + const incidentUrl = `${url}/${BASE_URL}/issue`; const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`; - const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; + const commentUrl = `${incidentUrl}/{issueId}/comment`; const getIssueTypesOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; const getIssueTypeFieldsOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`; const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 95f368a8b4d6d..f74c2f9461341 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -80,7 +80,9 @@ const JiraParamsFields: React.FunctionComponent ({ label })) : []; + const labelOptions = useMemo(() => (labels ? labels.map((label: string) => ({ label })) : []), [ + labels, + ]); const editSubActionProperty = (key: string, value: any) => { const newProps = { ...actionParams.subActionParams, [key]: value }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx index 7f2e719073ff1..2c50d5846ab76 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -27,7 +27,7 @@ interface Props { issueType: string; } -export interface UseCreateIssueMetadata { +export interface UseGetFieldsByIssueType { fields: Fields; isLoading: boolean; } @@ -37,7 +37,7 @@ export const useGetFieldsByIssueType = ({ toastNotifications, actionConnector, issueType, -}: Props): UseCreateIssueMetadata => { +}: Props): UseGetFieldsByIssueType => { const [isLoading, setIsLoading] = useState(true); const [fields, setFields] = useState({}); const abortCtrl = useRef(new AbortController()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx index 5f3f865178fcd..102b3b4a7def3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx @@ -21,7 +21,7 @@ interface Props { actionConnector: ActionConnector; } -export interface UseCreateIssueMetadata { +export interface UseGetIssueTypes { issueTypes: IssueTypes; isLoading: boolean; } @@ -30,7 +30,7 @@ export const useGetIssueTypes = ({ http, actionConnector, toastNotifications, -}: Props): UseCreateIssueMetadata => { +}: Props): UseGetIssueTypes => { const [isLoading, setIsLoading] = useState(true); const [issueTypes, setIssueTypes] = useState([]); const abortCtrl = useRef(new AbortController()); From a2f20405d5aa9f5980047f5e97bb942d979e4521 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 1 Sep 2020 10:30:53 +0300 Subject: [PATCH 53/67] Fixes --- .../server/builtin_action_types/jira/api.ts | 2 +- .../builtin_action_types/jira/case_types.ts | 2 - .../builtin_action_types/jira/schema.ts | 2 +- .../servicenow/api.test.ts | 8 +- .../jira/jira_connectors.tsx | 8 +- .../jira/jira_params.test.tsx | 83 ++++++++++--- .../actions/builtin_action_types/jira.ts | 6 +- .../server/jira_simulation.ts | 51 ++++++++ .../actions_simulators/server/plugin.ts | 1 + .../actions/builtin_action_types/jira.ts | 111 ++++-------------- .../case_api_integration/common/lib/utils.ts | 3 +- 11 files changed, 153 insertions(+), 124 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index dd38ac852ebfb..de456691ea72c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -64,7 +64,7 @@ const pushToServiceHandler = async ({ currentIncident = await externalService.getIncident(externalId); } catch (ex) { logger.debug( - `Retrieving Incident by id ${externalId} from ServiceNow was failed with exception: ${ex}` + `Retrieving Incident by id ${externalId} from Jira was failed with exception: ${ex}` ); } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts index 2a535764ae3b9..b041ceb4c58c2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import { TypeOf } from '@kbn/config-schema'; import { ExecutorSubActionGetIncidentParamsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 7821568c957ec..20283e86f5393 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -10,7 +10,7 @@ import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '. export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), projectKey: schema.string(), - // TODO: to remove - set it optional for the current stage to support Case ServiceNow implementation + // TODO: to remove - set it optional for the current stage to support Case Jira implementation incidentConfiguration: schema.nullable(IncidentConfigurationSchema), isCaseOwned: schema.maybe(schema.boolean()), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 0bb096ecd0f62..7a68781bb9a75 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -91,7 +91,7 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); - test('it calls updateIncident correctly', async () => { + test('it calls updateIncident correctly when creating an incident and having comments', async () => { const params = { ...apiParams, externalId: null }; await api.pushToService({ externalService, @@ -103,7 +103,7 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenCalledTimes(2); expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { incident: { - comments: 'A comment', + comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', short_description: @@ -114,7 +114,7 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { incident: { - comments: 'Another comment', + comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', short_description: @@ -215,7 +215,7 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { incident: { - comments: 'A comment', + comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', short_description: diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index 0789821b30a05..e1be3adb0a246 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -21,7 +21,7 @@ import { JiraActionConnector, CasesConfigurationMapping } from './types'; import { connectorConfiguration } from './config'; import { FieldMapping } from './case_mappings/field_mapping'; -const ServiceNowConnectorFields: React.FC> = ({ +const JiraConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, @@ -30,7 +30,7 @@ const ServiceNowConnectorFields: React.FC { - // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution + // TODO: remove incidentConfiguration later, when Case Jira will move their fields to the level of action execution const { apiUrl, projectKey, incidentConfiguration, isCaseOwned } = action.config; const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; @@ -185,7 +185,7 @@ const ServiceNowConnectorFields: React.FC - {consumer === 'case' && ( // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + {consumer === 'case' && ( // TODO: remove this block later, when Case Jira will move their fields to the level of action execution <> @@ -215,4 +215,4 @@ export const createDefaultMapping = (fields: Record): CasesConfigur ); // eslint-disable-next-line import/no-default-export -export { ServiceNowConnectorFields as default }; +export { JiraConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx index 230fa09c659b5..26d358310741c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -80,12 +80,11 @@ describe('JiraParamsFields renders', () => { }; beforeEach(() => { - // jest.resetAllMocks(); useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); }); - test('all params fields is rendered', () => { + test('all params fields are rendered', () => { const wrapper = mountWithIntl( { expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); }); - test('hide issue types and fields when loading issue types', () => { + test('it shows loading when loading issue types', () => { useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true }); const wrapper = mountWithIntl( { actionConnector={connector} /> ); - expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeFalsy(); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('isLoading') + ).toBeTruthy(); }); - test('hide fields when loading fields', () => { - useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + test('it shows loading when loading fields', () => { useGetFieldsByIssueTypeMock.mockReturnValue({ ...useGetFieldsByIssueTypeResponse, isLoading: true, }); + const wrapper = mountWithIntl( { actionConnector={connector} /> ); - expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeFalsy(); + + expect( + wrapper.find('[data-test-subj="prioritySelect"]').first().prop('isLoading') + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading issue types', () => { + useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled') + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading fields', () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled') + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); }); test('hide unsupported fields', () => { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts index 33b4f67e85a9f..0afd10d5de42b 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts @@ -35,7 +35,7 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); - const mockServiceNow = { + const mockJira = { config: { apiUrl: 'www.jiraisinkibanaactions.com', incidentConfiguration: { mapping: [...mapping] }, @@ -81,10 +81,10 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: 'CK', - incidentConfiguration: { ...mockServiceNow.config.incidentConfiguration }, + incidentConfiguration: { ...mockJira.config.incidentConfiguration }, isCaseOwned: true, }, - secrets: mockServiceNow.secrets, + secrets: mockJira.secrets, }); }); }); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts index 4b65b7a8f2636..6041251dc28a4 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts @@ -105,6 +105,57 @@ export function initPlugin(router: IRouter, path: string) { }); } ); + + router.get( + { + path: `${path}/rest/capabilities`, + options: { + authRequired: false, + }, + validate: {}, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + capabilities: {}, + }); + } + ); + + router.get( + { + path: `${path}/rest/api/2/issue/createmeta`, + options: { + authRequired: false, + }, + validate: {}, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Sub-task', + }, + ], + }, + ], + }); + } + ); } function jsonResponse( diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 0f7acf5ead1a1..88f0f02794c9b 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -38,6 +38,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/createmeta`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); return allPaths; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 3ffd58b945ddb..61b3bf2867988 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -43,7 +43,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: 'www.jiraisinkibanaactions.com', projectKey: 'CK', - casesConfiguration: { mapping }, + incidentConfiguration: { mapping }, }, secrets: { apiToken: 'elastic', @@ -94,6 +94,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { ...mockJira.config, apiUrl: jiraSimulatorURL, + incidentConfiguration: mockJira.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockJira.secrets, }) @@ -107,7 +109,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, + isCaseOwned: true, }, }); @@ -123,7 +126,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, + isCaseOwned: true, }, }); }); @@ -178,7 +182,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://jira.mynonexistent.com', projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, }, secrets: mockJira.secrets, }) @@ -203,7 +207,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, }, }) .expect(400) @@ -217,30 +221,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a jira action without casesConfiguration', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A jira action', - actionTypeId: '.jira', - config: { - apiUrl: jiraSimulatorURL, - projectKey: mockJira.config.projectKey, - }, - secrets: mockJira.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', - }); - }); - }); - it('should respond with a 400 Bad Request when creating a jira action with empty mapping', async () => { await supertest .post('/api/actions/action') @@ -251,7 +231,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: { mapping: [] }, + incidentConfiguration: { mapping: [] }, }, secrets: mockJira.secrets, }) @@ -261,7 +241,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', }); }); }); @@ -276,7 +256,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -307,7 +287,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, }, secrets: mockJira.secrets, }); @@ -353,7 +333,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -371,7 +351,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -389,7 +369,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -412,31 +392,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', - }); - }); - }); - - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockJira.params, - subActionParams: { - savedObjectId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -464,7 +420,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -492,35 +448,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', - }); - }); - }); - - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockJira.params, - subActionParams: { - ...mockJira.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -537,6 +465,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { subActionParams: { ...mockJira.params.subActionParams, comments: [], + issueType: '10006', }, }, }) diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index fb6f4fce3c29a..c23df53c4feef 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -66,7 +66,7 @@ export const getJiraConnector = () => ({ config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey', - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -85,6 +85,7 @@ export const getJiraConnector = () => ({ }, ], }, + isCaseOwned: true, }, }); From 8dba0237d1e281cb395962101f710b4bc5a1de03 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Sep 2020 15:00:40 +0300 Subject: [PATCH 54/67] Validate 403 status code --- .../basic/tests/actions/builtin_action_types/jira.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts index 0afd10d5de42b..025fd558ee1ca 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts @@ -85,6 +85,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { isCaseOwned: true, }, secrets: mockJira.secrets, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .jira is disabled because your basic license does not support it. Please upgrade your license.', }); }); }); From 53fb03eac8025e2ba5a95df87559dd7702fb749c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Sep 2020 15:17:19 +0300 Subject: [PATCH 55/67] Change maybe to nullable --- .../server/builtin_action_types/case/utils.ts | 10 +++--- .../server/builtin_action_types/jira/api.ts | 36 ++++++++++--------- .../builtin_action_types/jira/case_schema.ts | 13 +++++-- .../builtin_action_types/jira/case_types.ts | 8 ++++- .../server/builtin_action_types/jira/index.ts | 4 ++- .../builtin_action_types/jira/schema.ts | 4 +-- .../builtin_action_types/jira/service.test.ts | 27 ++++++++++++-- .../actions/builtin_action_types/jira.ts | 4 +-- 8 files changed, 72 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index d895bf386a367..701bbea14fde8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -51,10 +51,7 @@ export const buildMap = (mapping: MapRecord[]): Map => { }, new Map()); }; -export const mapParams = ( - params: Partial, - mapping: Map -): AnyParams => { +export const mapParams = (params: T, mapping: Map): AnyParams => { return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { const field = mapping.get(curr); if (field) { @@ -106,7 +103,10 @@ export const createConnectorExecutor = ({ const { comments, externalId, ...restParams } = pushToServiceParams; const mapping = buildMap(config.casesConfiguration.mapping); - const externalCase = mapParams(restParams, mapping); + const externalCase = mapParams( + restParams as ExecutorSubActionPushParams, + mapping + ); data = await api.pushToService({ externalService, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index de456691ea72c..22a411bce1ea6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -18,7 +18,12 @@ import { // TODO: to remove, need to support Case import { transformers } from '../case/transformers'; -import { PushToServiceResponse, TransformFieldsArgs, Comment } from './case_types'; +import { + PushToServiceResponse, + TransformFieldsArgs, + Comment, + EntityInformation, +} from './case_types'; import { prepareFieldsForTransformation } from '../case/utils'; const handshakeHandler = async ({ @@ -141,14 +146,7 @@ export const transformFields = ({ [cur.key]: transform({ value: cur.value, date: params.updatedAt ?? params.createdAt, - user: - (params.updatedBy != null - ? params.updatedBy.fullName - ? params.updatedBy.fullName - : params.updatedBy.username - : params.createdBy.fullName - ? params.createdBy.fullName - : params.createdBy.username) ?? '', + user: getEntity(params), previousValue: currentIncident ? currentIncident[cur.key] : '', }).value, }; @@ -161,18 +159,22 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment comment: flow(...pipes.map((p) => transformers[p]))({ value: c.comment, date: c.updatedAt ?? c.createdAt, - user: - (c.updatedBy != null - ? c.updatedBy.fullName - ? c.updatedBy.fullName - : c.updatedBy.username - : c.createdBy.fullName - ? c.createdBy.fullName - : c.createdBy.username) ?? '', + user: getEntity(c), }).value, })); }; +export const getEntity = (entity: EntityInformation): string => + (entity.updatedBy != null + ? entity.updatedBy.fullName + ? entity.updatedBy.fullName + : entity.updatedBy.username + : entity.createdBy != null + ? entity.createdBy.fullName + ? entity.createdBy.fullName + : entity.createdBy.username + : '') ?? ''; + export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts index 2df8c8156cde8..5a23eb89339e6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts @@ -22,13 +22,20 @@ export const IncidentConfigurationSchema = schema.object({ mapping: schema.arrayOf(MapRecordSchema), }); +export const UserSchema = schema.object({ + fullName: schema.nullable(schema.string()), + username: schema.nullable(schema.string()), +}); + export const EntityInformation = { - createdAt: schema.maybe(schema.string()), - createdBy: schema.maybe(schema.any()), + createdAt: schema.nullable(schema.string()), + createdBy: schema.nullable(UserSchema), updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(schema.any()), + updatedBy: schema.nullable(UserSchema), }; +export const EntityInformationSchema = schema.object(EntityInformation); + export const CommentSchema = schema.object({ commentId: schema.string(), comment: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts index b041ceb4c58c2..ae44c5b4eb8aa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts @@ -9,7 +9,12 @@ import { ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, } from './schema'; -import { IncidentConfigurationSchema, MapRecordSchema, CommentSchema } from './case_schema'; +import { + IncidentConfigurationSchema, + MapRecordSchema, + CommentSchema, + EntityInformationSchema, +} from './case_schema'; import { PushToServiceApiParams, ExternalServiceIncidentResponse, @@ -23,6 +28,7 @@ export interface CreateCommentRequest { export type IncidentConfiguration = TypeOf; export type MapRecord = TypeOf; export type Comment = TypeOf; +export type EntityInformation = TypeOf; export interface ExternalServiceCommentResponse { commentId: string; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index e2f13fe5d5f9a..d3346557f3684 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -107,7 +107,9 @@ async function executor( const incidentConfiguration = config.incidentConfiguration; const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; const externalObject = - config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; + config.incidentConfiguration && mapping + ? mapParams(restParams as ExecutorSubActionPushParams, mapping) + : {}; data = await api.pushToService({ externalService, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 20283e86f5393..cab1625b6423e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -12,7 +12,7 @@ export const ExternalIncidentServiceConfiguration = { projectKey: schema.string(), // TODO: to remove - set it optional for the current stage to support Case Jira implementation incidentConfiguration: schema.nullable(IncidentConfigurationSchema), - isCaseOwned: schema.maybe(schema.boolean()), + isCaseOwned: schema.nullable(schema.boolean()), }; export const ExternalIncidentServiceConfigurationSchema = schema.object( @@ -45,7 +45,7 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ priority: schema.nullable(schema.string()), labels: schema.nullable(schema.arrayOf(schema.string())), // TODO: modify later to string[] - need for support Case schema - comments: schema.maybe(schema.arrayOf(CommentSchema)), + comments: schema.nullable(schema.arrayOf(CommentSchema)), ...EntityInformation, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index fda57d4c9c0dc..2439c507c3328 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -455,7 +455,14 @@ describe('Jira service', () => { const res = await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(res).toEqual({ @@ -476,7 +483,14 @@ describe('Jira service', () => { await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -498,7 +512,14 @@ describe('Jira service', () => { expect( service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1', updatedAt: null }, + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }) ).rejects.toThrow( '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 61b3bf2867988..84fad699525a9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -420,7 +420,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -448,7 +448,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); From 8be3a5b6acc219023f1d06a0f33bdec60fc7c524 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Sep 2020 15:20:49 +0300 Subject: [PATCH 56/67] Fix grammar --- x-pack/plugins/actions/server/builtin_action_types/jira/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 22a411bce1ea6..cc580bc56a793 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -69,7 +69,7 @@ const pushToServiceHandler = async ({ currentIncident = await externalService.getIncident(externalId); } catch (ex) { logger.debug( - `Retrieving Incident by id ${externalId} from Jira was failed with exception: ${ex}` + `Retrieving Incident by id ${externalId} from Jira failed with exception: ${ex}` ); } } From ce33e1b6fed258e5179a3ae3fe738339ac06d237 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Sep 2020 15:21:25 +0300 Subject: [PATCH 57/67] Improve filtering in case of null dereference --- .../case/server/routes/api/cases/configure/get_connectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 381d5a8da4970..a22d7ae5cea21 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -39,7 +39,7 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou // Need this filtering temporary to display only Case owned ServiceNow connectors (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) || ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID].includes(action.actionTypeId) && - action.config!.isCaseOwned)) + action.config?.isCaseOwned === true)) ); return response.ok({ body: results }); } catch (error) { From d3a111217c5a6d268d2ee716ab5355df13438d84 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 2 Sep 2020 20:02:11 +0300 Subject: [PATCH 58/67] Improve UI --- .../builtin_action_types/jira/jira_params.tsx | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index f74c2f9461341..04ec5fc0f32a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -6,7 +6,7 @@ import React, { Fragment, useEffect, useState, useMemo } from 'react'; import { map } from 'lodash/fp'; -import { EuiFormRow, EuiComboBox, EuiSelectOption } from '@elastic/eui'; +import { EuiFormRow, EuiComboBox, EuiSelectOption, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiSelect } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; @@ -138,14 +138,14 @@ const JiraParamsFields: React.FunctionComponent - + {/*

{i18n.translate('xpack.triggersActionsUI.components.builtinActionTypes.jira.title', { defaultMessage: 'Issue', })}

- + */} <> - + <> {hasPriority && ( - - - - + + + { - editSubActionProperty('priority', e.target.value); - }} - /> - - - + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.severitySelectFieldLabel', + { + defaultMessage: 'Priority', + } + )} + > + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + + )} - Date: Fri, 4 Sep 2020 18:07:01 +0300 Subject: [PATCH 59/67] Improve README --- x-pack/plugins/actions/README.md | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 7c7a4aee3a5cc..3bc8acead6c13 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -537,10 +537,10 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `config` -| Property | Description | Type | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| apiUrl | ServiceNow instance URL. | string | -| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object _(optional)_ | +| Property | Description | Type | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | +| apiUrl | ServiceNow instance URL. | string | +| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object _(optional)_ | ### `secrets` @@ -580,10 +580,10 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `config` -| Property | Description | Type | -| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| apiUrl | Jira instance URL. | string | -| incidentConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object _(optional)_ | +| Property | Description | Type | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| apiUrl | Jira instance URL. | string | +| incidentConfiguration | Optional property and specific to **Cases only**. if defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object _(optional)_ | ### `secrets` @@ -685,30 +685,30 @@ Consider working with the alerting team on early structure /design feedback of n ## licensing -Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. +Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. ## plugin location -Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. +Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. -Actions that take URLs or hostnames should check that those values are allowed. The allowed host list utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). +Actions that take URLs or hostnames should check that those values are allowed. The allowed host list utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). ## documentation -You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). +You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). ## tests -The action type should have both jest tests and functional tests. For functional tests, if your action interacts with a 3rd party service via HTTP, you may be able to create a simulator for your service, to test with. See the existing functional test servers in the directory [`x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server`](../../test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server) +The action type should have both jest tests and functional tests. For functional tests, if your action interacts with a 3rd party service via HTTP, you may be able to create a simulator for your service, to test with. See the existing functional test servers in the directory [`x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server`](../../test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server) ## action type config and secrets -Action types must define `config` and `secrets` which are used to create connectors. This data should be described with `@kbn/config-schema` object schemas, and you **MUST NOT** use `schema.maybe()` to define properties. +Action types must define `config` and `secrets` which are used to create connectors. This data should be described with `@kbn/config-schema` object schemas, and you **MUST NOT** use `schema.maybe()` to define properties. -This is due to the fact that the structures are persisted in saved objects, which performs partial updates on the persisted data. If a property value is already persisted, but an update either doesn't include the property, or sets it to `undefined`, the persisted value will not be changed. Beyond this being a semantic error in general, it also ends up invalidating the encryption used to save secrets, and will render the secrets will not be able to be unencrypted later. +This is due to the fact that the structures are persisted in saved objects, which performs partial updates on the persisted data. If a property value is already persisted, but an update either doesn't include the property, or sets it to `undefined`, the persisted value will not be changed. Beyond this being a semantic error in general, it also ends up invalidating the encryption used to save secrets, and will render the secrets will not be able to be unencrypted later. -Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `schema.maybe()` except that when passed an `undefined` value, the object returned from the validation will be set to `null`. The resulting type will be `property-type | null`, whereas with `schema.maybe()` it would be `property-type | undefined`. +Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `schema.maybe()` except that when passed an `undefined` value, the object returned from the validation will be set to `null`. The resulting type will be `property-type | null`, whereas with `schema.maybe()` it would be `property-type | undefined`. ## user interface -In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). +In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). From 1a43d83e0d4a831f7cc2d28e8aa8a2698adab35d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 4 Sep 2020 18:34:43 +0300 Subject: [PATCH 60/67] Move common types and schema to case folder --- .../case_schema.ts => case/common_schema.ts} | 0 .../case_types.ts => case/common_types.ts} | 29 ++------- .../server/builtin_action_types/jira/api.ts | 12 ++-- .../server/builtin_action_types/jira/mocks.ts | 2 +- .../builtin_action_types/jira/schema.ts | 6 +- .../server/builtin_action_types/jira/types.ts | 8 ++- .../builtin_action_types/servicenow/api.ts | 35 +++++------ .../servicenow/case_shema.ts | 36 ----------- .../servicenow/case_types.ts | 63 ------------------- .../builtin_action_types/servicenow/index.ts | 2 +- .../builtin_action_types/servicenow/mocks.ts | 2 +- .../builtin_action_types/servicenow/schema.ts | 6 +- .../builtin_action_types/servicenow/types.ts | 7 ++- .../jira/case_mappings/field_mapping.tsx | 9 ++- 14 files changed, 54 insertions(+), 163 deletions(-) rename x-pack/plugins/actions/server/builtin_action_types/{jira/case_schema.ts => case/common_schema.ts} (100%) rename x-pack/plugins/actions/server/builtin_action_types/{jira/case_types.ts => case/common_types.ts} (60%) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/common_schema.ts similarity index 100% rename from x-pack/plugins/actions/server/builtin_action_types/jira/case_schema.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/common_schema.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/common_types.ts similarity index 60% rename from x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/common_types.ts index ae44c5b4eb8aa..cca83fb88ca92 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/case_types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/common_types.ts @@ -5,21 +5,12 @@ */ import { TypeOf } from '@kbn/config-schema'; -import { - ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, -} from './schema'; import { IncidentConfigurationSchema, MapRecordSchema, CommentSchema, EntityInformationSchema, -} from './case_schema'; -import { - PushToServiceApiParams, - ExternalServiceIncidentResponse, - ExternalServiceParams, -} from './types'; +} from './common_schema'; export interface CreateCommentRequest { [key: string]: string; @@ -36,18 +27,6 @@ export interface ExternalServiceCommentResponse { externalCommentId?: string; } -export type ExecutorSubActionGetIncidentParams = TypeOf< - typeof ExecutorSubActionGetIncidentParamsSchema ->; - -export type ExecutorSubActionHandshakeParams = TypeOf< - typeof ExecutorSubActionHandshakeParamsSchema ->; - -export interface PushToServiceResponse extends ExternalServiceIncidentResponse { - comments?: ExternalServiceCommentResponse[]; -} - export interface PipedField { key: string; value: string; @@ -55,10 +34,10 @@ export interface PipedField { pipes: string[]; } -export interface TransformFieldsArgs { - params: PushToServiceApiParams; +export interface TransformFieldsArgs { + params: P; fields: PipedField[]; - currentIncident?: ExternalServiceParams; + currentIncident?: S; } export interface TransformerArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index cc580bc56a793..da47a4bfb839b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -14,16 +14,14 @@ import { Incident, GetFieldsByIssueTypeHandlerArgs, GetIssueTypesHandlerArgs, + PushToServiceApiParams, } from './types'; // TODO: to remove, need to support Case import { transformers } from '../case/transformers'; -import { - PushToServiceResponse, - TransformFieldsArgs, - Comment, - EntityInformation, -} from './case_types'; +import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types'; + +import { PushToServiceResponse } from './types'; import { prepareFieldsForTransformation } from '../case/utils'; const handshakeHandler = async ({ @@ -138,7 +136,7 @@ export const transformFields = ({ params, fields, currentIncident, -}: TransformFieldsArgs): Incident => { +}: TransformFieldsArgs): Incident => { return fields.reduce((prev, cur) => { const transform = flow(...cur.pipes.map((p) => transformers[p])); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index 53f8d43ebc2d8..e7841996fedef 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -6,7 +6,7 @@ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from '../case/types'; +import { MapRecord } from '../case/common_types'; const createMock = (): jest.Mocked => { const service = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index cab1625b6423e..07c8e22812b27 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -5,7 +5,11 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_schema'; +import { + CommentSchema, + EntityInformation, + IncidentConfigurationSchema, +} from '../case/common_schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 938ec02dd031d..eea8d1e7bcef5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -19,8 +19,8 @@ import { ExecutorSubActionGetFieldsByIssueTypeParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { IncidentConfigurationSchema } from './case_schema'; -import { PushToServiceResponse, Comment } from './case_types'; +import { IncidentConfigurationSchema } from '../case/common_schema'; +import { Comment } from '../case/common_types'; import { Logger } from '../../../../../../src/core/server'; export type JiraPublicConfigurationType = TypeOf; @@ -146,6 +146,10 @@ export interface GetFieldsByIssueTypeHandlerArgs { params: ExecutorSubActionGetFieldsByIssueTypeParams; } +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + export interface ExternalServiceApi { handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 06b92233627cc..c8e6147ecef46 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -10,11 +10,13 @@ import { HandshakeApiHandlerArgs, GetIncidentApiHandlerArgs, ExternalServiceApi, + PushToServiceApiParams, + PushToServiceResponse, } from './types'; // TODO: to remove, need to support Case import { transformers } from '../case/transformers'; -import { PushToServiceResponse, TransformFieldsArgs, Comment } from './case_types'; +import { TransformFieldsArgs, Comment, EntityInformation } from '../case/common_types'; import { prepareFieldsForTransformation } from '../case/utils'; const handshakeHandler = async ({ @@ -119,7 +121,7 @@ export const transformFields = ({ params, fields, currentIncident, -}: TransformFieldsArgs): Record => { +}: TransformFieldsArgs): Record => { return fields.reduce((prev, cur) => { const transform = flow(...cur.pipes.map((p) => transformers[p])); return { @@ -127,14 +129,7 @@ export const transformFields = ({ [cur.key]: transform({ value: cur.value, date: params.updatedAt ?? params.createdAt, - user: - (params.updatedBy != null - ? params.updatedBy.fullName - ? params.updatedBy.fullName - : params.updatedBy.username - : params.createdBy.fullName - ? params.createdBy.fullName - : params.createdBy.username) ?? '', + user: getEntity(params), previousValue: currentIncident ? currentIncident[cur.key] : '', }).value, }; @@ -147,18 +142,22 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment comment: flow(...pipes.map((p) => transformers[p]))({ value: c.comment, date: c.updatedAt ?? c.createdAt, - user: - (c.updatedBy != null - ? c.updatedBy.fullName - ? c.updatedBy.fullName - : c.updatedBy.username - : c.createdBy.fullName - ? c.createdBy.fullName - : c.createdBy.username) ?? '', + user: getEntity(c), }).value, })); }; +export const getEntity = (entity: EntityInformation): string => + (entity.updatedBy != null + ? entity.updatedBy.fullName + ? entity.updatedBy.fullName + : entity.updatedBy.username + : entity.createdBy != null + ? entity.createdBy.fullName + ? entity.createdBy.fullName + : entity.createdBy.username + : '') ?? ''; + export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts deleted file mode 100644 index 2df8c8156cde8..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 { schema } from '@kbn/config-schema'; - -export const MappingActionType = schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), -]); - -export const MapRecordSchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: MappingActionType, -}); - -export const IncidentConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapRecordSchema), -}); - -export const EntityInformation = { - createdAt: schema.maybe(schema.string()), - createdBy: schema.maybe(schema.any()), - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(schema.any()), -}; - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - ...EntityInformation, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts deleted file mode 100644 index 23a72ad4a894b..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { TypeOf } from '@kbn/config-schema'; -import { - ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, -} from './schema'; -import { IncidentConfigurationSchema, MapRecordSchema, CommentSchema } from './case_shema'; -import { - PushToServiceApiParams, - ExternalServiceIncidentResponse, - ExternalServiceParams, -} from './types'; - -export interface CreateCommentRequest { - [key: string]: string; -} - -export type IncidentConfiguration = TypeOf; -export type MapRecord = TypeOf; -export type Comment = TypeOf; - -export interface ExternalServiceCommentResponse { - commentId: string; - pushedDate: string; - externalCommentId?: string; -} - -export type ExecutorSubActionGetIncidentParams = TypeOf< - typeof ExecutorSubActionGetIncidentParamsSchema ->; - -export type ExecutorSubActionHandshakeParams = TypeOf< - typeof ExecutorSubActionHandshakeParamsSchema ->; - -export interface PushToServiceResponse extends ExternalServiceIncidentResponse { - comments?: ExternalServiceCommentResponse[]; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} - -export interface TransformFieldsArgs { - params: PushToServiceApiParams; - fields: PipedField[]; - currentIncident?: ExternalServiceParams; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 3addbe7c54dac..41a577918b18e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -24,11 +24,11 @@ import { ExecutorSubActionPushParams, ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, + PushToServiceResponse, } from './types'; // TODO: to remove, need to support Case import { buildMap, mapParams } from '../case/utils'; -import { PushToServiceResponse } from './case_types'; interface GetActionTypeParams { logger: Logger; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 5f22fcd4fdc85..55a14e4528acf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -5,7 +5,7 @@ */ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from './case_types'; +import { MapRecord } from '../case/common_types'; const createMock = (): jest.Mocked => { const service = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 82afebaaee445..921de42adfcaf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -5,7 +5,11 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_shema'; +import { + CommentSchema, + EntityInformation, + IncidentConfigurationSchema, +} from '../case/common_schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 0db9b6642ea5c..e8fcfac45d789 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -16,8 +16,8 @@ import { ExecutorSubActionHandshakeParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { IncidentConfigurationSchema } from './case_shema'; -import { PushToServiceResponse } from './case_types'; +import { ExternalServiceCommentResponse } from '../case/common_types'; +import { IncidentConfigurationSchema } from '../case/common_schema'; import { Logger } from '../../../../../../src/core/server'; export type ServiceNowPublicConfigurationType = TypeOf< @@ -52,6 +52,9 @@ export interface ExternalServiceIncidentResponse { url: string; pushedDate: string; } +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} export type ExternalServiceParams = Record; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx index ddab6c5b31a4f..a3382513d2bcb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx @@ -13,9 +13,8 @@ import * as i18n from './translations'; import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types'; -import { CasesConfigurationMapping } from '../types'; -import { connectorConfiguration } from '../config'; -import { createDefaultMapping } from '../jira_connectors'; +import { CasesConfigurationMapping } from './types'; +import { createDefaultMapping } from './utils'; const FieldRowWrapper = styled.div` margin-top: 8px; @@ -70,15 +69,15 @@ const getThirdPartyOptions = ( export interface FieldMappingProps { disabled: boolean; mapping: CasesConfigurationMapping[] | null; - connectorActionTypeId: string; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; + connectorConfiguration: Record; } const FieldMappingComponent: React.FC = ({ disabled, mapping, onChangeMapping, - connectorActionTypeId, + connectorConfiguration, }) => { const onChangeActionType = useCallback( (caseField: string, newActionType: string) => { From c9d843b77dee25ccb6a9d17d794ca6cf6e444fa9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 4 Sep 2020 18:36:58 +0300 Subject: [PATCH 61/67] Remove findIncidents function --- .../actions/server/builtin_action_types/jira/service.ts | 5 ----- .../actions/server/builtin_action_types/jira/types.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 8d4b674e08a79..84b6e70d2a100 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -143,10 +143,6 @@ export const createExternalService = ( } }; - const findIncidents = async (params?: Record) => { - return undefined; - }; - const createIncident = async ({ incident, }: CreateIncidentParams): Promise => { @@ -387,7 +383,6 @@ export const createExternalService = ( createIncident, updateIncident, createComment, - findIncidents, getCapabilities, getIssueTypes, getFieldsByIssueType, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index eea8d1e7bcef5..5e97f5309f8ee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -85,7 +85,6 @@ export type GetFieldsByIssueTypeResponse = Record< export interface ExternalService { getIncident: (id: string) => Promise; - findIncidents: (params?: Record) => Promise; createIncident: (params: CreateIncidentParams) => Promise; updateIncident: (params: UpdateIncidentParams) => Promise; createComment: (params: CreateCommentParams) => Promise; From 97a67c2dcf7f7411fabe9b517e411adad2b541e9 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 4 Sep 2020 18:41:20 +0300 Subject: [PATCH 62/67] Refactor for inclusive language --- .../actions/server/builtin_action_types/jira/translations.ts | 4 ++-- .../actions/server/builtin_action_types/jira/validators.ts | 4 ++-- .../server/builtin_action_types/servicenow/translations.ts | 4 ++-- .../server/builtin_action_types/servicenow/validators.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts index 2e70bc3a71a6d..0e71de813eb5d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -10,8 +10,8 @@ export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', { defaultMessage: 'Jira', }); -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.jira.configuration.apiWhitelistError', { +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.jira.configuration.apiAllowedHostsError', { defaultMessage: 'error configuring connector action: {message}', values: { message, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index 06a4b3fc68e77..58a3e27247fae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -27,8 +27,8 @@ export const validateCommonConfig = ( try { configurationUtilities.ensureUriAllowed(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 05c7d805a1852..7cc97a241c4bc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -10,8 +10,8 @@ export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { defaultMessage: 'ServiceNow', }); -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.configuration.apiWhitelistError', { +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', { defaultMessage: 'error configuring connector action: {message}', values: { message, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 6eec3b8d63b86..87bbfd9c7ea95 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -27,8 +27,8 @@ export const validateCommonConfig = ( try { configurationUtilities.ensureUriAllowed(configObject.apiUrl); - } catch (allowListError) { - return i18n.WHITE_LISTED_ERROR(allowListError.message); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); } }; From f2723d99d567acacae4265241f3023b97d2cdc64 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 4 Sep 2020 18:47:20 +0300 Subject: [PATCH 63/67] Make actionConnector optional --- .../builtin_action_types/email/email_params.test.tsx | 10 ---------- .../es_index/es_index_params.test.tsx | 9 --------- .../jira/use_get_fields_by_issue_type.tsx | 4 ++-- .../builtin_action_types/jira/use_get_issue_types.tsx | 7 ++++++- .../pagerduty/pagerduty_params.test.tsx | 9 --------- .../server_log/server_log_params.test.tsx | 11 ----------- .../servicenow/servicenow_params.test.tsx | 9 --------- .../builtin_action_types/slack/slack_params.test.tsx | 9 --------- .../webhook/webhook_params.test.tsx | 9 --------- x-pack/plugins/triggers_actions_ui/public/types.ts | 2 +- 10 files changed, 9 insertions(+), 70 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx index b1322367d497c..be3e8a31820c4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -18,15 +18,6 @@ describe('EmailParamsFields renders', () => { message: 'test message', }; - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; - const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 8819606b8c89e..25c04bda3f536 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -13,14 +13,6 @@ describe('IndexParamsFields renders', () => { const actionParams = { documents: [{ test: 123 }], }; - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(`{ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx index 2c50d5846ab76..08715822e5277 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -23,8 +23,8 @@ interface Props { ToastsApi, 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' >; - actionConnector: ActionConnector; issueType: string; + actionConnector?: ActionConnector; } export interface UseGetFieldsByIssueType { @@ -45,7 +45,7 @@ export const useGetFieldsByIssueType = ({ useEffect(() => { let didCancel = false; const fetchData = async () => { - if (!issueType) { + if (!actionConnector || !issueType) { setIsLoading(false); return; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx index 102b3b4a7def3..9ebaf5882d9b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx @@ -18,7 +18,7 @@ interface Props { ToastsApi, 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' >; - actionConnector: ActionConnector; + actionConnector?: ActionConnector; } export interface UseGetIssueTypes { @@ -38,6 +38,11 @@ export const useGetIssueTypes = ({ useEffect(() => { let didCancel = false; const fetchData = async () => { + if (!actionConnector) { + setIsLoading(false); + return; + } + abortCtrl.current = new AbortController(); setIsLoading(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index a03f6a0ae6927..9e37047ccda50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -22,14 +22,6 @@ describe('PagerDutyParamsFields renders', () => { group: 'group', class: 'test class', }; - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx index 95d77cab48dd1..3a015cddcd335 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx @@ -10,15 +10,6 @@ import ServerLogParamsFields from './server_log_params'; import { DocLinksStart } from 'kibana/public'; describe('ServerLogParamsFields renders', () => { - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; - test('all params fields is rendered', () => { const actionParams = { level: ServerLogLevelOptions.TRACE, @@ -32,7 +23,6 @@ describe('ServerLogParamsFields renders', () => { index={0} defaultMessage={'test default message'} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); @@ -54,7 +44,6 @@ describe('ServerLogParamsFields renders', () => { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx index 6cf0733b1f84f..1fc856b1e1ab2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -23,14 +23,6 @@ describe('ServiceNowParamsFields renders', () => { externalId: null, }, }; - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; const wrapper = mountWithIntl( { index={0} messageVariables={[]} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="urgencySelect"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx index 362f63ad4b6cf..7649d2dcb62c7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_params.test.tsx @@ -13,14 +13,6 @@ describe('SlackParamsFields renders', () => { const actionParams = { message: 'test message', }; - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx index 4f01ad3aa98b2..825c1372dfaf7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx @@ -13,14 +13,6 @@ describe('WebhookParamsFields renders', () => { const actionParams = { body: 'test message', }; - const connector = { - secrets: {}, - config: {}, - id: 'test', - actionTypeId: '.test', - name: 'Test', - isPreconfigured: false, - }; const wrapper = mountWithIntl( { editAction={() => {}} index={0} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} - actionConnector={connector} /> ); expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index cc4eb5aba3eaa..109d473c56e66 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -54,7 +54,7 @@ export interface ActionParamsProps { messageVariables?: ActionVariable[]; defaultMessage?: string; docLinks: DocLinksStart; - actionConnector: ActionConnector; + actionConnector?: ActionConnector; } export interface Pagination { From ccd74cbbbb4c4d736d1457fadd27ebb515d4f48d Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 4 Sep 2020 18:51:30 +0300 Subject: [PATCH 64/67] Move case mappings to its own folder --- .../case_mappings/field_mapping.tsx | 0 .../case_mappings/field_mapping_row.tsx | 0 .../{jira => }/case_mappings/translations.ts | 0 .../{jira => }/case_mappings/types.ts | 7 +- .../{servicenow => }/case_mappings/utils.ts | 12 +- .../jira/case_mappings/utils.ts | 38 ---- .../jira/jira_connectors.tsx | 19 +- .../builtin_action_types/jira/jira_params.tsx | 9 - .../builtin_action_types/jira/types.ts | 7 +- .../case_mappings/field_mapping.tsx | 141 ------------- .../case_mappings/field_mapping_row.tsx | 78 ------- .../servicenow/case_mappings/translations.ts | 190 ------------------ .../servicenow/case_mappings/types.ts | 16 -- .../servicenow/servicenow_connectors.tsx | 19 +- .../builtin_action_types/servicenow/types.ts | 9 +- 15 files changed, 33 insertions(+), 512 deletions(-) rename x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/{jira => }/case_mappings/field_mapping.tsx (100%) rename x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/{jira => }/case_mappings/field_mapping_row.tsx (100%) rename x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/{jira => }/case_mappings/translations.ts (100%) rename x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/{jira => }/case_mappings/types.ts (72%) rename x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/{servicenow => }/case_mappings/utils.ts (76%) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/field_mapping_row.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/translations.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts index 6cd2200e1dc74..3571db39b596a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../../../../types'; +import { ActionType } from '../../../../types'; export { ActionType }; @@ -14,3 +14,8 @@ export interface ThirdPartyField { defaultSourceField: string; defaultActionType: string; } +export interface CasesConfigurationMapping { + source: string; + target: string; + actionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts index a173d90515302..b14b1b76427c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../types'; +import { CasesConfigurationMapping } from './types'; export const setActionTypeToMapping = ( caseField: string, @@ -36,3 +36,13 @@ export const setThirdPartyToMapping = ( } return item; }); + +export const createDefaultMapping = (fields: Record): CasesConfigurationMapping[] => + Object.keys(fields).map( + (key) => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts deleted file mode 100644 index a173d90515302..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/case_mappings/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { CasesConfigurationMapping } from '../types'; - -export const setActionTypeToMapping = ( - caseField: string, - newActionType: string, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => { - const findItemIndex = mapping.findIndex((item) => item.source === caseField); - - if (findItemIndex >= 0) { - return [ - ...mapping.slice(0, findItemIndex), - { ...mapping[findItemIndex], actionType: newActionType }, - ...mapping.slice(findItemIndex + 1), - ]; - } - - return [...mapping]; -}; - -export const setThirdPartyToMapping = ( - caseField: string, - newThirdPartyField: string, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => - mapping.map((item) => { - if (item.source !== caseField && item.target === newThirdPartyField) { - return { ...item, target: 'not_mapped' }; - } else if (item.source === caseField) { - return { ...item, target: newThirdPartyField }; - } - return item; - }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index e1be3adb0a246..1fc1853a514c0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -16,10 +16,13 @@ import { import { isEmpty } from 'lodash'; import { ActionConnectorFieldsProps } from '../../../../types'; +import { CasesConfigurationMapping } from '../case_mappings/types'; +import { FieldMapping } from '../case_mappings/field_mapping'; +import { createDefaultMapping } from '../case_mappings/utils'; + import * as i18n from './translations'; -import { JiraActionConnector, CasesConfigurationMapping } from './types'; +import { JiraActionConnector } from './types'; import { connectorConfiguration } from './config'; -import { FieldMapping } from './case_mappings/field_mapping'; const JiraConnectorFields: React.FC> = ({ action, @@ -192,7 +195,7 @@ const JiraConnectorFields: React.FC @@ -204,15 +207,5 @@ const JiraConnectorFields: React.FC): CasesConfigurationMapping[] => - Object.keys(fields).map( - (key) => - ({ - source: fields[key].defaultSourceField, - target: key, - actionType: fields[key].defaultActionType, - } as CasesConfigurationMapping) - ); - // eslint-disable-next-line import/no-default-export export { JiraConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 04ec5fc0f32a3..25995eabb250d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -12,7 +12,6 @@ import { EuiSelect } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { EuiTitle } from '@elastic/eui'; import { useAppDependencies } from '../../../app_context'; import { ActionParamsProps } from '../../../../types'; @@ -138,14 +137,6 @@ const JiraParamsFields: React.FunctionComponent - {/* -

- {i18n.translate('xpack.triggersActionsUI.components.builtinActionTypes.jira.title', { - defaultMessage: 'Issue', - })} -

-
- */} <> > = [ - { - value: 'nothing', - inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}, - 'data-test-subj': 'edit-update-option-nothing', - }, - { - value: 'overwrite', - inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}, - 'data-test-subj': 'edit-update-option-overwrite', - }, - { - value: 'append', - inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}, - 'data-test-subj': 'edit-update-option-append', - }, -]; - -const getThirdPartyOptions = ( - caseField: string, - thirdPartyFields: Record -): Array> => - (Object.keys(thirdPartyFields) as string[]).reduce>>( - (acc, key) => { - if (thirdPartyFields[key].validSourceFields.includes(caseField)) { - return [ - ...acc, - { - value: key, - inputDisplay: {thirdPartyFields[key].label}, - 'data-test-subj': `dropdown-mapping-${key}`, - }, - ]; - } - return acc; - }, - [ - { - value: 'not_mapped', - inputDisplay: i18n.MAPPING_FIELD_NOT_MAPPED, - 'data-test-subj': 'dropdown-mapping-not_mapped', - }, - ] - ); - -export interface FieldMappingProps { - disabled: boolean; - mapping: CasesConfigurationMapping[] | null; - connectorActionTypeId: string; - onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; -} - -const FieldMappingComponent: React.FC = ({ - disabled, - mapping, - onChangeMapping, - connectorActionTypeId, -}) => { - const onChangeActionType = useCallback( - (caseField: string, newActionType: string) => { - const myMapping = mapping ?? defaultMapping; - onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapping] - ); - - const onChangeThirdParty = useCallback( - (caseField: string, newThirdPartyField: string) => { - const myMapping = mapping ?? defaultMapping; - onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [mapping] - ); - - const selectedConnector = connectorConfiguration ?? { fields: {} }; - const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ - selectedConnector.fields, - ]); - - return ( - <> - - - - {i18n.FIELD_MAPPING_FIRST_COL} - - - {i18n.FIELD_MAPPING_SECOND_COL} - - - {i18n.FIELD_MAPPING_THIRD_COL} - - - - - {(mapping ?? defaultMapping).map((item) => ( - - ))} - - - ); -}; - -export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx deleted file mode 100644 index beca8f1fbbc77..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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, { useMemo } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiSuperSelect, - EuiIcon, - EuiSuperSelectOption, -} from '@elastic/eui'; - -import { capitalize } from 'lodash'; - -export interface RowProps { - id: string; - disabled: boolean; - securitySolutionField: string; - thirdPartyOptions: Array>; - actionTypeOptions: Array>; - onChangeActionType: (caseField: string, newActionType: string) => void; - onChangeThirdParty: (caseField: string, newThirdPartyField: string) => void; - selectedActionType: string; - selectedThirdParty: string; -} - -const FieldMappingRowComponent: React.FC = ({ - id, - disabled, - securitySolutionField, - thirdPartyOptions, - actionTypeOptions, - onChangeActionType, - onChangeThirdParty, - selectedActionType, - selectedThirdParty, -}) => { - const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ - securitySolutionField, - ]); - return ( - - - - - {securitySolutionFieldCapitalized} - - - - - - - - - - - - - - ); -}; - -export const FieldMappingRow = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts deleted file mode 100644 index 665ccbcfa114d..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle', - { - defaultMessage: 'Connect to external incident management system', - } -); - -export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc', - { - defaultMessage: - 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', - } -); - -export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel', - { - defaultMessage: 'Incident management system', - } -); - -export const NO_CONNECTOR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.noConnector', - { - defaultMessage: 'No connector selected', - } -); - -export const ADD_NEW_CONNECTOR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector', - { - defaultMessage: 'Add new connector', - } -); - -export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle', - { - defaultMessage: 'Case Closures', - } -); - -export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc', - { - defaultMessage: - 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', - } -); - -export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel', - { - defaultMessage: 'Case closure options', - } -); - -export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual', - { - defaultMessage: 'Manually close Security cases', - } -); - -export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident', - { - defaultMessage: - 'Automatically close Security cases when pushing new incident to external system', - } -); - -export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident', - { - defaultMessage: 'Automatically close Security cases when incident is closed in external system', - } -); - -export const FIELD_MAPPING_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle', - { - defaultMessage: 'Field mappings', - } -); - -export const FIELD_MAPPING_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc', - { - defaultMessage: - 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', - } -); - -export const FIELD_MAPPING_FIRST_COL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol', - { - defaultMessage: 'Security case field', - } -); - -export const FIELD_MAPPING_SECOND_COL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol', - { - defaultMessage: 'External incident field', - } -); - -export const FIELD_MAPPING_THIRD_COL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol', - { - defaultMessage: 'On edit and update', - } -); - -export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing', - { - defaultMessage: 'Nothing', - } -); - -export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite', - { - defaultMessage: 'Overwrite', - } -); - -export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend', - { - defaultMessage: 'Append', - } -); - -export const CANCEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.cancelButton', - { - defaultMessage: 'Cancel', - } -); - -export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.warningTitle', - { - defaultMessage: 'Warning', - } -); - -export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.warningMessage', - { - defaultMessage: - 'The selected connector has been deleted. Either select a different connector or create a new one.', - } -); - -export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped', - { - defaultMessage: 'Not mapped', - } -); - -export const UPDATE_CONNECTOR = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.updateConnector', - { - defaultMessage: 'Update connector', - } -); - -export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { - return i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector', - { - values: { connectorName }, - defaultMessage: 'Update { connectorName }', - } - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts deleted file mode 100644 index 6cd2200e1dc74..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { ActionType } from '../../../../../types'; - -export { ActionType }; - -export interface ThirdPartyField { - label: string; - validSourceFields: string[]; - defaultSourceField: string; - defaultActionType: string; -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index f99a276305d75..21ae22ca728e3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -18,10 +18,13 @@ import { import { isEmpty } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; +import { CasesConfigurationMapping } from '../case_mappings/types'; +import { FieldMapping } from '../case_mappings/field_mapping'; +import { createDefaultMapping } from '../case_mappings/utils'; + import * as i18n from './translations'; -import { ServiceNowActionConnector, CasesConfigurationMapping } from './types'; +import { ServiceNowActionConnector } from './types'; import { connectorConfiguration } from './config'; -import { FieldMapping } from './case_mappings/field_mapping'; const ServiceNowConnectorFields: React.FC @@ -184,15 +187,5 @@ const ServiceNowConnectorFields: React.FC): CasesConfigurationMapping[] => - Object.keys(fields).map( - (key) => - ({ - source: fields[key].defaultSourceField, - target: key, - actionType: fields[key].defaultActionType, - } as CasesConfigurationMapping) - ); - // eslint-disable-next-line import/no-default-export export { ServiceNowConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts index 92252efc3a41c..2b02f67fd9179 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CasesConfigurationMapping } from '../case_mappings/types'; + export interface ServiceNowActionConnector { config: ServiceNowConfig; secrets: ServiceNowSecrets; @@ -37,10 +39,3 @@ interface ServiceNowSecrets { username: string; password: string; } - -// to remove -export interface CasesConfigurationMapping { - source: string; - target: string; - actionType: string; -} From e8c3a251b3dede02a5b8d654cc090de7c29af2f5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sat, 5 Sep 2020 00:49:21 +0300 Subject: [PATCH 65/67] Fix i18n --- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 10483b2c0d58f..2f2699bc4b49c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4446,7 +4446,6 @@ "xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", - "xpack.actions.builtin.configuration.apiWhitelistError": "コネクターアクションの構成エラー:{message}", "xpack.actions.builtin.email.errorSendingErrorMessage": "エラー送信メールアドレス", "xpack.actions.builtin.emailTitle": "メール", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cfc7885881cb3..c768fd120d827 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4447,7 +4447,6 @@ "xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", - "xpack.actions.builtin.configuration.apiWhitelistError": "配置连接器操作时出错:{message}", "xpack.actions.builtin.email.errorSendingErrorMessage": "发送电子邮件时出错", "xpack.actions.builtin.emailTitle": "电子邮件", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错", From caabc6ef2be9f063524fc4ae80da8a17c4a50e0b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 7 Sep 2020 13:10:22 +0300 Subject: [PATCH 66/67] Improve UX when editing an alert --- .../builtin_action_types/jira/api.test.ts | 2 +- .../builtin_action_types/jira/jira_params.tsx | 27 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index 1aa6b91a11f1c..4495c37f758ee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -360,7 +360,7 @@ describe('api', () => { }); describe('fieldsByIssueType', () => { - test('it returns the issue types correctly', async () => { + test('it returns the fields correctly', async () => { const res = await api.fieldsByIssueType({ externalService, params: { id: '10006' }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 25995eabb250d..bde3d67ffd65f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -33,13 +33,20 @@ const JiraParamsFields: React.FunctionComponent([]); + const [firstLoad, setFirstLoad] = useState(false); const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); const { http, toastNotifications } = useAppDependencies(); + + useEffect(() => { + setFirstLoad(true); + }, []); + const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({ http, toastNotifications, actionConnector, }); + const { isLoading: isLoadingFields, fields } = useGetFieldsByIssueType({ http, toastNotifications, @@ -90,15 +97,27 @@ const JiraParamsFields: React.FunctionComponent { + if (!firstLoad) { + return; + } + setIssueTypesSelectOptions([]); - editAction('subActionParams', { savedObjectId }, index); + editAction('subActionParams', { title, comments, description: '', savedObjectId }, index); // eslint-disable-next-line react-hooks/exhaustive-deps }, [actionConnector]); // Reset fields when changing connector or issue type useEffect(() => { + if (!firstLoad) { + return; + } + setPrioritiesSelectOptions([]); - editAction('subActionParams', { issueType, savedObjectId }, index); + editAction( + 'subActionParams', + { title, issueType, comments, description: '', savedObjectId }, + index + ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [issueType, savedObjectId]); @@ -121,7 +140,7 @@ const JiraParamsFields: React.FunctionComponent { - if (issueTypesSelectOptions.length > 0) { + if (!issueType && issueTypesSelectOptions.length > 0) { editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -129,7 +148,7 @@ const JiraParamsFields: React.FunctionComponent { - if (prioritiesSelectOptions.length > 0) { + if (!priority && prioritiesSelectOptions.length > 0) { editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); } // eslint-disable-next-line react-hooks/exhaustive-deps From 627a3f2eb75b5cd28a1f843fa00578db133bd6be Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 9 Sep 2020 13:48:53 +0300 Subject: [PATCH 67/67] Improve imports --- .../builtin_action_types/case_mappings/index.ts | 10 ++++++++++ .../builtin_action_types/jira/jira_connectors.tsx | 4 +--- .../components/builtin_action_types/jira/types.ts | 2 +- .../servicenow/servicenow_connectors.tsx | 4 +--- .../builtin_action_types/servicenow/types.ts | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts new file mode 100644 index 0000000000000..2de9b87ead3fe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './types'; +export * from './field_mapping'; +export * from './field_mapping_row'; +export * from './utils'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index 1fc1853a514c0..2ab9843c143b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -16,9 +16,7 @@ import { import { isEmpty } from 'lodash'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesConfigurationMapping } from '../case_mappings/types'; -import { FieldMapping } from '../case_mappings/field_mapping'; -import { createDefaultMapping } from '../case_mappings/utils'; +import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings'; import * as i18n from './translations'; import { JiraActionConnector } from './types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts index b18ffdd45ba92..ff11199f35fea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../case_mappings/types'; +import { CasesConfigurationMapping } from '../case_mappings'; export interface JiraActionConnector { config: JiraConfig; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 21ae22ca728e3..a8f1ed8d55447 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -18,9 +18,7 @@ import { import { isEmpty } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { CasesConfigurationMapping } from '../case_mappings/types'; -import { FieldMapping } from '../case_mappings/field_mapping'; -import { createDefaultMapping } from '../case_mappings/utils'; +import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings'; import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts index 2b02f67fd9179..a4f1ff2be0f69 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../case_mappings/types'; +import { CasesConfigurationMapping } from '../case_mappings'; export interface ServiceNowActionConnector { config: ServiceNowConfig;