mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solutions][Case] Settings per case per connector (#77327)
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cc7ac29622
commit
287541891e
154 changed files with 8636 additions and 983 deletions
|
@ -74,6 +74,10 @@ describe('api', () => {
|
|||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description:
|
||||
'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
|
@ -233,6 +237,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description:
|
||||
'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
|
@ -443,6 +451,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description:
|
||||
'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
|
@ -480,6 +492,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description:
|
||||
'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
|
@ -516,6 +532,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary:
|
||||
'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description:
|
||||
|
@ -553,7 +573,12 @@ describe('api', () => {
|
|||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {},
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -587,6 +612,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
|
@ -622,6 +651,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description:
|
||||
'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
|
@ -659,6 +692,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
description:
|
||||
'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
|
@ -695,6 +732,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary:
|
||||
'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
description:
|
||||
|
@ -733,6 +774,10 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
labels: ['kibana', 'elastic'],
|
||||
priority: 'High',
|
||||
issueType: '10006',
|
||||
parent: null,
|
||||
summary:
|
||||
'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)',
|
||||
},
|
||||
|
|
|
@ -91,11 +91,25 @@ const pushToServiceHandler = async ({
|
|||
defaultPipes,
|
||||
});
|
||||
|
||||
incident = transformFields<PushToServiceApiParams, ExternalServiceParams, Incident>({
|
||||
const transformedFields = transformFields<
|
||||
PushToServiceApiParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
const { priority, labels, issueType, parent } = params;
|
||||
incident = {
|
||||
summary: transformedFields.summary,
|
||||
description: transformedFields.description,
|
||||
priority,
|
||||
labels,
|
||||
issueType,
|
||||
parent,
|
||||
};
|
||||
} else {
|
||||
const { title, description, priority, labels, issueType, parent } = params;
|
||||
incident = { summary: title, description, priority, labels, issueType, parent };
|
||||
|
|
|
@ -99,7 +99,21 @@ export const createExternalService = (
|
|||
return fields;
|
||||
};
|
||||
|
||||
const createErrorMessage = (errors: ResponseError) => {
|
||||
const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => {
|
||||
if (errorResponse == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { errorMessages, errors } = errorResponse;
|
||||
|
||||
if (errors == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
|
||||
return `${errorMessages.join(', ')}`;
|
||||
}
|
||||
|
||||
return Object.entries(errors).reduce((errorMessage, [, value]) => {
|
||||
const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value;
|
||||
return msg;
|
||||
|
@ -154,7 +168,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)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -207,7 +221,7 @@ export const createExternalService = (
|
|||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data?.errors ?? {}
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
@ -249,7 +263,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)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -280,7 +294,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)}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -302,7 +316,7 @@ export const createExternalService = (
|
|||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data?.errors ?? {}
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
@ -342,7 +356,7 @@ export const createExternalService = (
|
|||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data?.errors ?? {}
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
@ -388,7 +402,7 @@ export const createExternalService = (
|
|||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data?.errors ?? {}
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
@ -415,7 +429,7 @@ export const createExternalService = (
|
|||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to get issues. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data?.errors ?? {}
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
@ -439,7 +453,7 @@ export const createExternalService = (
|
|||
getErrorMessage(
|
||||
i18n.NAME,
|
||||
`Unable to get issue with id ${id}. Error: ${error.message}. Reason: ${createErrorMessage(
|
||||
error.response?.data?.errors ?? {}
|
||||
error.response?.data
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
|
|
@ -199,5 +199,6 @@ export interface Fields {
|
|||
[key: string]: string | string[] | { name: string } | { key: string } | { id: string };
|
||||
}
|
||||
export interface ResponseError {
|
||||
[k: string]: string;
|
||||
errorMessages: string[] | null | undefined;
|
||||
errors: { [k: string]: string } | null | undefined;
|
||||
}
|
||||
|
|
|
@ -74,6 +74,8 @@ describe('api', () => {
|
|||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'Incident description (created at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
name: 'Incident title (created at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
|
@ -175,6 +177,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
|
@ -298,6 +302,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
|
@ -335,6 +341,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
|
@ -371,6 +379,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name:
|
||||
'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
|
@ -408,7 +418,10 @@ describe('api', () => {
|
|||
});
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {},
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -442,6 +455,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
});
|
||||
|
@ -477,6 +492,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
|
@ -514,6 +531,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
description:
|
||||
'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
|
@ -550,6 +569,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name:
|
||||
'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
description:
|
||||
|
@ -588,6 +609,8 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
incidentTypes: [1001],
|
||||
severityCode: 6,
|
||||
name:
|
||||
'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
|
||||
},
|
||||
|
|
|
@ -73,11 +73,23 @@ const pushToServiceHandler = async ({
|
|||
defaultPipes,
|
||||
});
|
||||
|
||||
incident = transformFields<PushToServiceApiParams, ExternalServiceParams, Incident>({
|
||||
const transformedFields = transformFields<
|
||||
PushToServiceApiParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
const { incidentTypes, severityCode } = params;
|
||||
incident = {
|
||||
name: transformedFields.name,
|
||||
description: transformedFields.description,
|
||||
incidentTypes,
|
||||
severityCode,
|
||||
};
|
||||
} else {
|
||||
const { title, description, incidentTypes, severityCode } = params;
|
||||
incident = { name: title, description, incidentTypes, severityCode };
|
||||
|
|
|
@ -76,12 +76,16 @@ describe('api', () => {
|
|||
externalService,
|
||||
mapping,
|
||||
params,
|
||||
secrets: {},
|
||||
secrets: { username: 'elastic', password: 'elastic' },
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(externalService.createIncident).toHaveBeenCalledWith({
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
caller_id: 'elastic',
|
||||
description:
|
||||
'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description:
|
||||
|
@ -103,6 +107,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledTimes(2);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
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)',
|
||||
|
@ -114,6 +121,9 @@ describe('api', () => {
|
|||
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
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)',
|
||||
|
@ -184,6 +194,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description:
|
||||
'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description:
|
||||
|
@ -205,6 +218,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledTimes(3);
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description:
|
||||
'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
short_description:
|
||||
|
@ -215,6 +231,9 @@ describe('api', () => {
|
|||
|
||||
expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, {
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
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)',
|
||||
|
@ -258,6 +277,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description:
|
||||
|
@ -297,6 +319,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description:
|
||||
'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
|
@ -334,6 +359,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description:
|
||||
|
@ -370,9 +398,14 @@ describe('api', () => {
|
|||
secrets: {},
|
||||
logger: mockedLogger,
|
||||
});
|
||||
|
||||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {},
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -407,6 +440,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
|
@ -444,6 +480,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description:
|
||||
|
@ -483,6 +522,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
description:
|
||||
'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
|
@ -520,6 +562,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
description:
|
||||
|
@ -559,6 +604,9 @@ describe('api', () => {
|
|||
expect(externalService.updateIncident).toHaveBeenCalledWith({
|
||||
incidentId: 'incident-3',
|
||||
incident: {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
short_description:
|
||||
'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)',
|
||||
},
|
||||
|
|
|
@ -60,11 +60,23 @@ const pushToServiceHandler = async ({
|
|||
defaultPipes,
|
||||
});
|
||||
|
||||
incident = transformFields<PushToServiceApiParams, ExternalServiceParams, Incident>({
|
||||
const transformedFields = transformFields<
|
||||
PushToServiceApiParams,
|
||||
ExternalServiceParams,
|
||||
Incident
|
||||
>({
|
||||
params,
|
||||
fields,
|
||||
currentIncident,
|
||||
});
|
||||
|
||||
incident = {
|
||||
severity: params.severity,
|
||||
urgency: params.urgency,
|
||||
impact: params.impact,
|
||||
short_description: transformedFields.short_description,
|
||||
description: transformedFields.description,
|
||||
};
|
||||
} else {
|
||||
incident = { ...params, short_description: params.title, comments: params.comment };
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ const executorParams: ExecutorSubActionPushParams = {
|
|||
comment: 'test-alert comment',
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '1',
|
||||
impact: '3',
|
||||
comments: [
|
||||
{
|
||||
commentId: 'case-comment-1',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { NumberFromString } from '../saved_object';
|
|||
import { UserRT } from '../user';
|
||||
import { CommentResponseRt } from './comment';
|
||||
import { CasesStatusResponseRt } from './status';
|
||||
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
export { ActionTypeExecutorResult } from '../../../../actions/server/types';
|
||||
|
@ -17,7 +18,7 @@ export { ActionTypeExecutorResult } from '../../../../actions/server/types';
|
|||
const StatusRt = rt.union([rt.literal('open'), rt.literal('closed')]);
|
||||
|
||||
const CaseBasicRt = rt.type({
|
||||
connector_id: rt.string,
|
||||
connector: CaseConnectorRt,
|
||||
description: rt.string,
|
||||
status: StatusRt,
|
||||
tags: rt.array(rt.string),
|
||||
|
@ -60,6 +61,7 @@ export const CasePostRequestRt = rt.type({
|
|||
description: rt.string,
|
||||
tags: rt.array(rt.string),
|
||||
title: rt.string,
|
||||
connector: CaseConnectorRt,
|
||||
});
|
||||
|
||||
export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt;
|
||||
|
@ -115,6 +117,8 @@ export const CasesResponseRt = rt.array(CaseResponseRt);
|
|||
* so we redefine then so we can use/validate types
|
||||
*/
|
||||
|
||||
// TODO: Refactor to support multiple connectors with various fields
|
||||
|
||||
const ServiceConnectorUserParams = rt.type({
|
||||
fullName: rt.union([rt.string, rt.null]),
|
||||
username: rt.string,
|
||||
|
@ -130,15 +134,15 @@ export const ServiceConnectorCommentParamsRt = rt.type({
|
|||
});
|
||||
|
||||
export const ServiceConnectorCaseParamsRt = rt.type({
|
||||
savedObjectId: rt.string,
|
||||
comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]),
|
||||
createdAt: rt.string,
|
||||
createdBy: ServiceConnectorUserParams,
|
||||
description: rt.union([rt.string, rt.null]),
|
||||
externalId: rt.union([rt.string, rt.null]),
|
||||
savedObjectId: rt.string,
|
||||
title: rt.string,
|
||||
updatedAt: rt.union([rt.string, rt.null]),
|
||||
updatedBy: rt.union([ServiceConnectorUserParams, rt.null]),
|
||||
description: rt.union([rt.string, rt.null]),
|
||||
comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]),
|
||||
});
|
||||
|
||||
export const ServiceConnectorCaseResponseRt = rt.intersection([
|
||||
|
@ -174,3 +178,8 @@ export type ServiceConnectorCaseParams = rt.TypeOf<typeof ServiceConnectorCasePa
|
|||
export type ServiceConnectorCaseResponse = rt.TypeOf<typeof ServiceConnectorCaseResponseRt>;
|
||||
export type CaseFullExternalService = rt.TypeOf<typeof CaseFullExternalServiceRt>;
|
||||
export type ServiceConnectorCommentParams = rt.TypeOf<typeof ServiceConnectorCommentParamsRt>;
|
||||
|
||||
export type ESCaseAttributes = Omit<CaseAttributes, 'connector'> & { connector: ESCaseConnector };
|
||||
export type ESCasePatchRequest = Omit<CasePatchRequest, 'connector'> & {
|
||||
connector?: ESCaseConnector;
|
||||
};
|
||||
|
|
|
@ -8,9 +8,10 @@ import * as rt from 'io-ts';
|
|||
|
||||
import { ActionResult } from '../../../../actions/common';
|
||||
import { UserRT } from '../user';
|
||||
import { JiraFieldsRT } from '../connectors/jira';
|
||||
import { ServiceNowFieldsRT } from '../connectors/servicenow';
|
||||
import { ResilientFieldsRT } from '../connectors/resilient';
|
||||
import { JiraCaseFieldsRt } from '../connectors/jira';
|
||||
import { ServiceNowCaseFieldsRT } from '../connectors/servicenow';
|
||||
import { ResilientCaseFieldsRT } from '../connectors/resilient';
|
||||
import { CaseConnectorRt, ESCaseConnector } from '../connectors';
|
||||
|
||||
/*
|
||||
* This types below are related to the service now configuration
|
||||
|
@ -31,9 +32,9 @@ const CaseFieldRT = rt.union([
|
|||
]);
|
||||
|
||||
const ThirdPartyFieldRT = rt.union([
|
||||
JiraFieldsRT,
|
||||
ServiceNowFieldsRT,
|
||||
ResilientFieldsRT,
|
||||
JiraCaseFieldsRt,
|
||||
ServiceNowCaseFieldsRT,
|
||||
ResilientCaseFieldsRT,
|
||||
rt.literal('not_mapped'),
|
||||
]);
|
||||
|
||||
|
@ -62,14 +63,13 @@ export type CasesConnectorConfiguration = rt.TypeOf<typeof CasesConnectorConfigu
|
|||
|
||||
/** ********************************************************************** */
|
||||
|
||||
export type Connector = ActionResult;
|
||||
export type ActionConnector = ActionResult;
|
||||
|
||||
// TO DO we will need to add this type rt.literal('close-by-thrid-party')
|
||||
// TODO: we will need to add this type rt.literal('close-by-third-party')
|
||||
const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]);
|
||||
|
||||
const CasesConfigureBasicRt = rt.type({
|
||||
connector_id: rt.string,
|
||||
connector_name: rt.string,
|
||||
connector: CaseConnectorRt,
|
||||
closure_type: ClosureTypeRT,
|
||||
});
|
||||
|
||||
|
@ -97,8 +97,12 @@ export const CaseConfigureResponseRt = rt.intersection([
|
|||
]);
|
||||
|
||||
export type ClosureType = rt.TypeOf<typeof ClosureTypeRT>;
|
||||
|
||||
export type CasesConfigure = rt.TypeOf<typeof CasesConfigureBasicRt>;
|
||||
export type CasesConfigureRequest = rt.TypeOf<typeof CasesConfigureRequestRt>;
|
||||
export type CasesConfigurePatch = rt.TypeOf<typeof CasesConfigurePatchRt>;
|
||||
export type CasesConfigureAttributes = rt.TypeOf<typeof CaseConfigureAttributesRt>;
|
||||
export type CasesConfigureResponse = rt.TypeOf<typeof CaseConfigureResponseRt>;
|
||||
|
||||
export type ESCasesConfigureAttributes = Omit<CasesConfigureAttributes, 'connector'> & {
|
||||
connector: ESCaseConnector;
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { UserRT } from '../user';
|
|||
const UserActionFieldRt = rt.array(
|
||||
rt.union([
|
||||
rt.literal('comment'),
|
||||
rt.literal('connector_id'),
|
||||
rt.literal('connector'),
|
||||
rt.literal('description'),
|
||||
rt.literal('pushed'),
|
||||
rt.literal('tags'),
|
||||
|
|
|
@ -3,7 +3,80 @@
|
|||
* 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 rt from 'io-ts';
|
||||
|
||||
import { JiraFieldsRT } from './jira';
|
||||
import { ResilientFieldsRT } from './resilient';
|
||||
import { ServiceNowFieldsRT } from './servicenow';
|
||||
|
||||
export * from './jira';
|
||||
export * from './servicenow';
|
||||
export * from './resilient';
|
||||
|
||||
export const ConnectorFieldsRt = rt.union([
|
||||
JiraFieldsRT,
|
||||
ResilientFieldsRT,
|
||||
ServiceNowFieldsRT,
|
||||
rt.null,
|
||||
]);
|
||||
|
||||
export enum ConnectorTypes {
|
||||
jira = '.jira',
|
||||
resilient = '.resilient',
|
||||
servicenow = '.servicenow',
|
||||
none = '.none',
|
||||
}
|
||||
|
||||
const ConnectorJiraTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.jira),
|
||||
fields: rt.union([JiraFieldsRT, rt.null]),
|
||||
});
|
||||
|
||||
const ConnectorResillientTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.resilient),
|
||||
fields: rt.union([ResilientFieldsRT, rt.null]),
|
||||
});
|
||||
|
||||
const ConnectorServiceNowTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.servicenow),
|
||||
fields: rt.union([ServiceNowFieldsRT, rt.null]),
|
||||
});
|
||||
|
||||
const ConnectorNoneTypeFieldsRt = rt.type({
|
||||
type: rt.literal(ConnectorTypes.none),
|
||||
fields: rt.null,
|
||||
});
|
||||
|
||||
export const ConnectorTypeFieldsRt = rt.union([
|
||||
ConnectorJiraTypeFieldsRt,
|
||||
ConnectorResillientTypeFieldsRt,
|
||||
ConnectorServiceNowTypeFieldsRt,
|
||||
ConnectorNoneTypeFieldsRt,
|
||||
]);
|
||||
|
||||
export const CaseConnectorRt = rt.intersection([
|
||||
rt.type({
|
||||
id: rt.string,
|
||||
name: rt.string,
|
||||
}),
|
||||
ConnectorTypeFieldsRt,
|
||||
]);
|
||||
|
||||
export type CaseConnector = rt.TypeOf<typeof CaseConnectorRt>;
|
||||
export type ConnectorTypeFields = rt.TypeOf<typeof ConnectorTypeFieldsRt>;
|
||||
|
||||
// we need to change these types back and forth for storing in ES (arrays overwrite, objects merge)
|
||||
export type ConnectorFields = rt.TypeOf<typeof ConnectorFieldsRt>;
|
||||
|
||||
export type ESConnectorFields = Array<{
|
||||
key: string;
|
||||
value: unknown;
|
||||
}>;
|
||||
|
||||
export type ESCaseConnectorTypes = ConnectorTypes;
|
||||
export interface ESCaseConnector {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ESCaseConnectorTypes;
|
||||
fields: ESConnectorFields | null;
|
||||
}
|
||||
|
|
|
@ -6,10 +6,16 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const JiraFieldsRT = rt.union([
|
||||
export const JiraCaseFieldsRt = rt.union([
|
||||
rt.literal('summary'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
export const JiraFieldsRT = rt.type({
|
||||
issueType: rt.union([rt.string, rt.null]),
|
||||
priority: rt.union([rt.string, rt.null]),
|
||||
parent: rt.union([rt.string, rt.null]),
|
||||
});
|
||||
|
||||
export type JiraFieldsType = rt.TypeOf<typeof JiraFieldsRT>;
|
||||
|
|
|
@ -6,10 +6,15 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const ResilientFieldsRT = rt.union([
|
||||
export const ResilientCaseFieldsRT = rt.union([
|
||||
rt.literal('name'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
export const ResilientFieldsRT = rt.type({
|
||||
incidentTypes: rt.union([rt.array(rt.string), rt.null]),
|
||||
severityCode: rt.union([rt.string, rt.null]),
|
||||
});
|
||||
|
||||
export type ResilientFieldsType = rt.TypeOf<typeof ResilientFieldsRT>;
|
||||
|
|
|
@ -6,10 +6,16 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
export const ServiceNowFieldsRT = rt.union([
|
||||
export const ServiceNowCaseFieldsRT = rt.union([
|
||||
rt.literal('short_description'),
|
||||
rt.literal('description'),
|
||||
rt.literal('comments'),
|
||||
]);
|
||||
|
||||
export const ServiceNowFieldsRT = rt.type({
|
||||
impact: rt.union([rt.string, rt.null]),
|
||||
severity: rt.union([rt.string, rt.null]),
|
||||
urgency: rt.union([rt.string, rt.null]),
|
||||
});
|
||||
|
||||
export type ServiceNowFieldsType = rt.TypeOf<typeof ServiceNowFieldsRT>;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
export * from './cases';
|
||||
export * from './connectors';
|
||||
export * from './runtime_types';
|
||||
export * from './saved_object';
|
||||
export * from './user';
|
||||
|
|
|
@ -127,7 +127,7 @@ export const createMockSavedObjectsRepository = ({
|
|||
|
||||
if (
|
||||
type === CASE_CONFIGURE_SAVED_OBJECT &&
|
||||
attributes.connector_id === 'throw-error-create'
|
||||
attributes.connector.id === 'throw-error-create'
|
||||
) {
|
||||
throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing');
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ export const createMockSavedObjectsRepository = ({
|
|||
id: 'mock-configuration',
|
||||
attributes,
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
version: attributes.connector_id === 'no-version' ? undefined : 'WzksMV0=',
|
||||
version: attributes.connector.id === 'no-version' ? undefined : 'WzksMV0=',
|
||||
};
|
||||
|
||||
caseConfigureSavedObject = [newConfiguration];
|
||||
|
@ -194,7 +194,7 @@ export const createMockSavedObjectsRepository = ({
|
|||
type,
|
||||
updated_at: '2019-11-22T22:50:55.191Z',
|
||||
attributes,
|
||||
version: attributes.connector_id === 'no-version' ? undefined : 'WzE3LDFd',
|
||||
version: attributes.connector?.id === 'no-version' ? undefined : 'WzE3LDFd',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { mockCases, mockCasesErrorTriggerData, mockCaseComments } from './mock_saved_objects';
|
||||
export * from './mock_saved_objects';
|
||||
export { createMockSavedObjectsRepository } from './create_mock_so_repository';
|
||||
export { createRouteContext } from './route_contexts';
|
||||
export { authenticationMock } from './authc_mock';
|
||||
|
|
|
@ -6,19 +6,25 @@
|
|||
|
||||
import { SavedObject } from 'kibana/server';
|
||||
import {
|
||||
CaseAttributes,
|
||||
ESCasesConfigureAttributes,
|
||||
CommentAttributes,
|
||||
CasesConfigureAttributes,
|
||||
ESCaseAttributes,
|
||||
ConnectorTypes,
|
||||
} from '../../../../common/api';
|
||||
|
||||
export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
||||
export const mockCases: Array<SavedObject<ESCaseAttributes>> = [
|
||||
{
|
||||
type: 'cases',
|
||||
id: 'mock-id-1',
|
||||
attributes: {
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: [],
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -47,7 +53,12 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
attributes: {
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: [],
|
||||
},
|
||||
created_at: '2019-11-25T22:32:00.900Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -76,7 +87,16 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
attributes: {
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: '123',
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
},
|
||||
created_at: '2019-11-25T22:32:17.947Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -109,7 +129,16 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
connector_id: '123',
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
},
|
||||
created_at: '2019-11-25T22:32:17.947Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -134,7 +163,7 @@ export const mockCases: Array<SavedObject<CaseAttributes>> = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockCaseNoConnectorId: SavedObject<Partial<CaseAttributes>> = {
|
||||
export const mockCaseNoConnectorId: SavedObject<Partial<ESCaseAttributes>> = {
|
||||
type: 'cases',
|
||||
id: 'mock-no-connector_id',
|
||||
attributes: {
|
||||
|
@ -266,13 +295,17 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockCaseConfigure: Array<SavedObject<CasesConfigureAttributes>> = [
|
||||
export const mockCaseConfigure: Array<SavedObject<ESCasesConfigureAttributes>> = [
|
||||
{
|
||||
type: 'cases-configure',
|
||||
id: 'mock-configuration-1',
|
||||
attributes: {
|
||||
connector_id: '123',
|
||||
connector_name: 'My connector',
|
||||
connector: {
|
||||
id: '789',
|
||||
name: 'My connector 3',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-user',
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: {
|
||||
|
|
|
@ -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 { CasePostRequest, CasesConfigureRequest } from '../../../../common/api';
|
||||
import { CasePostRequest, CasesConfigureRequest, ConnectorTypes } from '../../../../common/api';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { FindActionResult } from '../../../../../actions/server/types';
|
||||
|
||||
|
@ -11,6 +11,12 @@ export const newCase: CasePostRequest = {
|
|||
title: 'My new case',
|
||||
description: 'A description',
|
||||
tags: ['new', 'case'],
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const getActions = (): FindActionResult[] => [
|
||||
|
@ -59,7 +65,11 @@ export const getActions = (): FindActionResult[] => [
|
|||
];
|
||||
|
||||
export const newConfiguration: CasesConfigureRequest = {
|
||||
connector_id: '456',
|
||||
connector_name: 'My connector 2',
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ import { buildCommentUserActionItem } from '../../../../services/user_actions/he
|
|||
import { RouteDeps } from '../../types';
|
||||
import { escapeHatch, wrapError, flattenCaseSavedObject } from '../../utils';
|
||||
import { CASE_COMMENTS_URL } from '../../../../../common/constants';
|
||||
import { getConnectorId } from '../helpers';
|
||||
|
||||
export function initPatchCommentApi({
|
||||
caseConfigureService,
|
||||
|
@ -71,7 +70,7 @@ export function initPatchCommentApi({
|
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { username, full_name, email } = await caseService.getUser({ request, response });
|
||||
const updatedDate = new Date().toISOString();
|
||||
const [updatedComment, updatedCase, myCaseConfigure] = await Promise.all([
|
||||
const [updatedComment, updatedCase] = await Promise.all([
|
||||
caseService.patchComment({
|
||||
client,
|
||||
commentId: query.id,
|
||||
|
@ -91,7 +90,6 @@ export function initPatchCommentApi({
|
|||
},
|
||||
version: myCase.version,
|
||||
}),
|
||||
caseConfigureService.find({ client }),
|
||||
]);
|
||||
|
||||
const totalCommentsFindByCases = await caseService.getAllCaseComments({
|
||||
|
@ -103,7 +101,7 @@ export function initPatchCommentApi({
|
|||
perPage: 1,
|
||||
},
|
||||
});
|
||||
const caseConfigureConnectorId = getConnectorId(myCaseConfigure);
|
||||
|
||||
const [comments] = await Promise.all([
|
||||
caseService.getAllCaseComments({
|
||||
client,
|
||||
|
@ -142,7 +140,6 @@ export function initPatchCommentApi({
|
|||
references: myCase.references,
|
||||
},
|
||||
comments: comments.saved_objects,
|
||||
caseConfigureConnectorId,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ import { buildCommentUserActionItem } from '../../../../services/user_actions/he
|
|||
import { escapeHatch, transformNewComment, wrapError, flattenCaseSavedObject } from '../../utils';
|
||||
import { RouteDeps } from '../../types';
|
||||
import { CASE_COMMENTS_URL } from '../../../../../common/constants';
|
||||
import { getConnectorId } from '../helpers';
|
||||
|
||||
export function initPostCommentApi({
|
||||
caseConfigureService,
|
||||
|
@ -52,7 +51,7 @@ export function initPostCommentApi({
|
|||
const { username, full_name, email } = await caseService.getUser({ request, response });
|
||||
const createdDate = new Date().toISOString();
|
||||
|
||||
const [newComment, updatedCase, myCaseConfigure] = await Promise.all([
|
||||
const [newComment, updatedCase] = await Promise.all([
|
||||
caseService.postNewComment({
|
||||
client,
|
||||
attributes: transformNewComment({
|
||||
|
@ -79,10 +78,8 @@ export function initPostCommentApi({
|
|||
},
|
||||
version: myCase.version,
|
||||
}),
|
||||
caseConfigureService.find({ client }),
|
||||
]);
|
||||
|
||||
const caseConfigureConnectorId = getConnectorId(myCaseConfigure);
|
||||
const totalCommentsFindByCases = await caseService.getAllCaseComments({
|
||||
client,
|
||||
caseId,
|
||||
|
@ -130,7 +127,6 @@ export function initPostCommentApi({
|
|||
references: myCase.references,
|
||||
},
|
||||
comments: comments.saved_objects,
|
||||
caseConfigureConnectorId,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -58,8 +58,12 @@ describe('GET configuration', () => {
|
|||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.payload).toEqual({
|
||||
connector_id: '123',
|
||||
connector_name: 'My connector',
|
||||
connector: {
|
||||
id: '789',
|
||||
name: 'My connector 3',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-user',
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: {
|
||||
|
@ -91,6 +95,7 @@ describe('GET configuration', () => {
|
|||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
expect(res.payload).toEqual({});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { CaseConfigureResponseRt } from '../../../../../common/api';
|
|||
import { RouteDeps } from '../../types';
|
||||
import { wrapError } from '../../utils';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
import { transformESConnectorToCaseConnector } from '../helpers';
|
||||
|
||||
export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps) {
|
||||
router.get(
|
||||
|
@ -21,11 +22,15 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps
|
|||
|
||||
const myCaseConfigure = await caseConfigureService.find({ client });
|
||||
|
||||
const { connector, ...caseConfigureWithoutConnector } = myCaseConfigure.saved_objects[0]
|
||||
?.attributes ?? { connector: null };
|
||||
|
||||
return response.ok({
|
||||
body:
|
||||
myCaseConfigure.saved_objects.length > 0
|
||||
? CaseConfigureResponseRt.encode({
|
||||
...myCaseConfigure.saved_objects[0].attributes,
|
||||
...caseConfigureWithoutConnector,
|
||||
connector: transformESConnectorToCaseConnector(connector),
|
||||
version: myCaseConfigure.saved_objects[0].version ?? '',
|
||||
})
|
||||
: {},
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects';
|
||||
import { initPatchCaseConfigure } from './patch_configure';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
import { ConnectorTypes } from '../../../../../common/api/connectors';
|
||||
|
||||
describe('PATCH configuration', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -50,6 +51,7 @@ describe('PATCH configuration', () => {
|
|||
expect(res.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
...mockCaseConfigure[0].attributes,
|
||||
connector: { fields: null, id: '789', name: 'My connector 3', type: '.jira' },
|
||||
closure_type: 'close-by-pushing',
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
|
||||
|
@ -82,6 +84,7 @@ describe('PATCH configuration', () => {
|
|||
expect(res.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
...mockCaseConfigure[0].attributes,
|
||||
connector: { fields: null, id: '789', name: 'My connector 3', type: '.jira' },
|
||||
closure_type: 'close-by-pushing',
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: { email: null, full_name: null, username: null },
|
||||
|
@ -90,6 +93,44 @@ describe('PATCH configuration', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('patch configuration - connector', async () => {
|
||||
routeHandler = await createRoute(initPatchCaseConfigure, 'patch');
|
||||
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'patch',
|
||||
body: {
|
||||
connector: {
|
||||
id: 'connector-new',
|
||||
name: 'New connector',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
version: mockCaseConfigure[0].version,
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
...mockCaseConfigure[0].attributes,
|
||||
connector: { id: 'connector-new', name: 'New connector', type: '.jira', fields: null },
|
||||
closure_type: 'close-by-user',
|
||||
updated_at: '2020-04-09T09:43:51.778Z',
|
||||
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
|
||||
version: 'WzE3LDFd',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throw error when configuration have not being created', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
|
@ -138,7 +179,15 @@ describe('PATCH configuration', () => {
|
|||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'patch',
|
||||
body: { connector_id: 'no-version', version: mockCaseConfigure[0].version },
|
||||
body: {
|
||||
connector: {
|
||||
id: 'no-version',
|
||||
name: 'no version',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
version: mockCaseConfigure[0].version,
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
import { RouteDeps } from '../../types';
|
||||
import { wrapError, escapeHatch } from '../../utils';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
transformCaseConnectorToEsConnector,
|
||||
transformESConnectorToCaseConnector,
|
||||
} from '../helpers';
|
||||
|
||||
export function initPatchCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) {
|
||||
router.patch(
|
||||
|
@ -35,8 +39,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
|
|||
);
|
||||
|
||||
const myCaseConfigure = await caseConfigureService.find({ client });
|
||||
const { version, ...queryWithoutVersion } = query;
|
||||
|
||||
const { version, connector, ...queryWithoutVersion } = query;
|
||||
if (myCaseConfigure.saved_objects.length === 0) {
|
||||
throw Boom.conflict(
|
||||
'You can not patch this configuration since you did not created first with a post.'
|
||||
|
@ -58,6 +61,9 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
|
|||
caseConfigureId: myCaseConfigure.saved_objects[0].id,
|
||||
updatedAttributes: {
|
||||
...queryWithoutVersion,
|
||||
...(connector != null
|
||||
? { connector: transformCaseConnectorToEsConnector(connector) }
|
||||
: {}),
|
||||
updated_at: updateDate,
|
||||
updated_by: { email, full_name, username },
|
||||
},
|
||||
|
@ -67,6 +73,9 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout
|
|||
body: CaseConfigureResponseRt.encode({
|
||||
...myCaseConfigure.saved_objects[0].attributes,
|
||||
...patch.attributes,
|
||||
connector: transformESConnectorToCaseConnector(
|
||||
patch.attributes.connector ?? myCaseConfigure.saved_objects[0].attributes.connector
|
||||
),
|
||||
version: patch.version ?? '',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects';
|
|||
import { initPostCaseConfigure } from './post_configure';
|
||||
import { newConfiguration } from '../../__mocks__/request_responses';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
import { ConnectorTypes } from '../../../../../common/api/connectors';
|
||||
|
||||
describe('POST configuration', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -47,8 +48,12 @@ describe('POST configuration', () => {
|
|||
expect(res.status).toEqual(200);
|
||||
expect(res.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
connector_id: '456',
|
||||
connector_name: 'My connector 2',
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
|
||||
|
@ -78,8 +83,12 @@ describe('POST configuration', () => {
|
|||
expect(res.status).toEqual(200);
|
||||
expect(res.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
connector_id: '456',
|
||||
connector_name: 'My connector 2',
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: null, full_name: null, username: null },
|
||||
|
@ -89,12 +98,16 @@ describe('POST configuration', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('throws when missing connector_id', async () => {
|
||||
it('throws when missing connector.id', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
connector_name: 'My connector 2',
|
||||
connector: {
|
||||
name: 'My connector 2',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
},
|
||||
});
|
||||
|
@ -110,12 +123,66 @@ describe('POST configuration', () => {
|
|||
expect(res.payload.isBoom).toEqual(true);
|
||||
});
|
||||
|
||||
it('throws when missing connector_name', async () => {
|
||||
it('throws when missing connector.name', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
connector_id: '456',
|
||||
connector: {
|
||||
id: '456',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(400);
|
||||
expect(res.payload.isBoom).toEqual(true);
|
||||
});
|
||||
|
||||
it('throws when missing connector.type', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(400);
|
||||
expect(res.payload.isBoom).toEqual(true);
|
||||
});
|
||||
|
||||
it('throws when missing connector.fields', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: ConnectorTypes.none,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
},
|
||||
});
|
||||
|
@ -136,8 +203,12 @@ describe('POST configuration', () => {
|
|||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
connector_id: '456',
|
||||
connector_name: 'My connector 2',
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: '.jira',
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -254,8 +325,11 @@ describe('POST configuration', () => {
|
|||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
connector_id: 'throw-error-create',
|
||||
connector_name: 'My connector 2',
|
||||
connector: {
|
||||
id: 'throw-error-create',
|
||||
name: 'My connector 2',
|
||||
fields: null,
|
||||
},
|
||||
closure_type: 'close-by-pushing',
|
||||
},
|
||||
});
|
||||
|
@ -275,7 +349,15 @@ describe('POST configuration', () => {
|
|||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: { ...newConfiguration, connector_id: 'no-version' },
|
||||
body: {
|
||||
...newConfiguration,
|
||||
connector: {
|
||||
id: 'no-version',
|
||||
name: 'no version',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
|
@ -292,4 +374,46 @@ describe('POST configuration', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an error if fields are not null', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
...newConfiguration,
|
||||
connector: { id: 'not-null', name: 'not-null', type: ConnectorTypes.none, fields: {} },
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(400);
|
||||
expect(res.payload.isBoom).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns an error if the type of the connector does not exists', async () => {
|
||||
const req = httpServerMock.createKibanaRequest({
|
||||
path: CASE_CONFIGURE_URL,
|
||||
method: 'post',
|
||||
body: {
|
||||
...newConfiguration,
|
||||
connector: { id: 'not-exists', name: 'not-exist', type: '.not-exists', fields: null },
|
||||
},
|
||||
});
|
||||
|
||||
const context = createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseConfigureSavedObject: mockCaseConfigure,
|
||||
})
|
||||
);
|
||||
|
||||
const res = await routeHandler(context, req, kibanaResponseFactory);
|
||||
expect(res.status).toEqual(400);
|
||||
expect(res.payload.isBoom).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
import { RouteDeps } from '../../types';
|
||||
import { wrapError, escapeHatch } from '../../utils';
|
||||
import { CASE_CONFIGURE_URL } from '../../../../../common/constants';
|
||||
import {
|
||||
transformCaseConnectorToEsConnector,
|
||||
transformESConnectorToCaseConnector,
|
||||
} from '../helpers';
|
||||
|
||||
export function initPostCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) {
|
||||
router.post(
|
||||
|
@ -51,6 +55,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
|
|||
client,
|
||||
attributes: {
|
||||
...query,
|
||||
connector: transformCaseConnectorToEsConnector(query.connector),
|
||||
created_at: creationDate,
|
||||
created_by: { email, full_name, username },
|
||||
updated_at: null,
|
||||
|
@ -59,7 +64,12 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route
|
|||
});
|
||||
|
||||
return response.ok({
|
||||
body: CaseConfigureResponseRt.encode({ ...post.attributes, version: post.version ?? '' }),
|
||||
body: CaseConfigureResponseRt.encode({
|
||||
...post.attributes,
|
||||
// Reserve for future implementations
|
||||
connector: transformESConnectorToCaseConnector(post.attributes.connector),
|
||||
version: post.version ?? '',
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('FIND all cases', () => {
|
|||
beforeAll(async () => {
|
||||
routeHandler = await createRoute(initFindCasesApi, 'get');
|
||||
});
|
||||
|
||||
it(`gets all the cases`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: `${CASES_URL}/_find`,
|
||||
|
@ -38,7 +39,8 @@ describe('FIND all cases', () => {
|
|||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.cases).toHaveLength(4);
|
||||
});
|
||||
it(`has proper connector id on cases with configured id`, async () => {
|
||||
|
||||
it(`has proper connector id on cases with configured connector`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: `${CASES_URL}/_find`,
|
||||
method: 'get',
|
||||
|
@ -52,8 +54,9 @@ describe('FIND all cases', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.cases[2].connector_id).toEqual('123');
|
||||
expect(response.payload.cases[2].connector.id).toEqual('123');
|
||||
});
|
||||
|
||||
it(`adds 'none' connector id to cases without when 3rd party unconfigured`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: `${CASES_URL}/_find`,
|
||||
|
@ -68,8 +71,9 @@ describe('FIND all cases', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.cases[0].connector_id).toEqual('none');
|
||||
expect(response.payload.cases[0].connector.id).toEqual('none');
|
||||
});
|
||||
|
||||
it(`adds default connector id to cases without when 3rd party configured`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: `${CASES_URL}/_find`,
|
||||
|
@ -85,6 +89,6 @@ describe('FIND all cases', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.cases[0].connector_id).toEqual('123');
|
||||
expect(response.payload.cases[0].connector.id).toEqual('none');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,6 @@ import { transformCases, sortToSnake, wrapError, escapeHatch } from '../utils';
|
|||
import { RouteDeps, TotalCommentByCase } from '../types';
|
||||
import { CASE_SAVED_OBJECT } from '../../../saved_object_types';
|
||||
import { CASES_URL } from '../../../../common/constants';
|
||||
import { getConnectorId } from './helpers';
|
||||
|
||||
const combineFilters = (filters: string[], operator: 'OR' | 'AND'): string =>
|
||||
filters?.filter((i) => i !== '').join(` ${operator} `);
|
||||
|
@ -95,11 +94,10 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }:
|
|||
filter: getStatusFilter('closed', myFilters),
|
||||
},
|
||||
};
|
||||
const [cases, openCases, closesCases, myCaseConfigure] = await Promise.all([
|
||||
const [cases, openCases, closesCases] = await Promise.all([
|
||||
caseService.findCases(args),
|
||||
caseService.findCases(argsOpenCases),
|
||||
caseService.findCases(argsClosedCases),
|
||||
caseConfigureService.find({ client }),
|
||||
]);
|
||||
const totalCommentsFindByCases = await Promise.all(
|
||||
cases.saved_objects.map((c) =>
|
||||
|
@ -136,8 +134,7 @@ export function initFindCasesApi({ caseService, caseConfigureService, router }:
|
|||
cases,
|
||||
openCases.total ?? 0,
|
||||
closesCases.total ?? 0,
|
||||
totalCommentsByCases,
|
||||
getConnectorId(myCaseConfigure)
|
||||
totalCommentsByCases
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server';
|
||||
import { httpServerMock } from 'src/core/server/mocks';
|
||||
|
||||
import { CaseAttributes } from '../../../../common/api';
|
||||
import { ConnectorTypes, ESCaseAttributes } from '../../../../common/api';
|
||||
import {
|
||||
createMockSavedObjectsRepository,
|
||||
createRoute,
|
||||
|
@ -15,11 +15,12 @@ import {
|
|||
mockCases,
|
||||
mockCasesErrorTriggerData,
|
||||
mockCaseComments,
|
||||
mockCaseNoConnectorId,
|
||||
mockCaseConfigure,
|
||||
} from '../__fixtures__';
|
||||
import { flattenCaseSavedObject } from '../utils';
|
||||
import { initGetCaseApi } from './get_case';
|
||||
import { CASE_DETAILS_URL } from '../../../../common/constants';
|
||||
import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects';
|
||||
|
||||
describe('GET case', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -46,12 +47,17 @@ describe('GET case', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
const savedObject = (mockCases.find((s) => s.id === 'mock-id-1') as unknown) as SavedObject<
|
||||
CaseAttributes
|
||||
ESCaseAttributes
|
||||
>;
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload).toEqual(flattenCaseSavedObject({ savedObject }));
|
||||
expect(response.payload).toEqual(
|
||||
flattenCaseSavedObject({
|
||||
savedObject,
|
||||
})
|
||||
);
|
||||
expect(response.payload.comments).toEqual([]);
|
||||
});
|
||||
|
||||
it(`returns an error when thrown from getCase`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASE_DETAILS_URL,
|
||||
|
@ -75,6 +81,7 @@ describe('GET case', () => {
|
|||
expect(response.status).toEqual(404);
|
||||
expect(response.payload.isBoom).toEqual(true);
|
||||
});
|
||||
|
||||
it(`returns the case with case comments when includeComments is true`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASE_DETAILS_URL,
|
||||
|
@ -99,6 +106,7 @@ describe('GET case', () => {
|
|||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.comments).toHaveLength(3);
|
||||
});
|
||||
|
||||
it(`returns an error when thrown from getAllCaseComments`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASE_DETAILS_URL,
|
||||
|
@ -121,7 +129,8 @@ describe('GET case', () => {
|
|||
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
it(`case w/o connector_id - returns the case with connector id when 3rd party unconfigured`, async () => {
|
||||
|
||||
it(`case w/o connector.id - returns the case with connector id when 3rd party unconfigured`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASE_DETAILS_URL,
|
||||
method: 'get',
|
||||
|
@ -142,9 +151,15 @@ describe('GET case', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.connector_id).toEqual('none');
|
||||
expect(response.payload.connector).toEqual({
|
||||
fields: null,
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
});
|
||||
});
|
||||
it(`case w/o connector_id - returns the case with connector id when 3rd party configured`, async () => {
|
||||
|
||||
it(`case w/o connector.id - returns the case with connector id when 3rd party configured`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASE_DETAILS_URL,
|
||||
method: 'get',
|
||||
|
@ -166,9 +181,15 @@ describe('GET case', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.connector_id).toEqual('123');
|
||||
expect(response.payload.connector).toEqual({
|
||||
fields: null,
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: '.none',
|
||||
});
|
||||
});
|
||||
it(`case w/ connector_id - returns the case with connector id when case already has connectorId`, async () => {
|
||||
|
||||
it(`case w/ connector.id - returns the case with connector id when case already has connectorId`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASE_DETAILS_URL,
|
||||
method: 'get',
|
||||
|
@ -190,6 +211,11 @@ describe('GET case', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.connector_id).toEqual('123');
|
||||
expect(response.payload.connector).toEqual({
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
type: '.jira',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@ import { CaseResponseRt } from '../../../../common/api';
|
|||
import { RouteDeps } from '../types';
|
||||
import { flattenCaseSavedObject, wrapError } from '../utils';
|
||||
import { CASE_DETAILS_URL } from '../../../../common/constants';
|
||||
import { getConnectorId } from './helpers';
|
||||
|
||||
export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) {
|
||||
router.get(
|
||||
|
@ -30,22 +29,18 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro
|
|||
const client = context.core.savedObjects.client;
|
||||
const includeComments = JSON.parse(request.query.includeComments);
|
||||
|
||||
const [theCase, myCaseConfigure] = await Promise.all([
|
||||
const [theCase] = await Promise.all([
|
||||
caseService.getCase({
|
||||
client,
|
||||
caseId: request.params.case_id,
|
||||
}),
|
||||
caseConfigureService.find({ client }),
|
||||
]);
|
||||
|
||||
const caseConfigureConnectorId = getConnectorId(myCaseConfigure);
|
||||
|
||||
if (!includeComments) {
|
||||
return response.ok({
|
||||
body: CaseResponseRt.encode(
|
||||
flattenCaseSavedObject({
|
||||
savedObject: theCase,
|
||||
caseConfigureConnectorId,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
@ -66,7 +61,6 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro
|
|||
savedObject: theCase,
|
||||
comments: theComments.saved_objects,
|
||||
totalComment: theComments.total,
|
||||
caseConfigureConnectorId,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
110
x-pack/plugins/case/server/routes/api/cases/helpers.test.ts
Normal file
110
x-pack/plugins/case/server/routes/api/cases/helpers.test.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import {
|
||||
CaseConnector,
|
||||
ConnectorTypes,
|
||||
ESCaseConnector,
|
||||
ESCasesConfigureAttributes,
|
||||
} from '../../../../common/api';
|
||||
import { mockCaseConfigure } from '../__fixtures__';
|
||||
import {
|
||||
transformCaseConnectorToEsConnector,
|
||||
transformESConnectorToCaseConnector,
|
||||
getConnectorFromConfiguration,
|
||||
} from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
const caseConnector: CaseConnector = {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
};
|
||||
|
||||
const esCaseConnector: ESCaseConnector = {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
};
|
||||
|
||||
const caseConfigure: SavedObjectsFindResponse<ESCasesConfigureAttributes> = {
|
||||
saved_objects: [{ ...mockCaseConfigure[0], score: 0 }],
|
||||
total: 1,
|
||||
per_page: 20,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
describe('transformCaseConnectorToEsConnector', () => {
|
||||
it('transform correctly', () => {
|
||||
expect(transformCaseConnectorToEsConnector(caseConnector)).toEqual(esCaseConnector);
|
||||
});
|
||||
|
||||
it('transform correctly with null attributes', () => {
|
||||
// @ts-ignore this is case the connector does not exist for old cases object or configurations
|
||||
expect(transformCaseConnectorToEsConnector(null)).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformESConnectorToCaseConnector', () => {
|
||||
it('transform correctly', () => {
|
||||
expect(transformESConnectorToCaseConnector(esCaseConnector)).toEqual(caseConnector);
|
||||
});
|
||||
|
||||
it('transform correctly with null attributes', () => {
|
||||
// @ts-ignore this is case the connector does not exist for old cases object or configurations
|
||||
expect(transformESConnectorToCaseConnector(null)).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConnectorFromConfiguration', () => {
|
||||
it('transform correctly', () => {
|
||||
expect(getConnectorFromConfiguration(caseConfigure)).toEqual({
|
||||
id: '789',
|
||||
name: 'My connector 3',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('transform correctly with no connector', () => {
|
||||
const caseConfigureNoConnector: SavedObjectsFindResponse<ESCasesConfigureAttributes> = {
|
||||
...caseConfigure,
|
||||
saved_objects: [
|
||||
{
|
||||
...mockCaseConfigure[0],
|
||||
// @ts-ignore this is case the connector does not exist for old cases object or configurations
|
||||
attributes: { ...mockCaseConfigure[0].attributes, connector: null },
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getConnectorFromConfiguration(caseConfigureNoConnector)).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,10 +4,19 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get, isPlainObject } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { SavedObjectsFindResponse } from 'kibana/server';
|
||||
import { CaseAttributes, CasePatchRequest, CasesConfigureAttributes } from '../../../../common/api';
|
||||
import {
|
||||
CaseConnector,
|
||||
ESCaseConnector,
|
||||
ESCaseAttributes,
|
||||
ESCasePatchRequest,
|
||||
ESCasesConfigureAttributes,
|
||||
ConnectorTypes,
|
||||
} from '../../../../common/api';
|
||||
import { ESConnectorFields, ConnectorTypeFields } from '../../../../common/api/connectors';
|
||||
|
||||
interface CompareArrays {
|
||||
addedItems: string[];
|
||||
|
@ -57,9 +66,9 @@ export const isTwoArraysDifference = (
|
|||
};
|
||||
|
||||
export const getCaseToUpdate = (
|
||||
currentCase: CaseAttributes,
|
||||
queryCase: CasePatchRequest
|
||||
): CasePatchRequest =>
|
||||
currentCase: ESCaseAttributes,
|
||||
queryCase: ESCasePatchRequest
|
||||
): ESCasePatchRequest =>
|
||||
Object.entries(queryCase).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const currentValue = get(currentCase, key);
|
||||
|
@ -70,26 +79,89 @@ export const getCaseToUpdate = (
|
|||
[key]: value,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
} else if (isPlainObject(currentValue) && isPlainObject(value)) {
|
||||
if (!deepEqual(currentValue, value)) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
} else if (currentValue != null && value !== currentValue) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: value,
|
||||
};
|
||||
} else if (currentValue == null && key === 'connector_id' && value !== currentValue) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ id: queryCase.id, version: queryCase.version }
|
||||
);
|
||||
|
||||
export const getConnectorId = (
|
||||
caseConfigure: SavedObjectsFindResponse<CasesConfigureAttributes>
|
||||
): string =>
|
||||
caseConfigure.saved_objects.length > 0
|
||||
? caseConfigure.saved_objects[0].attributes.connector_id
|
||||
: 'none';
|
||||
export const getNoneCaseConnector = () => ({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
|
||||
export const getConnectorFromConfiguration = (
|
||||
caseConfigure: SavedObjectsFindResponse<ESCasesConfigureAttributes>
|
||||
): CaseConnector => {
|
||||
let caseConnector = getNoneCaseConnector();
|
||||
if (
|
||||
caseConfigure.saved_objects.length > 0 &&
|
||||
caseConfigure.saved_objects[0].attributes.connector
|
||||
) {
|
||||
caseConnector = {
|
||||
id: caseConfigure.saved_objects[0].attributes.connector.id,
|
||||
name: caseConfigure.saved_objects[0].attributes.connector.name,
|
||||
type: caseConfigure.saved_objects[0].attributes.connector.type,
|
||||
fields: null,
|
||||
};
|
||||
}
|
||||
return caseConnector;
|
||||
};
|
||||
|
||||
export const transformCaseConnectorToEsConnector = (connector: CaseConnector): ESCaseConnector => ({
|
||||
id: connector?.id ?? 'none',
|
||||
name: connector?.name ?? 'none',
|
||||
type: connector?.type ?? '.none',
|
||||
fields:
|
||||
connector?.fields != null
|
||||
? Object.entries(connector.fields).reduce<ESConnectorFields>(
|
||||
(acc, [key, value]) => [
|
||||
...acc,
|
||||
{
|
||||
key,
|
||||
value,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
: [],
|
||||
});
|
||||
|
||||
export const transformESConnectorToCaseConnector = (connector?: ESCaseConnector): CaseConnector => {
|
||||
const connectorTypeField = {
|
||||
type: connector?.type ?? '.none',
|
||||
fields:
|
||||
connector && connector.fields != null && connector.fields.length > 0
|
||||
? connector.fields.reduce(
|
||||
(fields, { key, value }) => ({
|
||||
...fields,
|
||||
[key]: value,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
: null,
|
||||
} as ConnectorTypeFields;
|
||||
|
||||
return {
|
||||
id: connector?.id ?? 'none',
|
||||
name: connector?.name ?? 'none',
|
||||
...connectorTypeField,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '../__fixtures__';
|
||||
import { initPatchCasesApi } from './patch_cases';
|
||||
import { mockCaseConfigure, mockCaseNoConnectorId } from '../__fixtures__/mock_saved_objects';
|
||||
import { ConnectorTypes } from '../../../../common/api/connectors';
|
||||
|
||||
describe('PATCH cases', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -26,6 +27,7 @@ describe('PATCH cases', () => {
|
|||
toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'),
|
||||
}));
|
||||
});
|
||||
|
||||
it(`Close a case`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
|
@ -54,7 +56,12 @@ describe('PATCH cases', () => {
|
|||
closed_at: '2019-11-25T21:54:48.952Z',
|
||||
closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
|
||||
comments: [],
|
||||
connector_id: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
|
@ -70,6 +77,7 @@ describe('PATCH cases', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it(`Open a case`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
|
@ -99,7 +107,12 @@ describe('PATCH cases', () => {
|
|||
closed_at: null,
|
||||
closed_by: null,
|
||||
comments: [],
|
||||
connector_id: '123',
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
type: '.jira',
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
created_at: '2019-11-25T22:32:17.947Z',
|
||||
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
|
||||
description: 'Oh no, a bad meanie going LOLBins all over the place!',
|
||||
|
@ -115,7 +128,8 @@ describe('PATCH cases', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
it(`Patches a case without a connector_id`, async () => {
|
||||
|
||||
it(`Patches a case without a connector.id`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
method: 'patch',
|
||||
|
@ -138,9 +152,10 @@ describe('PATCH cases', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload[0].connector_id).toEqual('none');
|
||||
expect(response.payload[0].connector.id).toEqual('none');
|
||||
});
|
||||
it(`Patches a case with a connector_id`, async () => {
|
||||
|
||||
it(`Patches a case with a connector.id`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
method: 'patch',
|
||||
|
@ -163,8 +178,45 @@ describe('PATCH cases', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload[0].connector_id).toEqual('123');
|
||||
expect(response.payload[0].connector.id).toEqual('123');
|
||||
});
|
||||
|
||||
it(`Change connector`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
method: 'patch',
|
||||
body: {
|
||||
cases: [
|
||||
{
|
||||
id: 'mock-id-3',
|
||||
connector: {
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: '.jira',
|
||||
fields: { issueType: 'Bug', priority: 'Low', parent: null },
|
||||
},
|
||||
version: 'WzUsMV0=',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const theContext = createRouteContext(
|
||||
createMockSavedObjectsRepository({
|
||||
caseSavedObject: mockCases,
|
||||
})
|
||||
);
|
||||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload[0].connector).toEqual({
|
||||
id: '456',
|
||||
name: 'My connector 2',
|
||||
type: '.jira',
|
||||
fields: { issueType: 'Bug', priority: 'Low', parent: null },
|
||||
});
|
||||
});
|
||||
|
||||
it(`Fails with 409 if version does not match`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
|
@ -189,6 +241,7 @@ describe('PATCH cases', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(409);
|
||||
});
|
||||
|
||||
it(`Fails with 406 if updated field is unchanged`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
|
@ -214,6 +267,7 @@ describe('PATCH cases', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(406);
|
||||
});
|
||||
|
||||
it(`Returns an error if updateCase throws`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: '/api/cases',
|
||||
|
|
|
@ -15,10 +15,11 @@ import {
|
|||
CasePatchRequest,
|
||||
excess,
|
||||
throwErrors,
|
||||
ESCasePatchRequest,
|
||||
} from '../../../../common/api';
|
||||
import { escapeHatch, wrapError, flattenCaseSavedObject } from '../utils';
|
||||
import { RouteDeps } from '../types';
|
||||
import { getCaseToUpdate, getConnectorId } from './helpers';
|
||||
import { getCaseToUpdate, transformCaseConnectorToEsConnector } from './helpers';
|
||||
import { buildCaseUserActions } from '../../../services/user_actions/helpers';
|
||||
import { CASES_URL } from '../../../../common/constants';
|
||||
|
||||
|
@ -43,14 +44,10 @@ export function initPatchCasesApi({
|
|||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const [myCases, myCaseConfigure] = await Promise.all([
|
||||
caseService.getCases({
|
||||
client,
|
||||
caseIds: query.cases.map((q) => q.id),
|
||||
}),
|
||||
caseConfigureService.find({ client }),
|
||||
]);
|
||||
const caseConfigureConnectorId = getConnectorId(myCaseConfigure);
|
||||
const myCases = await caseService.getCases({
|
||||
client,
|
||||
caseIds: query.cases.map((q) => q.id),
|
||||
});
|
||||
|
||||
let nonExistingCases: CasePatchRequest[] = [];
|
||||
const conflictedCases = query.cases.filter((q) => {
|
||||
|
@ -76,16 +73,25 @@ export function initPatchCasesApi({
|
|||
.join(', ')} has been updated. Please refresh before saving additional updates.`
|
||||
);
|
||||
}
|
||||
const updateCases: CasePatchRequest[] = query.cases.map((thisCase) => {
|
||||
const currentCase = myCases.saved_objects.find((c) => c.id === thisCase.id);
|
||||
|
||||
const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => {
|
||||
const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id);
|
||||
const { connector, ...thisCase } = updateCase;
|
||||
return currentCase != null
|
||||
? getCaseToUpdate(currentCase.attributes, thisCase)
|
||||
? getCaseToUpdate(currentCase.attributes, {
|
||||
...thisCase,
|
||||
...(connector != null
|
||||
? { connector: transformCaseConnectorToEsConnector(connector) }
|
||||
: {}),
|
||||
})
|
||||
: { id: thisCase.id, version: thisCase.version };
|
||||
});
|
||||
|
||||
const updateFilterCases = updateCases.filter((updateCase) => {
|
||||
const { id, version, ...updateCaseAttributes } = updateCase;
|
||||
return Object.keys(updateCaseAttributes).length > 0;
|
||||
});
|
||||
|
||||
if (updateFilterCases.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { username, full_name, email } = await caseService.getUser({ request, response });
|
||||
|
@ -133,7 +139,6 @@ export function initPatchCasesApi({
|
|||
references: myCase.references,
|
||||
version: updatedCase?.version ?? myCase.version,
|
||||
},
|
||||
caseConfigureConnectorId,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { initPostCaseApi } from './post_case';
|
||||
import { CASES_URL } from '../../../../common/constants';
|
||||
import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects';
|
||||
import { ConnectorTypes } from '../../../../common/api/connectors';
|
||||
|
||||
describe('POST cases', () => {
|
||||
let routeHandler: RequestHandler<any, any, any>;
|
||||
|
@ -26,6 +27,7 @@ describe('POST cases', () => {
|
|||
toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'),
|
||||
}));
|
||||
});
|
||||
|
||||
it(`Posts a new case, no connector configured`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASES_URL,
|
||||
|
@ -34,6 +36,12 @@ describe('POST cases', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Security Issue',
|
||||
tags: ['defacement'],
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -47,9 +55,15 @@ describe('POST cases', () => {
|
|||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.id).toEqual('mock-it');
|
||||
expect(response.payload.created_by.username).toEqual('awesome');
|
||||
expect(response.payload.connector_id).toEqual('none');
|
||||
expect(response.payload.connector).toEqual({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
});
|
||||
it(`Posts a new case, connector configured`, async () => {
|
||||
|
||||
it(`Posts a new case, connector provided`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASES_URL,
|
||||
method: 'post',
|
||||
|
@ -57,6 +71,12 @@ describe('POST cases', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Security Issue',
|
||||
tags: ['defacement'],
|
||||
connector: {
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: '.jira',
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -69,7 +89,12 @@ describe('POST cases', () => {
|
|||
|
||||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.payload.connector_id).toEqual('123');
|
||||
expect(response.payload.connector).toEqual({
|
||||
id: '123',
|
||||
name: 'Jira',
|
||||
type: '.jira',
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
});
|
||||
});
|
||||
|
||||
it(`Error if you passing status for a new case`, async () => {
|
||||
|
@ -81,6 +106,7 @@ describe('POST cases', () => {
|
|||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
tags: ['defacement'],
|
||||
connector: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -93,6 +119,7 @@ describe('POST cases', () => {
|
|||
const response = await routeHandler(theContext, request, kibanaResponseFactory);
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
it(`Returns an error if postNewCase throws`, async () => {
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
path: CASES_URL,
|
||||
|
@ -101,6 +128,7 @@ describe('POST cases', () => {
|
|||
description: 'Throw an error',
|
||||
title: 'Super Bad Security Issue',
|
||||
tags: ['error'],
|
||||
connector: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -114,6 +142,7 @@ describe('POST cases', () => {
|
|||
expect(response.status).toEqual(400);
|
||||
expect(response.payload.isBoom).toEqual(true);
|
||||
});
|
||||
|
||||
it(`Allow user to create case without authentication`, async () => {
|
||||
routeHandler = await createRoute(initPostCaseApi, 'post', true);
|
||||
|
||||
|
@ -124,6 +153,12 @@ describe('POST cases', () => {
|
|||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
title: 'Super Bad Security Issue',
|
||||
tags: ['defacement'],
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -140,7 +175,12 @@ describe('POST cases', () => {
|
|||
closed_at: null,
|
||||
closed_by: null,
|
||||
comments: [],
|
||||
connector_id: '123',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
email: null,
|
||||
|
|
|
@ -15,7 +15,7 @@ import { CasePostRequestRt, throwErrors, excess, CaseResponseRt } from '../../..
|
|||
import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
|
||||
import { RouteDeps } from '../types';
|
||||
import { CASES_URL } from '../../../../common/constants';
|
||||
import { getConnectorId } from './helpers';
|
||||
import { getConnectorFromConfiguration, transformCaseConnectorToEsConnector } from './helpers';
|
||||
|
||||
export function initPostCaseApi({
|
||||
caseService,
|
||||
|
@ -42,7 +42,8 @@ export function initPostCaseApi({
|
|||
const { username, full_name, email } = await caseService.getUser({ request, response });
|
||||
const createdDate = new Date().toISOString();
|
||||
const myCaseConfigure = await caseConfigureService.find({ client });
|
||||
const connectorId = getConnectorId(myCaseConfigure);
|
||||
const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure);
|
||||
|
||||
const newCase = await caseService.postNewCase({
|
||||
client,
|
||||
attributes: transformNewCase({
|
||||
|
@ -51,7 +52,9 @@ export function initPostCaseApi({
|
|||
username,
|
||||
full_name,
|
||||
email,
|
||||
connectorId,
|
||||
connector: transformCaseConnectorToEsConnector(
|
||||
query.connector ?? caseConfigureConnector
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import Boom from 'boom';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
|
@ -16,7 +17,6 @@ import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../..
|
|||
import { buildCaseUserActionItem } from '../../../services/user_actions/helpers';
|
||||
import { RouteDeps } from '../types';
|
||||
import { CASE_DETAILS_URL } from '../../../../common/constants';
|
||||
import { getConnectorId } from './helpers';
|
||||
|
||||
export function initPushCaseUserActionApi({
|
||||
caseConfigureService,
|
||||
|
@ -94,14 +94,13 @@ export function initPushCaseUserActionApi({
|
|||
...query,
|
||||
};
|
||||
|
||||
const caseConfigureConnectorId = getConnectorId(myCaseConfigure);
|
||||
const updateConnector = myCase.attributes.connector;
|
||||
|
||||
// old case may not have new attribute connector_id, so we default to the configured system
|
||||
const updateConnectorId = {
|
||||
connector_id: myCase.attributes.connector_id ?? caseConfigureConnectorId,
|
||||
};
|
||||
|
||||
if (!connectors.some((connector) => connector.id === updateConnectorId.connector_id)) {
|
||||
if (
|
||||
isEmpty(updateConnector) ||
|
||||
(updateConnector != null && updateConnector.id === 'none') ||
|
||||
!connectors.some((connector) => connector.id === updateConnector.id)
|
||||
) {
|
||||
throw Boom.notFound('Connector not found or set to none');
|
||||
}
|
||||
|
||||
|
@ -121,7 +120,6 @@ export function initPushCaseUserActionApi({
|
|||
external_service: externalService,
|
||||
updated_at: pushedDate,
|
||||
updated_by: { username, full_name, email },
|
||||
...updateConnectorId,
|
||||
},
|
||||
version: myCase.version,
|
||||
}),
|
||||
|
|
|
@ -23,13 +23,24 @@ import {
|
|||
mockCaseComments,
|
||||
mockCaseNoConnectorId,
|
||||
} from './__fixtures__/mock_saved_objects';
|
||||
import { ConnectorTypes, ESCaseConnector } from '../../../common/api';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('transformNewCase', () => {
|
||||
const connector: ESCaseConnector = {
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: [
|
||||
{ key: 'issueType', value: 'Task' },
|
||||
{ key: 'priority', value: 'High' },
|
||||
{ key: 'parent', value: null },
|
||||
],
|
||||
};
|
||||
it('transform correctly', () => {
|
||||
const myCase = {
|
||||
newCase,
|
||||
connectorId: '123',
|
||||
connector,
|
||||
createdDate: '2020-04-09T09:43:51.778Z',
|
||||
email: 'elastic@elastic.co',
|
||||
full_name: 'Elastic',
|
||||
|
@ -42,7 +53,7 @@ describe('Utils', () => {
|
|||
...myCase.newCase,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: '123',
|
||||
connector,
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: 'elastic@elastic.co', full_name: 'Elastic', username: 'elastic' },
|
||||
external_service: null,
|
||||
|
@ -55,7 +66,7 @@ describe('Utils', () => {
|
|||
it('transform correctly without optional fields', () => {
|
||||
const myCase = {
|
||||
newCase,
|
||||
connectorId: '123',
|
||||
connector,
|
||||
createdDate: '2020-04-09T09:43:51.778Z',
|
||||
};
|
||||
|
||||
|
@ -65,7 +76,7 @@ describe('Utils', () => {
|
|||
...myCase.newCase,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: '123',
|
||||
connector,
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: undefined, full_name: undefined, username: undefined },
|
||||
external_service: null,
|
||||
|
@ -78,7 +89,7 @@ describe('Utils', () => {
|
|||
it('transform correctly with optional fields as null', () => {
|
||||
const myCase = {
|
||||
newCase,
|
||||
connectorId: '123',
|
||||
connector,
|
||||
createdDate: '2020-04-09T09:43:51.778Z',
|
||||
email: null,
|
||||
full_name: null,
|
||||
|
@ -91,7 +102,7 @@ describe('Utils', () => {
|
|||
...myCase.newCase,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: '123',
|
||||
connector,
|
||||
created_at: '2020-04-09T09:43:51.778Z',
|
||||
created_by: { email: null, full_name: null, username: null },
|
||||
external_service: null,
|
||||
|
@ -230,8 +241,7 @@ describe('Utils', () => {
|
|||
},
|
||||
2,
|
||||
2,
|
||||
extraCaseData,
|
||||
'123'
|
||||
extraCaseData
|
||||
);
|
||||
expect(res).toEqual({
|
||||
page: 1,
|
||||
|
@ -239,8 +249,7 @@ describe('Utils', () => {
|
|||
total: mockCases.length,
|
||||
cases: flattenCaseSavedObjects(
|
||||
mockCases.map((obj) => ({ ...obj, score: 1 })),
|
||||
extraCaseData,
|
||||
'123'
|
||||
extraCaseData
|
||||
),
|
||||
count_open_cases: 2,
|
||||
count_closed_cases: 2,
|
||||
|
@ -252,13 +261,19 @@ describe('Utils', () => {
|
|||
it('flattens correctly', () => {
|
||||
const extraCaseData = [{ caseId: mockCases[0].id, totalComments: 2 }];
|
||||
|
||||
const res = flattenCaseSavedObjects([mockCases[0]], extraCaseData, '123');
|
||||
const res = flattenCaseSavedObjects([mockCases[0]], extraCaseData);
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -285,14 +300,19 @@ describe('Utils', () => {
|
|||
|
||||
it('it handles total comments correctly when caseId is not in extraCaseData', () => {
|
||||
const extraCaseData = [{ caseId: mockCases[0].id, totalComments: 0 }];
|
||||
const res = flattenCaseSavedObjects([mockCases[0]], extraCaseData, '123');
|
||||
const res = flattenCaseSavedObjects([mockCases[0]], extraCaseData);
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: 'mock-id-1',
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -316,7 +336,8 @@ describe('Utils', () => {
|
|||
},
|
||||
]);
|
||||
});
|
||||
it('inserts missing connectorId', () => {
|
||||
|
||||
it('inserts missing connector', () => {
|
||||
const extraCaseData = [
|
||||
{
|
||||
caseId: mockCaseNoConnectorId.id,
|
||||
|
@ -324,53 +345,20 @@ describe('Utils', () => {
|
|||
},
|
||||
];
|
||||
|
||||
// @ts-ignore this is to update old case saved objects to include connector_id
|
||||
const res = flattenCaseSavedObjects([mockCaseNoConnectorId], extraCaseData, '123');
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: mockCaseNoConnectorId.id,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: '123',
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
comments: [],
|
||||
totalComment: 0,
|
||||
version: 'WzAsMV0=',
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('inserts missing connectorId (none)', () => {
|
||||
const extraCaseData = [
|
||||
{
|
||||
caseId: mockCaseNoConnectorId.id,
|
||||
totalComment: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// @ts-ignore this is to update old case saved objects to include connector_id
|
||||
// @ts-ignore this is to update old case saved objects to include connector
|
||||
const res = flattenCaseSavedObjects([mockCaseNoConnectorId], extraCaseData);
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
id: mockCaseNoConnectorId.id,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
@ -398,90 +386,89 @@ describe('Utils', () => {
|
|||
|
||||
describe('flattenCaseSavedObject', () => {
|
||||
it('flattens correctly', () => {
|
||||
const myCase = { ...mockCases[0] };
|
||||
const res = flattenCaseSavedObject({ savedObject: myCase, totalComment: 2 });
|
||||
const myCase = { ...mockCases[2] };
|
||||
const res = flattenCaseSavedObject({
|
||||
savedObject: myCase,
|
||||
totalComment: 2,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: myCase.id,
|
||||
version: myCase.version,
|
||||
comments: [],
|
||||
totalComment: 2,
|
||||
...myCase.attributes,
|
||||
connector: {
|
||||
...myCase.attributes.connector,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('flattens correctly without version', () => {
|
||||
const myCase = { ...mockCases[0] };
|
||||
const myCase = { ...mockCases[2] };
|
||||
myCase.version = undefined;
|
||||
const res = flattenCaseSavedObject({ savedObject: myCase, totalComment: 2 });
|
||||
const res = flattenCaseSavedObject({
|
||||
savedObject: myCase,
|
||||
totalComment: 2,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: myCase.id,
|
||||
version: '0',
|
||||
comments: [],
|
||||
totalComment: 2,
|
||||
...myCase.attributes,
|
||||
connector: {
|
||||
...myCase.attributes.connector,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('flattens correctly with comments', () => {
|
||||
const myCase = { ...mockCases[0] };
|
||||
const myCase = { ...mockCases[2] };
|
||||
const comments = [{ ...mockCaseComments[0] }];
|
||||
const res = flattenCaseSavedObject({ savedObject: myCase, comments, totalComment: 2 });
|
||||
const res = flattenCaseSavedObject({
|
||||
savedObject: myCase,
|
||||
comments,
|
||||
totalComment: 2,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: myCase.id,
|
||||
version: myCase.version,
|
||||
comments: flattenCommentSavedObjects(comments),
|
||||
totalComment: 2,
|
||||
...myCase.attributes,
|
||||
connector: {
|
||||
...myCase.attributes.connector,
|
||||
fields: { issueType: 'Task', priority: 'High', parent: null },
|
||||
},
|
||||
});
|
||||
});
|
||||
it('inserts missing connectorId', () => {
|
||||
|
||||
it('inserts missing connector', () => {
|
||||
const extraCaseData = {
|
||||
totalComment: 2,
|
||||
caseConfigureConnectorId: '123',
|
||||
};
|
||||
|
||||
// @ts-ignore this is to update old case saved objects to include connector_id
|
||||
const res = flattenCaseSavedObject({ savedObject: mockCaseNoConnectorId, ...extraCaseData });
|
||||
const res = flattenCaseSavedObject({
|
||||
// @ts-ignore this is to update old case saved objects to include connector
|
||||
savedObject: mockCaseNoConnectorId,
|
||||
...extraCaseData,
|
||||
});
|
||||
|
||||
expect(res).toEqual({
|
||||
id: mockCaseNoConnectorId.id,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: '123',
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
description: 'This is a brand new case of a bad meanie defacing data',
|
||||
external_service: null,
|
||||
title: 'Super Bad Security Issue',
|
||||
status: 'open',
|
||||
tags: ['defacement'],
|
||||
updated_at: '2019-11-25T21:54:48.952Z',
|
||||
updated_by: {
|
||||
full_name: 'elastic',
|
||||
email: 'testemail@elastic.co',
|
||||
username: 'elastic',
|
||||
},
|
||||
comments: [],
|
||||
totalComment: 2,
|
||||
version: 'WzAsMV0=',
|
||||
});
|
||||
});
|
||||
it('inserts missing connectorId (none)', () => {
|
||||
const extraCaseData = {
|
||||
totalComment: 2,
|
||||
caseConfigureConnectorId: 'none',
|
||||
};
|
||||
|
||||
// @ts-ignore this is to update old case saved objects to include connector_id
|
||||
const res = flattenCaseSavedObject({ savedObject: mockCaseNoConnectorId, ...extraCaseData });
|
||||
expect(res).toEqual({
|
||||
id: mockCaseNoConnectorId.id,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: 'none',
|
||||
created_at: '2019-11-25T21:54:48.952Z',
|
||||
created_by: {
|
||||
full_name: 'elastic',
|
||||
|
|
|
@ -17,16 +17,18 @@ import {
|
|||
CasePostRequest,
|
||||
CaseResponse,
|
||||
CasesFindResponse,
|
||||
CaseAttributes,
|
||||
CommentResponse,
|
||||
CommentsResponse,
|
||||
CommentAttributes,
|
||||
ESCaseConnector,
|
||||
ESCaseAttributes,
|
||||
} from '../../../common/api';
|
||||
import { transformESConnectorToCaseConnector } from './cases/helpers';
|
||||
|
||||
import { SortFieldCase, TotalCommentByCase } from './types';
|
||||
|
||||
export const transformNewCase = ({
|
||||
connectorId,
|
||||
connector,
|
||||
createdDate,
|
||||
email,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
@ -34,17 +36,17 @@ export const transformNewCase = ({
|
|||
newCase,
|
||||
username,
|
||||
}: {
|
||||
connectorId: string;
|
||||
connector: ESCaseConnector;
|
||||
createdDate: string;
|
||||
email?: string | null;
|
||||
full_name?: string | null;
|
||||
newCase: CasePostRequest;
|
||||
username?: string | null;
|
||||
}): CaseAttributes => ({
|
||||
}): ESCaseAttributes => ({
|
||||
...newCase,
|
||||
closed_at: null,
|
||||
closed_by: null,
|
||||
connector_id: connectorId,
|
||||
connector,
|
||||
created_at: createdDate,
|
||||
created_by: { email, full_name, username },
|
||||
external_service: null,
|
||||
|
@ -88,33 +90,30 @@ export function wrapError(error: any): CustomHttpResponseOptions<ResponseError>
|
|||
}
|
||||
|
||||
export const transformCases = (
|
||||
cases: SavedObjectsFindResponse<CaseAttributes>,
|
||||
cases: SavedObjectsFindResponse<ESCaseAttributes>,
|
||||
countOpenCases: number,
|
||||
countClosedCases: number,
|
||||
totalCommentByCase: TotalCommentByCase[],
|
||||
caseConfigureConnectorId: string = 'none'
|
||||
totalCommentByCase: TotalCommentByCase[]
|
||||
): CasesFindResponse => ({
|
||||
page: cases.page,
|
||||
per_page: cases.per_page,
|
||||
total: cases.total,
|
||||
cases: flattenCaseSavedObjects(cases.saved_objects, totalCommentByCase, caseConfigureConnectorId),
|
||||
cases: flattenCaseSavedObjects(cases.saved_objects, totalCommentByCase),
|
||||
count_open_cases: countOpenCases,
|
||||
count_closed_cases: countClosedCases,
|
||||
});
|
||||
|
||||
export const flattenCaseSavedObjects = (
|
||||
savedObjects: Array<SavedObject<CaseAttributes>>,
|
||||
totalCommentByCase: TotalCommentByCase[],
|
||||
caseConfigureConnectorId: string = 'none'
|
||||
savedObjects: Array<SavedObject<ESCaseAttributes>>,
|
||||
totalCommentByCase: TotalCommentByCase[]
|
||||
): CaseResponse[] =>
|
||||
savedObjects.reduce((acc: CaseResponse[], savedObject: SavedObject<CaseAttributes>) => {
|
||||
savedObjects.reduce((acc: CaseResponse[], savedObject: SavedObject<ESCaseAttributes>) => {
|
||||
return [
|
||||
...acc,
|
||||
flattenCaseSavedObject({
|
||||
savedObject,
|
||||
totalComment:
|
||||
totalCommentByCase.find((tc) => tc.caseId === savedObject.id)?.totalComments ?? 0,
|
||||
caseConfigureConnectorId,
|
||||
}),
|
||||
];
|
||||
}, []);
|
||||
|
@ -123,19 +122,17 @@ export const flattenCaseSavedObject = ({
|
|||
savedObject,
|
||||
comments = [],
|
||||
totalComment = 0,
|
||||
caseConfigureConnectorId = 'none',
|
||||
}: {
|
||||
savedObject: SavedObject<CaseAttributes>;
|
||||
savedObject: SavedObject<ESCaseAttributes>;
|
||||
comments?: Array<SavedObject<CommentAttributes>>;
|
||||
totalComment?: number;
|
||||
caseConfigureConnectorId?: string;
|
||||
}): CaseResponse => ({
|
||||
id: savedObject.id,
|
||||
version: savedObject.version ?? '0',
|
||||
comments: flattenCommentSavedObjects(comments),
|
||||
totalComment,
|
||||
...savedObject.attributes,
|
||||
connector_id: savedObject.attributes.connector_id ?? caseConfigureConnectorId,
|
||||
connector: transformESConnectorToCaseConnector(savedObject.attributes.connector),
|
||||
});
|
||||
|
||||
export const transformComments = (
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsType } from 'src/core/server';
|
||||
import { caseMigrations } from './migrations';
|
||||
|
||||
export const CASE_SAVED_OBJECT = 'cases';
|
||||
|
||||
|
@ -49,8 +50,28 @@ export const caseSavedObjectType: SavedObjectsType = {
|
|||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
connector_id: {
|
||||
type: 'keyword',
|
||||
connector: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
fields: {
|
||||
properties: {
|
||||
key: {
|
||||
type: 'text',
|
||||
},
|
||||
value: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
external_service: {
|
||||
properties: {
|
||||
|
@ -115,4 +136,5 @@ export const caseSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
migrations: caseMigrations,
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsType } from 'src/core/server';
|
||||
import { configureMigrations } from './migrations';
|
||||
|
||||
export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure';
|
||||
|
||||
|
@ -30,11 +31,28 @@ export const caseConfigureSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
connector_id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
connector_name: {
|
||||
type: 'keyword',
|
||||
connector: {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
fields: {
|
||||
properties: {
|
||||
key: {
|
||||
type: 'text',
|
||||
},
|
||||
value: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
closure_type: {
|
||||
type: 'keyword',
|
||||
|
@ -57,4 +75,5 @@ export const caseConfigureSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
migrations: configureMigrations,
|
||||
};
|
||||
|
|
128
x-pack/plugins/case/server/saved_object_types/migrations.ts
Normal file
128
x-pack/plugins/case/server/saved_object_types/migrations.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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/naming-convention */
|
||||
|
||||
import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server';
|
||||
import { ConnectorTypes } from '../../common/api/connectors';
|
||||
|
||||
interface UnsanitizedCase {
|
||||
connector_id: string;
|
||||
}
|
||||
|
||||
interface UnsanitizedConfigure {
|
||||
connector_id: string;
|
||||
connector_name: string;
|
||||
}
|
||||
|
||||
interface SanitizedCase {
|
||||
connector: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: string | null;
|
||||
fields: null;
|
||||
};
|
||||
}
|
||||
|
||||
interface SanitizedConfigure {
|
||||
connector: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
type: string | null;
|
||||
fields: null;
|
||||
};
|
||||
}
|
||||
|
||||
interface UserActions {
|
||||
action_field: string[];
|
||||
new_value: string;
|
||||
old_value: string;
|
||||
}
|
||||
|
||||
export const caseMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedCase>
|
||||
): SavedObjectSanitizedDoc<SanitizedCase> => {
|
||||
const { connector_id, ...attributesWithoutConnectorId } = doc.attributes;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...attributesWithoutConnectorId,
|
||||
connector: {
|
||||
id: connector_id ?? 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const configureMigrations = {
|
||||
'7.10.0': (
|
||||
doc: SavedObjectUnsanitizedDoc<UnsanitizedConfigure>
|
||||
): SavedObjectSanitizedDoc<SanitizedConfigure> => {
|
||||
const { connector_id, connector_name, ...restAttributes } = doc.attributes;
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
connector: {
|
||||
id: connector_id ?? 'none',
|
||||
name: connector_name ?? 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const userActionsMigrations = {
|
||||
'7.10.0': (doc: SavedObjectUnsanitizedDoc<UserActions>): SavedObjectSanitizedDoc<UserActions> => {
|
||||
const { action_field, new_value, old_value, ...restAttributes } = doc.attributes;
|
||||
|
||||
if (
|
||||
action_field == null ||
|
||||
!Array.isArray(action_field) ||
|
||||
action_field[0] !== 'connector_id'
|
||||
) {
|
||||
return { ...doc, references: doc.references || [] };
|
||||
}
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...restAttributes,
|
||||
action_field: ['connector'],
|
||||
new_value:
|
||||
new_value != null
|
||||
? JSON.stringify({
|
||||
id: new_value,
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
})
|
||||
: new_value,
|
||||
old_value:
|
||||
old_value != null
|
||||
? JSON.stringify({
|
||||
id: old_value,
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
})
|
||||
: old_value,
|
||||
},
|
||||
references: doc.references || [],
|
||||
};
|
||||
},
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsType } from 'src/core/server';
|
||||
import { userActionsMigrations } from './migrations';
|
||||
|
||||
export const CASE_USER_ACTION_SAVED_OBJECT = 'cases-user-actions';
|
||||
|
||||
|
@ -44,4 +45,5 @@ export const caseUserActionSavedObjectType: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
},
|
||||
migrations: userActionsMigrations,
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
SavedObjectsUpdateResponse,
|
||||
} from 'kibana/server';
|
||||
|
||||
import { CasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api';
|
||||
import { ESCasesConfigureAttributes, SavedObjectFindOptions } from '../../../common/api';
|
||||
import { CASE_CONFIGURE_SAVED_OBJECT } from '../../saved_object_types';
|
||||
|
||||
interface ClientArgs {
|
||||
|
@ -27,22 +27,22 @@ interface FindCaseConfigureArgs extends ClientArgs {
|
|||
}
|
||||
|
||||
interface PostCaseConfigureArgs extends ClientArgs {
|
||||
attributes: CasesConfigureAttributes;
|
||||
attributes: ESCasesConfigureAttributes;
|
||||
}
|
||||
|
||||
interface PatchCaseConfigureArgs extends ClientArgs {
|
||||
caseConfigureId: string;
|
||||
updatedAttributes: Partial<CasesConfigureAttributes>;
|
||||
updatedAttributes: Partial<ESCasesConfigureAttributes>;
|
||||
}
|
||||
|
||||
export interface CaseConfigureServiceSetup {
|
||||
delete(args: GetCaseConfigureArgs): Promise<{}>;
|
||||
get(args: GetCaseConfigureArgs): Promise<SavedObject<CasesConfigureAttributes>>;
|
||||
find(args: FindCaseConfigureArgs): Promise<SavedObjectsFindResponse<CasesConfigureAttributes>>;
|
||||
get(args: GetCaseConfigureArgs): Promise<SavedObject<ESCasesConfigureAttributes>>;
|
||||
find(args: FindCaseConfigureArgs): Promise<SavedObjectsFindResponse<ESCasesConfigureAttributes>>;
|
||||
patch(
|
||||
args: PatchCaseConfigureArgs
|
||||
): Promise<SavedObjectsUpdateResponse<CasesConfigureAttributes>>;
|
||||
post(args: PostCaseConfigureArgs): Promise<SavedObject<CasesConfigureAttributes>>;
|
||||
): Promise<SavedObjectsUpdateResponse<ESCasesConfigureAttributes>>;
|
||||
post(args: PostCaseConfigureArgs): Promise<SavedObject<ESCasesConfigureAttributes>>;
|
||||
}
|
||||
|
||||
export class CaseConfigureService {
|
||||
|
|
|
@ -18,7 +18,12 @@ import {
|
|||
} from 'kibana/server';
|
||||
|
||||
import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server';
|
||||
import { CaseAttributes, CommentAttributes, SavedObjectFindOptions, User } from '../../common/api';
|
||||
import {
|
||||
ESCaseAttributes,
|
||||
CommentAttributes,
|
||||
SavedObjectFindOptions,
|
||||
User,
|
||||
} from '../../common/api';
|
||||
import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../saved_object_types';
|
||||
import { readReporters } from './reporters/read_reporters';
|
||||
import { readTags } from './tags/read_tags';
|
||||
|
@ -55,7 +60,7 @@ interface GetCommentArgs extends ClientArgs {
|
|||
}
|
||||
|
||||
interface PostCaseArgs extends ClientArgs {
|
||||
attributes: CaseAttributes;
|
||||
attributes: ESCaseAttributes;
|
||||
}
|
||||
|
||||
interface PostCommentArgs extends ClientArgs {
|
||||
|
@ -65,7 +70,7 @@ interface PostCommentArgs extends ClientArgs {
|
|||
|
||||
interface PatchCase {
|
||||
caseId: string;
|
||||
updatedAttributes: Partial<CaseAttributes & PushedArgs>;
|
||||
updatedAttributes: Partial<ESCaseAttributes & PushedArgs>;
|
||||
version?: string;
|
||||
}
|
||||
type PatchCaseArgs = PatchCase & ClientArgs;
|
||||
|
@ -100,18 +105,18 @@ interface CaseServiceDeps {
|
|||
export interface CaseServiceSetup {
|
||||
deleteCase(args: GetCaseArgs): Promise<{}>;
|
||||
deleteComment(args: GetCommentArgs): Promise<{}>;
|
||||
findCases(args: FindCasesArgs): Promise<SavedObjectsFindResponse<CaseAttributes>>;
|
||||
findCases(args: FindCasesArgs): Promise<SavedObjectsFindResponse<ESCaseAttributes>>;
|
||||
getAllCaseComments(args: FindCommentsArgs): Promise<SavedObjectsFindResponse<CommentAttributes>>;
|
||||
getCase(args: GetCaseArgs): Promise<SavedObject<CaseAttributes>>;
|
||||
getCases(args: GetCasesArgs): Promise<SavedObjectsBulkResponse<CaseAttributes>>;
|
||||
getCase(args: GetCaseArgs): Promise<SavedObject<ESCaseAttributes>>;
|
||||
getCases(args: GetCasesArgs): Promise<SavedObjectsBulkResponse<ESCaseAttributes>>;
|
||||
getComment(args: GetCommentArgs): Promise<SavedObject<CommentAttributes>>;
|
||||
getTags(args: ClientArgs): Promise<string[]>;
|
||||
getReporters(args: ClientArgs): Promise<User[]>;
|
||||
getUser(args: GetUserArgs): Promise<AuthenticatedUser | User>;
|
||||
postNewCase(args: PostCaseArgs): Promise<SavedObject<CaseAttributes>>;
|
||||
postNewCase(args: PostCaseArgs): Promise<SavedObject<ESCaseAttributes>>;
|
||||
postNewComment(args: PostCommentArgs): Promise<SavedObject<CommentAttributes>>;
|
||||
patchCase(args: PatchCaseArgs): Promise<SavedObjectsUpdateResponse<CaseAttributes>>;
|
||||
patchCases(args: PatchCasesArgs): Promise<SavedObjectsBulkUpdateResponse<CaseAttributes>>;
|
||||
patchCase(args: PatchCaseArgs): Promise<SavedObjectsUpdateResponse<ESCaseAttributes>>;
|
||||
patchCases(args: PatchCasesArgs): Promise<SavedObjectsBulkUpdateResponse<ESCaseAttributes>>;
|
||||
patchComment(args: UpdateCommentArgs): Promise<SavedObjectsUpdateResponse<CommentAttributes>>;
|
||||
patchComments(args: PatchComments): Promise<SavedObjectsBulkUpdateResponse<CommentAttributes>>;
|
||||
}
|
||||
|
|
|
@ -5,16 +5,20 @@
|
|||
*/
|
||||
|
||||
import { SavedObject, SavedObjectsUpdateResponse } from 'kibana/server';
|
||||
import { get } from 'lodash';
|
||||
import { get, isPlainObject, isString } from 'lodash';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import {
|
||||
CaseUserActionAttributes,
|
||||
UserAction,
|
||||
UserActionField,
|
||||
CaseAttributes,
|
||||
ESCaseAttributes,
|
||||
User,
|
||||
} from '../../../common/api';
|
||||
import { isTwoArraysDifference } from '../../routes/api/cases/helpers';
|
||||
import {
|
||||
isTwoArraysDifference,
|
||||
transformESConnectorToCaseConnector,
|
||||
} from '../../routes/api/cases/helpers';
|
||||
import { UserActionItem } from '.';
|
||||
import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types';
|
||||
|
||||
|
@ -120,7 +124,7 @@ export const buildCaseUserActionItem = ({
|
|||
|
||||
const userActionFieldsAllowed: UserActionField = [
|
||||
'comment',
|
||||
'connector_id',
|
||||
'connector',
|
||||
'description',
|
||||
'tags',
|
||||
'title',
|
||||
|
@ -135,8 +139,8 @@ export const buildCaseUserActions = ({
|
|||
}: {
|
||||
actionDate: string;
|
||||
actionBy: User;
|
||||
originalCases: Array<SavedObject<CaseAttributes>>;
|
||||
updatedCases: Array<SavedObjectsUpdateResponse<CaseAttributes>>;
|
||||
originalCases: Array<SavedObject<ESCaseAttributes>>;
|
||||
updatedCases: Array<SavedObjectsUpdateResponse<ESCaseAttributes>>;
|
||||
}): UserActionItem[] =>
|
||||
updatedCases.reduce<UserActionItem[]>((acc, updatedItem) => {
|
||||
const originalItem = originalCases.find((oItem) => oItem.id === updatedItem.id);
|
||||
|
@ -145,37 +149,17 @@ export const buildCaseUserActions = ({
|
|||
const updatedFields = Object.keys(updatedItem.attributes) as UserActionField;
|
||||
updatedFields.forEach((field) => {
|
||||
if (userActionFieldsAllowed.includes(field)) {
|
||||
const origValue = get(originalItem, ['attributes', field]);
|
||||
const updatedValue = get(updatedItem, ['attributes', field]);
|
||||
const compareValues = isTwoArraysDifference(origValue, updatedValue);
|
||||
if (compareValues != null) {
|
||||
if (compareValues.addedItems.length > 0) {
|
||||
userActions = [
|
||||
...userActions,
|
||||
buildCaseUserActionItem({
|
||||
action: 'add',
|
||||
actionAt: actionDate,
|
||||
actionBy,
|
||||
caseId: updatedItem.id,
|
||||
fields: [field],
|
||||
newValue: compareValues.addedItems.join(', '),
|
||||
}),
|
||||
];
|
||||
}
|
||||
if (compareValues.deletedItems.length > 0) {
|
||||
userActions = [
|
||||
...userActions,
|
||||
buildCaseUserActionItem({
|
||||
action: 'delete',
|
||||
actionAt: actionDate,
|
||||
actionBy,
|
||||
caseId: updatedItem.id,
|
||||
fields: [field],
|
||||
newValue: compareValues.deletedItems.join(', '),
|
||||
}),
|
||||
];
|
||||
}
|
||||
} else if (origValue !== updatedValue) {
|
||||
const origValue =
|
||||
field === 'connector' && originalItem.attributes.connector
|
||||
? transformESConnectorToCaseConnector(originalItem.attributes.connector)
|
||||
: get(originalItem, ['attributes', field]);
|
||||
|
||||
const updatedValue =
|
||||
field === 'connector' && updatedItem.attributes.connector
|
||||
? transformESConnectorToCaseConnector(updatedItem.attributes.connector)
|
||||
: get(updatedItem, ['attributes', field]);
|
||||
|
||||
if (isString(origValue) && isString(updatedValue) && origValue !== updatedValue) {
|
||||
userActions = [
|
||||
...userActions,
|
||||
buildCaseUserActionItem({
|
||||
|
@ -188,6 +172,52 @@ export const buildCaseUserActions = ({
|
|||
oldValue: origValue,
|
||||
}),
|
||||
];
|
||||
} else if (Array.isArray(origValue) && Array.isArray(updatedValue)) {
|
||||
const compareValues = isTwoArraysDifference(origValue, updatedValue);
|
||||
if (compareValues && compareValues.addedItems.length > 0) {
|
||||
userActions = [
|
||||
...userActions,
|
||||
buildCaseUserActionItem({
|
||||
action: 'add',
|
||||
actionAt: actionDate,
|
||||
actionBy,
|
||||
caseId: updatedItem.id,
|
||||
fields: [field],
|
||||
newValue: compareValues.addedItems.join(', '),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (compareValues && compareValues.deletedItems.length > 0) {
|
||||
userActions = [
|
||||
...userActions,
|
||||
buildCaseUserActionItem({
|
||||
action: 'delete',
|
||||
actionAt: actionDate,
|
||||
actionBy,
|
||||
caseId: updatedItem.id,
|
||||
fields: [field],
|
||||
newValue: compareValues.deletedItems.join(', '),
|
||||
}),
|
||||
];
|
||||
}
|
||||
} else if (
|
||||
isPlainObject(origValue) &&
|
||||
isPlainObject(updatedValue) &&
|
||||
!deepEqual(origValue, updatedValue)
|
||||
) {
|
||||
userActions = [
|
||||
...userActions,
|
||||
buildCaseUserActionItem({
|
||||
action: 'update',
|
||||
actionAt: actionDate,
|
||||
actionBy,
|
||||
caseId: updatedItem.id,
|
||||
fields: [field],
|
||||
newValue: JSON.stringify(updatedValue),
|
||||
oldValue: JSON.stringify(origValue),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,12 +15,13 @@ import { TestProviders } from '../../../common/mock';
|
|||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
import { useGetCase } from '../../containers/use_get_case';
|
||||
import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../containers/configure/mock';
|
||||
|
||||
import { usePostPushToService } from '../../containers/use_post_push_to_service';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
jest.mock('../../containers/use_update_case');
|
||||
jest.mock('../../containers/use_get_case_user_actions');
|
||||
|
@ -37,7 +38,15 @@ const usePostPushToServiceMock = usePostPushToService as jest.Mock;
|
|||
export const caseProps: CaseProps = {
|
||||
caseId: basicCase.id,
|
||||
userCanCrud: true,
|
||||
caseData: { ...basicCase, connectorId: 'servicenow-2' },
|
||||
caseData: {
|
||||
...basicCase,
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'Resilient',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: null,
|
||||
},
|
||||
},
|
||||
fetchCase: jest.fn(),
|
||||
updateCase: jest.fn(),
|
||||
};
|
||||
|
@ -275,7 +284,8 @@ describe('CaseView ', () => {
|
|||
.first()
|
||||
.exists()
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="tag-list-edit"]').first().exists()).toBeFalsy();
|
||||
|
||||
expect(wrapper.find('button[data-test-subj="tag-list-edit"]').first().exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -442,34 +452,108 @@ describe('CaseView ', () => {
|
|||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should revert to the initial connector in case of failure', async () => {
|
||||
// TO DO fix when the useEffects in edit_connector are cleaned up
|
||||
it.skip('should revert to the initial connector in case of failure', async () => {
|
||||
updateCaseProperty.mockImplementation(({ onError }) => {
|
||||
onError();
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<CaseComponent
|
||||
{...caseProps}
|
||||
caseData={{ ...caseProps.caseData, connectorId: 'servicenow-1' }}
|
||||
caseData={{
|
||||
...caseProps.caseData,
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'SN 1',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
const connectorName = wrapper
|
||||
.find('[data-test-subj="settings-connector-card"] .euiTitle')
|
||||
.first()
|
||||
.text();
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
const updateObject = updateCaseProperty.mock.calls[0][0];
|
||||
expect(updateObject.updateKey).toEqual('connector');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="dropdown-connectors"]').at(0).prop('valueOfSelected')
|
||||
).toBe('servicenow-1');
|
||||
wrapper.find('[data-test-subj="settings-connector-card"] .euiTitle').first().text()
|
||||
).toBe(connectorName);
|
||||
});
|
||||
});
|
||||
// TO DO fix when the useEffects in edit_connector are cleaned up
|
||||
it.skip('should update connector', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<CaseComponent
|
||||
{...caseProps}
|
||||
caseData={{
|
||||
...caseProps.caseData,
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'SN 1',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
const updateObject = updateCaseProperty.mock.calls[0][0];
|
||||
expect(updateObject.updateKey).toEqual('connector');
|
||||
expect(updateObject.updateValue).toEqual({
|
||||
id: 'resilient-2',
|
||||
name: 'My Connector 2',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: {
|
||||
incidentTypes: null,
|
||||
severityCode: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,10 +14,11 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { Case } from '../../containers/types';
|
||||
import { getCaseUrl } from '../../../common/components/link_to';
|
||||
import { Case, CaseConnector } from '../../containers/types';
|
||||
import { getCaseDetailsUrl, getCaseUrl, useFormatUrl } from '../../../common/components/link_to';
|
||||
import { gutterTimeline } from '../../../common/lib/helpers';
|
||||
import { HeaderPage } from '../../../common/components/header_page';
|
||||
import { EditableTitle } from '../../../common/components/header_page/editable_title';
|
||||
|
@ -26,18 +27,20 @@ import { useGetCase } from '../../containers/use_get_case';
|
|||
import { UserActionTree } from '../user_action_tree';
|
||||
import { UserList } from '../user_list';
|
||||
import { useUpdateCase } from '../../containers/use_update_case';
|
||||
import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search';
|
||||
import { getTypedPayload } from '../../containers/utils';
|
||||
import { WhitePageWrapper, HeaderWrapper } from '../wrappers';
|
||||
import { useBasePath } from '../../../common/lib/kibana';
|
||||
import { CaseStatus } from '../case_status';
|
||||
import { navTabs } from '../../../app/home/home_navigations';
|
||||
import { SpyRoute } from '../../../common/utils/route/spy_routes';
|
||||
import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions';
|
||||
import { usePushToService } from '../use_push_to_service';
|
||||
import { EditConnector } from '../edit_connector';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { SecurityPageName } from '../../../app/types';
|
||||
import {
|
||||
getConnectorById,
|
||||
normalizeActionConnector,
|
||||
getNoneConnector,
|
||||
} from '../configure_cases/utils';
|
||||
|
||||
interface Props {
|
||||
caseId: string;
|
||||
|
@ -77,10 +80,11 @@ export interface CaseProps extends Props {
|
|||
|
||||
export const CaseComponent = React.memo<CaseProps>(
|
||||
({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => {
|
||||
const basePath = window.location.origin + useBasePath();
|
||||
const caseLink = `${basePath}/app/security/cases/${caseId}`;
|
||||
const search = useGetUrlSearch(navTabs.case);
|
||||
const { formatUrl, search } = useFormatUrl(SecurityPageName.case);
|
||||
const allCasesLink = getCaseUrl(search);
|
||||
const caseDetailsLink = formatUrl(getCaseDetailsUrl({ id: caseId }), { absolute: true });
|
||||
const [initLoadingData, setInitLoadingData] = useState(true);
|
||||
|
||||
const {
|
||||
caseUserActions,
|
||||
fetchCaseUserActions,
|
||||
|
@ -88,7 +92,8 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
hasDataToPush,
|
||||
isLoading: isLoadingUserActions,
|
||||
participants,
|
||||
} = useGetCaseUserActions(caseId, caseData.connectorId);
|
||||
} = useGetCaseUserActions(caseId, caseData.connector.id);
|
||||
|
||||
const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({
|
||||
caseId,
|
||||
});
|
||||
|
@ -113,13 +118,13 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'connectorId':
|
||||
const connectorId = getTypedPayload<string>(value);
|
||||
if (connectorId.length > 0) {
|
||||
case 'connector':
|
||||
const connector = getTypedPayload<CaseConnector>(value);
|
||||
if (connector != null) {
|
||||
updateCaseProperty({
|
||||
fetchCaseUserActions,
|
||||
updateKey: 'connector_id',
|
||||
updateValue: connectorId,
|
||||
updateKey: 'connector',
|
||||
updateValue: connector,
|
||||
updateCase: handleUpdateNewCase,
|
||||
version: caseData.version,
|
||||
onSuccess,
|
||||
|
@ -172,6 +177,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
},
|
||||
[fetchCaseUserActions, updateCaseProperty, updateCase, caseData]
|
||||
);
|
||||
|
||||
const handleUpdateCase = useCallback(
|
||||
(newCase: Case) => {
|
||||
updateCase(newCase);
|
||||
|
@ -182,22 +188,24 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
|
||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||
|
||||
const [caseConnectorName, isValidConnector] = useMemo(() => {
|
||||
const connector = connectors.find((c) => c.id === caseData.connectorId);
|
||||
return [connector?.name ?? 'none', !!connector];
|
||||
}, [connectors, caseData.connectorId]);
|
||||
const [connectorName, isValidConnector] = useMemo(() => {
|
||||
const connector = connectors.find((c) => c.id === caseData.connector.id);
|
||||
return [connector?.name ?? '', !!connector];
|
||||
}, [connectors, caseData.connector]);
|
||||
|
||||
const currentExternalIncident = useMemo(
|
||||
() =>
|
||||
caseServices != null && caseServices[caseData.connectorId] != null
|
||||
? caseServices[caseData.connectorId]
|
||||
caseServices != null && caseServices[caseData.connector.id] != null
|
||||
? caseServices[caseData.connector.id]
|
||||
: null,
|
||||
[caseServices, caseData.connectorId]
|
||||
[caseServices, caseData.connector]
|
||||
);
|
||||
|
||||
const { pushButton, pushCallouts } = usePushToService({
|
||||
caseConnectorId: caseData.connectorId,
|
||||
caseConnectorName,
|
||||
connector: {
|
||||
...caseData.connector,
|
||||
name: isEmpty(caseData.connector.name) ? connectorName : caseData.connector.name,
|
||||
},
|
||||
caseServices,
|
||||
caseId: caseData.id,
|
||||
caseStatus: caseData.status,
|
||||
|
@ -208,22 +216,31 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
});
|
||||
|
||||
const onSubmitConnector = useCallback(
|
||||
(connectorId, onSuccess, onError) =>
|
||||
(connectorId, connectorFields, onError, onSuccess) => {
|
||||
const connector = getConnectorById(connectorId, connectors);
|
||||
const connectorToUpdate = connector
|
||||
? normalizeActionConnector(connector)
|
||||
: getNoneConnector();
|
||||
|
||||
onUpdateField({
|
||||
key: 'connectorId',
|
||||
value: connectorId,
|
||||
key: 'connector',
|
||||
value: { ...connectorToUpdate, fields: connectorFields },
|
||||
onSuccess,
|
||||
onError,
|
||||
}),
|
||||
[onUpdateField]
|
||||
});
|
||||
},
|
||||
[onUpdateField, connectors]
|
||||
);
|
||||
|
||||
const onSubmitTags = useCallback((newTags) => onUpdateField({ key: 'tags', value: newTags }), [
|
||||
onUpdateField,
|
||||
]);
|
||||
|
||||
const onSubmitTitle = useCallback(
|
||||
(newTitle) => onUpdateField({ key: 'title', value: newTitle }),
|
||||
[onUpdateField]
|
||||
);
|
||||
|
||||
const toggleStatusCase = useCallback(
|
||||
(e) =>
|
||||
onUpdateField({
|
||||
|
@ -232,6 +249,7 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
}),
|
||||
[onUpdateField]
|
||||
);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
fetchCaseUserActions(caseData.id);
|
||||
fetchCase();
|
||||
|
@ -264,12 +282,13 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
},
|
||||
[caseData.closedAt, caseData.createdAt, caseData.status]
|
||||
);
|
||||
|
||||
const emailContent = useMemo(
|
||||
() => ({
|
||||
subject: i18n.EMAIL_SUBJECT(caseData.title),
|
||||
body: i18n.EMAIL_BODY(caseLink),
|
||||
body: i18n.EMAIL_BODY(caseDetailsLink),
|
||||
}),
|
||||
[caseLink, caseData.title]
|
||||
[caseDetailsLink, caseData.title]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -280,12 +299,12 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
|
||||
const backOptions = useMemo(
|
||||
() => ({
|
||||
href: getCaseUrl(search),
|
||||
href: allCasesLink,
|
||||
text: i18n.BACK_TO_ALL,
|
||||
dataTestSubj: 'backToCases',
|
||||
pageId: SecurityPageName.case,
|
||||
}),
|
||||
[search]
|
||||
[allCasesLink]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -380,10 +399,13 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
isLoading={isLoading && updateKey === 'tags'}
|
||||
/>
|
||||
<EditConnector
|
||||
isLoading={isLoadingConnectors}
|
||||
onSubmit={onSubmitConnector}
|
||||
caseFields={caseData.connector.fields}
|
||||
connectors={connectors}
|
||||
selectedConnector={caseData.connectorId}
|
||||
disabled={!userCanCrud}
|
||||
isLoading={isLoadingConnectors || (isLoading && updateKey === 'connector')}
|
||||
onSubmit={onSubmitConnector}
|
||||
selectedConnector={caseData.connector.id}
|
||||
userActions={caseUserActions}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -151,6 +151,14 @@ export const EMAIL_BODY = (caseUrl: string) =>
|
|||
values: { caseUrl },
|
||||
defaultMessage: 'Case reference: {caseUrl}',
|
||||
});
|
||||
|
||||
export const UNKNOWN = i18n.translate('xpack.securitySolution.case.caseView.unknown', {
|
||||
defaultMessage: 'Unknown',
|
||||
});
|
||||
|
||||
export const CHANGED_CONNECTOR_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.case.caseView.fieldChanged',
|
||||
{
|
||||
defaultMessage: `changed connector field`,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Connector } from '../../../containers/configure/types';
|
||||
import { ReturnConnectors } from '../../../containers/configure/use_connectors';
|
||||
import { ActionConnector } from '../../../containers/configure/types';
|
||||
import { UseConnectorsResponse } from '../../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../../containers/configure/mock';
|
||||
import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure';
|
||||
export { mapping } from '../../../containers/configure/mock';
|
||||
import { ConnectorTypes } from '../../../../../../case/common/api';
|
||||
|
||||
export const connectors: Connector[] = connectorsMock;
|
||||
export const connectors: ActionConnector[] = connectorsMock;
|
||||
|
||||
// x - pack / plugins / triggers_actions_ui;
|
||||
export const searchURL =
|
||||
|
@ -18,12 +19,20 @@ export const searchURL =
|
|||
|
||||
export const useCaseConfigureResponse: ReturnUseCaseConfigure = {
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'none',
|
||||
connectorName: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorId: 'none',
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
connectorName: 'none',
|
||||
},
|
||||
firstLoad: false,
|
||||
loading: false,
|
||||
|
@ -38,7 +47,7 @@ export const useCaseConfigureResponse: ReturnUseCaseConfigure = {
|
|||
version: '',
|
||||
};
|
||||
|
||||
export const useConnectorsResponse: ReturnConnectors = {
|
||||
export const useConnectorsResponse: UseConnectorsResponse = {
|
||||
loading: false,
|
||||
connectors,
|
||||
refetchConnectors: jest.fn(),
|
||||
|
|
|
@ -58,10 +58,10 @@ describe('Connectors', () => {
|
|||
|
||||
test('the connector is changed successfully', () => {
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
|
||||
expect(onChangeConnector).toHaveBeenCalled();
|
||||
expect(onChangeConnector).toHaveBeenCalledWith('servicenow-2');
|
||||
expect(onChangeConnector).toHaveBeenCalledWith('resilient-2');
|
||||
});
|
||||
|
||||
test('the connector is changed successfully to none', () => {
|
||||
|
|
|
@ -18,7 +18,7 @@ import styled from 'styled-components';
|
|||
import { ConnectorsDropdown } from './connectors_dropdown';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { Connector } from '../../containers/configure/types';
|
||||
import { ActionConnector } from '../../containers/configure/types';
|
||||
|
||||
const EuiFormRowExtended = styled(EuiFormRow)`
|
||||
.euiFormRow__labelWrapper {
|
||||
|
@ -29,7 +29,7 @@ const EuiFormRowExtended = styled(EuiFormRow)`
|
|||
`;
|
||||
|
||||
export interface Props {
|
||||
connectors: Connector[];
|
||||
connectors: ActionConnector[];
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
updateConnectorDisabled: boolean;
|
||||
|
|
|
@ -44,8 +44,8 @@ describe('ConnectorsDropdown', () => {
|
|||
'data-test-subj': 'dropdown-connector-servicenow-1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'servicenow-2',
|
||||
'data-test-subj': 'dropdown-connector-servicenow-2',
|
||||
value: 'resilient-2',
|
||||
'data-test-subj': 'dropdown-connector-resilient-2',
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
|
|
@ -8,12 +8,12 @@ import React, { useMemo } from 'react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Connector } from '../../containers/configure/types';
|
||||
import { ActionConnector } from '../../containers/configure/types';
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface Props {
|
||||
connectors: Connector[];
|
||||
connectors: ActionConnector[];
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
onChange: (id: string) => void;
|
||||
|
@ -96,13 +96,14 @@ const ConnectorsDropdownComponent: React.FC<Props> = ({
|
|||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
aria-label={i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL}
|
||||
data-test-subj="dropdown-connectors"
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
isLoading={isLoading}
|
||||
onChange={onChange}
|
||||
options={connectorsAsOptions}
|
||||
valueOfSelected={selectedConnector}
|
||||
fullWidth
|
||||
onChange={onChange}
|
||||
data-test-subj="dropdown-connectors"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ import { useCaseConfigure } from '../../containers/configure/use_configure';
|
|||
import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search';
|
||||
|
||||
import { connectors, searchURL, useCaseConfigureResponse, useConnectorsResponse } from './__mock__';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
|
@ -90,11 +91,19 @@ describe('ConfigureCases', () => {
|
|||
useCaseConfigureMock.mockImplementation(() => ({
|
||||
...useCaseConfigureResponse,
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'not-id',
|
||||
connectorName: 'unchanged',
|
||||
connector: {
|
||||
id: 'not-id',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorName: 'unchanged',
|
||||
connectorId: 'not-id',
|
||||
connector: {
|
||||
id: 'not-id',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
},
|
||||
}));
|
||||
|
@ -126,11 +135,19 @@ describe('ConfigureCases', () => {
|
|||
...useCaseConfigureResponse,
|
||||
mapping: connectors[0].config.incidentConfiguration.mapping,
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'servicenow-1',
|
||||
connectorName: 'unchanged',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorName: 'unchanged',
|
||||
connectorId: 'servicenow-1',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
},
|
||||
}));
|
||||
|
@ -213,11 +230,19 @@ describe('ConfigureCases', () => {
|
|||
...useCaseConfigureResponse,
|
||||
mapping: connectors[1].config.incidentConfiguration.mapping,
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'servicenow-2',
|
||||
connectorName: 'unchanged',
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorName: 'unchanged',
|
||||
connectorId: 'servicenow-1',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
},
|
||||
}));
|
||||
|
@ -258,7 +283,12 @@ describe('ConfigureCases', () => {
|
|||
beforeEach(() => {
|
||||
useCaseConfigureMock.mockImplementation(() => ({
|
||||
...useCaseConfigureResponse,
|
||||
connectorId: 'servicenow-1',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'SN',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
persistLoading: true,
|
||||
}));
|
||||
|
||||
|
@ -327,11 +357,19 @@ describe('ConfigureCases', () => {
|
|||
...useCaseConfigureResponse,
|
||||
mapping: connectors[0].config.incidentConfiguration.mapping,
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'servicenow-1',
|
||||
connectorName: 'My connector',
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorName: 'My connector',
|
||||
connectorId: 'My connector',
|
||||
connector: {
|
||||
id: 'My connector',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
},
|
||||
persistCaseConfigure,
|
||||
|
@ -345,13 +383,17 @@ describe('ConfigureCases', () => {
|
|||
test('it submits the configuration correctly when changing connector', () => {
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(persistCaseConfigure).toHaveBeenCalled();
|
||||
expect(persistCaseConfigure).toHaveBeenCalledWith({
|
||||
connectorId: 'servicenow-2',
|
||||
connectorName: 'My Connector 2',
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'My Connector 2',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
});
|
||||
});
|
||||
|
@ -360,18 +402,28 @@ describe('ConfigureCases', () => {
|
|||
useCaseConfigureMock
|
||||
.mockImplementationOnce(() => ({
|
||||
...useCaseConfigureResponse,
|
||||
connectorId: 'servicenow-1',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
}))
|
||||
.mockImplementation(() => ({
|
||||
...useCaseConfigureResponse,
|
||||
connectorId: 'servicenow-2',
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'My connector 2',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: null,
|
||||
},
|
||||
}));
|
||||
|
||||
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
|
||||
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
|
@ -393,11 +445,19 @@ describe('closure options', () => {
|
|||
...useCaseConfigureResponse,
|
||||
mapping: connectors[0].config.incidentConfiguration.mapping,
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'servicenow-1',
|
||||
connectorName: 'My connector',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorName: 'My connector',
|
||||
connectorId: 'My connector',
|
||||
connector: {
|
||||
id: 'My connector',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.jira,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
},
|
||||
persistCaseConfigure,
|
||||
|
@ -414,8 +474,12 @@ describe('closure options', () => {
|
|||
|
||||
expect(persistCaseConfigure).toHaveBeenCalled();
|
||||
expect(persistCaseConfigure).toHaveBeenCalledWith({
|
||||
connectorId: 'servicenow-1',
|
||||
connectorName: 'My Connector',
|
||||
connector: {
|
||||
id: 'servicenow-1',
|
||||
name: 'My connector',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-pushing',
|
||||
});
|
||||
});
|
||||
|
@ -427,11 +491,19 @@ describe('user interactions', () => {
|
|||
...useCaseConfigureResponse,
|
||||
mapping: connectors[1].config.incidentConfiguration.mapping,
|
||||
closureType: 'close-by-user',
|
||||
connectorId: 'servicenow-2',
|
||||
connectorName: 'unchanged',
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.resilient,
|
||||
fields: null,
|
||||
},
|
||||
currentConfiguration: {
|
||||
connectorName: 'unchanged',
|
||||
connectorId: 'servicenow-2',
|
||||
connector: {
|
||||
id: 'resilient-2',
|
||||
name: 'unchanged',
|
||||
type: ConnectorTypes.servicenow,
|
||||
fields: null,
|
||||
},
|
||||
closureType: 'close-by-user',
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -25,9 +25,15 @@ import { ClosureType } from '../../containers/configure/types';
|
|||
import { ActionConnectorTableItem } from '../../../../../triggers_actions_ui/public/types';
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
|
||||
import { SectionWrapper } from '../wrappers';
|
||||
import { Connectors } from './connectors';
|
||||
import { ClosureOptions } from './closure_options';
|
||||
import { SectionWrapper } from '../wrappers';
|
||||
import {
|
||||
getConnectorById,
|
||||
getNoneConnector,
|
||||
normalizeActionConnector,
|
||||
normalizeCaseConnector,
|
||||
} from './utils';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
|
@ -65,12 +71,10 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC
|
|||
);
|
||||
|
||||
const {
|
||||
connectorId,
|
||||
connector,
|
||||
closureType,
|
||||
currentConfiguration,
|
||||
loading: loadingCaseConfigure,
|
||||
persistLoading,
|
||||
version,
|
||||
persistCaseConfigure,
|
||||
setConnector,
|
||||
setClosureType,
|
||||
|
@ -83,7 +87,7 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const reloadConnectors = useCallback(async () => refetchConnectors(), []);
|
||||
const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure;
|
||||
const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none';
|
||||
const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none';
|
||||
|
||||
const onClickUpdateConnector = useCallback(() => {
|
||||
setEditFlyoutVisibility(true);
|
||||
|
@ -93,16 +97,14 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC
|
|||
(isVisible: boolean) => {
|
||||
setAddFlyoutVisibility(isVisible);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[currentConfiguration, connectorId, closureType]
|
||||
[setAddFlyoutVisibility]
|
||||
);
|
||||
|
||||
const handleSetEditFlyoutVisibility = useCallback(
|
||||
(isVisible: boolean) => {
|
||||
setEditFlyoutVisibility(isVisible);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[currentConfiguration, connectorId, closureType]
|
||||
[setEditFlyoutVisibility]
|
||||
);
|
||||
|
||||
const onChangeConnector = useCallback(
|
||||
|
@ -112,54 +114,52 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC
|
|||
return;
|
||||
}
|
||||
|
||||
setConnector(id);
|
||||
const actionConnector = getConnectorById(id, connectors);
|
||||
const caseConnector =
|
||||
actionConnector != null ? normalizeActionConnector(actionConnector) : getNoneConnector();
|
||||
|
||||
setConnector(caseConnector);
|
||||
persistCaseConfigure({
|
||||
connectorId: id,
|
||||
connectorName: connectors.find((c) => c.id === id)?.name ?? '',
|
||||
connector: caseConnector,
|
||||
closureType,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[connectorId, closureType, version]
|
||||
[connectors, closureType, persistCaseConfigure, setConnector]
|
||||
);
|
||||
|
||||
const onChangeClosureType = useCallback(
|
||||
(type: ClosureType) => {
|
||||
setClosureType(type);
|
||||
persistCaseConfigure({
|
||||
connectorId,
|
||||
connectorName: connectors.find((c) => c.id === connectorId)?.name ?? '',
|
||||
connector,
|
||||
closureType: type,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[connectorId, closureType, version]
|
||||
[connector, persistCaseConfigure, setClosureType]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isLoadingConnectors &&
|
||||
connectorId !== 'none' &&
|
||||
!connectors.some((c) => c.id === connectorId)
|
||||
connector.id !== 'none' &&
|
||||
!connectors.some((c) => c.id === connector.id)
|
||||
) {
|
||||
setConnectorIsValid(false);
|
||||
} else if (
|
||||
!isLoadingConnectors &&
|
||||
(connectorId === 'none' || connectors.some((c) => c.id === connectorId))
|
||||
(connector.id === 'none' || connectors.some((c) => c.id === connector.id))
|
||||
) {
|
||||
setConnectorIsValid(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connectors, connectorId]);
|
||||
}, [connectors, connector, isLoadingConnectors]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingConnectors && connectorId !== 'none') {
|
||||
if (!isLoadingConnectors && connector.id !== 'none') {
|
||||
setEditedConnectorItem(
|
||||
connectors.find((c) => c.id === connectorId) as ActionConnectorTableItem
|
||||
normalizeCaseConnector(connectors, connector) as ActionConnectorTableItem
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connectors, connectorId]);
|
||||
}, [connectors, connector, isLoadingConnectors]);
|
||||
|
||||
return (
|
||||
<FormWrapper>
|
||||
|
@ -190,7 +190,7 @@ const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userC
|
|||
onChangeConnector={onChangeConnector}
|
||||
updateConnectorDisabled={updateConnectorDisabled || !userCanCrud}
|
||||
handleShowEditFlyout={onClickUpdateConnector}
|
||||
selectedConnector={connectorId}
|
||||
selectedConnector={connector.id}
|
||||
/>
|
||||
</SectionWrapper>
|
||||
<ActionsConnectorsContextProvider
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export * from '../../translations';
|
||||
|
||||
export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.case.configureCases.incidentManagementSystemTitle',
|
||||
{
|
||||
|
@ -28,13 +30,6 @@ export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const NO_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.case.configureCases.noConnector',
|
||||
{
|
||||
defaultMessage: 'No connector selected',
|
||||
}
|
||||
);
|
||||
|
||||
export const ADD_NEW_CONNECTOR = i18n.translate(
|
||||
'xpack.securitySolution.case.configureCases.addNewConnector',
|
||||
{
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ConnectorTypeFields, ConnectorTypes } from '../../../../../case/common/api';
|
||||
import {
|
||||
CaseField,
|
||||
ActionType,
|
||||
CasesConfigurationMapping,
|
||||
ThirdPartyField,
|
||||
ActionConnector,
|
||||
CaseConnector,
|
||||
} from '../../containers/configure/types';
|
||||
|
||||
export const setActionTypeToMapping = (
|
||||
|
@ -41,3 +44,35 @@ export const setThirdPartyToMapping = (
|
|||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
export const getNoneConnector = (): CaseConnector => ({
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
fields: null,
|
||||
});
|
||||
|
||||
export const getConnectorById = (
|
||||
id: string,
|
||||
connectors: ActionConnector[]
|
||||
): ActionConnector | null => connectors.find((c) => c.id === id) ?? null;
|
||||
|
||||
export const normalizeActionConnector = (
|
||||
actionConnector: ActionConnector,
|
||||
fields: CaseConnector['fields'] = null
|
||||
): CaseConnector => {
|
||||
const caseConnectorFieldsType = {
|
||||
type: actionConnector.actionTypeId,
|
||||
fields,
|
||||
} as ConnectorTypeFields;
|
||||
return {
|
||||
id: actionConnector.id,
|
||||
name: actionConnector.name,
|
||||
...caseConnectorFieldsType,
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeCaseConnector = (
|
||||
connectors: ActionConnector[],
|
||||
caseConnector: CaseConnector
|
||||
): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null;
|
||||
|
|
|
@ -9,24 +9,26 @@ import React, { useCallback, useEffect } from 'react';
|
|||
|
||||
import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
|
||||
import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown';
|
||||
import { Connector } from '../../../../../case/common/api/cases';
|
||||
import { ActionConnector } from '../../../../../case/common/api/cases';
|
||||
|
||||
interface ConnectorSelectorProps {
|
||||
connectors: Connector[];
|
||||
connectors: ActionConnector[];
|
||||
dataTestSubj: string;
|
||||
defaultValue?: ActionConnector;
|
||||
disabled: boolean;
|
||||
field: FieldHook;
|
||||
idAria: string;
|
||||
defaultValue?: string;
|
||||
disabled: boolean;
|
||||
isEdit: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
export const ConnectorSelector = ({
|
||||
connectors,
|
||||
dataTestSubj,
|
||||
defaultValue,
|
||||
disabled = false,
|
||||
field,
|
||||
idAria,
|
||||
disabled = false,
|
||||
isEdit = true,
|
||||
isLoading = false,
|
||||
}: ConnectorSelectorProps) => {
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
@ -37,13 +39,13 @@ export const ConnectorSelector = ({
|
|||
}, [defaultValue]);
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(newContent: string) => {
|
||||
field.setValue(newContent);
|
||||
(newConnector: string) => {
|
||||
field.setValue(newConnector);
|
||||
},
|
||||
[field]
|
||||
);
|
||||
|
||||
return (
|
||||
return isEdit ? (
|
||||
<EuiFormRow
|
||||
data-test-subj={dataTestSubj}
|
||||
describedByIds={idAria ? [idAria] : undefined}
|
||||
|
@ -56,11 +58,11 @@ export const ConnectorSelector = ({
|
|||
>
|
||||
<ConnectorsDropdown
|
||||
connectors={connectors}
|
||||
selectedConnector={field.value as string}
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onChange={handleContentChange}
|
||||
selectedConnector={(field.value as string) ?? 'none'}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,9 @@ import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/fo
|
|||
import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data';
|
||||
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { connectorsMock } from '../../containers/configure/mock';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
|
@ -40,6 +43,7 @@ jest.mock(
|
|||
);
|
||||
|
||||
jest.mock('../../containers/use_get_tags');
|
||||
jest.mock('../../containers/configure/use_connectors');
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider',
|
||||
() => ({
|
||||
|
@ -47,7 +51,7 @@ jest.mock(
|
|||
children({ tags: ['rad', 'dude'] }),
|
||||
})
|
||||
);
|
||||
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const useFormMock = useForm as jest.Mock;
|
||||
const useFormDataMock = useFormData as jest.Mock;
|
||||
|
||||
|
@ -72,6 +76,12 @@ const sampleData = {
|
|||
description: 'what a great description',
|
||||
tags: sampleTags,
|
||||
title: 'what a cool title',
|
||||
connector: {
|
||||
fields: null,
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: ConnectorTypes.none,
|
||||
},
|
||||
};
|
||||
const defaultPostCase = {
|
||||
isLoading: false,
|
||||
|
@ -79,6 +89,7 @@ const defaultPostCase = {
|
|||
caseData: null,
|
||||
postCase,
|
||||
};
|
||||
const sampleConnectorData = { loading: false, connectors: [] };
|
||||
describe('Create case', () => {
|
||||
const fetchTags = jest.fn();
|
||||
const formHookMock = getFormMock(sampleData);
|
||||
|
@ -87,7 +98,12 @@ describe('Create case', () => {
|
|||
useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
|
||||
usePostCaseMock.mockImplementation(() => defaultPostCase);
|
||||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
useFormDataMock.mockImplementation(() => [{ description: sampleData.description }]);
|
||||
useFormDataMock.mockImplementation(() => [
|
||||
{
|
||||
description: sampleData.description,
|
||||
},
|
||||
]);
|
||||
useConnectorsMock.mockReturnValue(sampleConnectorData);
|
||||
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
|
||||
(useGetTags as jest.Mock).mockImplementation(() => ({
|
||||
tags: sampleTags,
|
||||
|
@ -95,63 +111,122 @@ describe('Create case', () => {
|
|||
}));
|
||||
});
|
||||
|
||||
it('should post case on submit click', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
|
||||
});
|
||||
describe('Step 1 - Case Fields', () => {
|
||||
it('should post case on submit click', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
|
||||
});
|
||||
|
||||
it('should redirect to all cases on cancel click', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
|
||||
expect(mockHistory.push).toHaveBeenCalledWith('/');
|
||||
});
|
||||
it('should redirect to new case when caseData is there', () => {
|
||||
const sampleId = '777777';
|
||||
usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } }));
|
||||
mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/777777');
|
||||
});
|
||||
it('should redirect to all cases on cancel click', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
|
||||
await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/'));
|
||||
});
|
||||
it('should redirect to new case when caseData is there', async () => {
|
||||
const sampleId = '777777';
|
||||
usePostCaseMock.mockImplementation(() => ({
|
||||
...defaultPostCase,
|
||||
caseData: { id: sampleId },
|
||||
}));
|
||||
mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/777777'));
|
||||
});
|
||||
|
||||
it('should render spinner when loading', () => {
|
||||
usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true }));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy();
|
||||
it('should render spinner when loading', async () => {
|
||||
usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true }));
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy()
|
||||
);
|
||||
});
|
||||
it('Tag options render with new tags added', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`)
|
||||
.first()
|
||||
.prop('options')
|
||||
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }])
|
||||
);
|
||||
});
|
||||
});
|
||||
it('Tag options render with new tags added', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`).first().prop('options')
|
||||
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
|
||||
describe('Step 2 - Connector Fields', () => {
|
||||
const connectorTypes = [
|
||||
{
|
||||
label: 'Jira',
|
||||
testId: 'jira-1',
|
||||
dataTestSubj: 'connector-settings-jira',
|
||||
},
|
||||
{
|
||||
label: 'Resilient',
|
||||
testId: 'resilient-2',
|
||||
dataTestSubj: 'connector-settings-resilient',
|
||||
},
|
||||
{
|
||||
label: 'ServiceNow',
|
||||
testId: 'servicenow-1',
|
||||
dataTestSubj: 'connector-settings-sn',
|
||||
},
|
||||
];
|
||||
connectorTypes.forEach(({ label, testId, dataTestSubj }) => {
|
||||
it(`should change from none to ${label} connector fields`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Create />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-${testId}"]`).simulate('click');
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="${dataTestSubj}"]`).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -11,44 +11,55 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPanel,
|
||||
EuiSteps,
|
||||
} from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { isEqual } from 'lodash/fp';
|
||||
import { CasePostRequest } from '../../../../../case/common/api';
|
||||
|
||||
import {
|
||||
Field,
|
||||
Form,
|
||||
getUseField,
|
||||
useForm,
|
||||
UseField,
|
||||
FormDataProvider,
|
||||
getUseField,
|
||||
UseField,
|
||||
useForm,
|
||||
useFormData,
|
||||
} from '../../../shared_imports';
|
||||
import { usePostCase } from '../../containers/use_post_case';
|
||||
import { schema } from './schema';
|
||||
import { schema, FormProps } from './schema';
|
||||
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
|
||||
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
|
||||
import * as i18n from '../../translations';
|
||||
import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
|
||||
import { useGetTags } from '../../containers/use_get_tags';
|
||||
import { getCaseDetailsUrl } from '../../../common/components/link_to';
|
||||
import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
|
||||
import { SettingFieldsForm } from '../settings/fields_form';
|
||||
import { useConnectors } from '../../containers/configure/use_connectors';
|
||||
import { ConnectorSelector } from '../connector_selector/form';
|
||||
import { useCaseConfigure } from '../../containers/configure/use_configure';
|
||||
import {
|
||||
normalizeCaseConnector,
|
||||
getConnectorById,
|
||||
getNoneConnector,
|
||||
normalizeActionConnector,
|
||||
} from '../configure_cases/utils';
|
||||
import { ActionConnector } from '../../containers/types';
|
||||
import { ConnectorFields } from '../../../../../case/common/api/connectors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const CommonUseField = getUseField({ component: Field });
|
||||
|
||||
const ContainerBig = styled.div`
|
||||
${({ theme }) => css`
|
||||
margin-top: ${theme.eui.euiSizeXL};
|
||||
interface ContainerProps {
|
||||
big?: boolean;
|
||||
}
|
||||
|
||||
const Container = styled.div.attrs((props) => props)<ContainerProps>`
|
||||
${({ big, theme }) => css`
|
||||
margin-top: ${big ? theme.eui.euiSizeXL : theme.eui.euiSize};
|
||||
`}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
${({ theme }) => css`
|
||||
margin-top: ${theme.eui.euiSize};
|
||||
`}
|
||||
`;
|
||||
const MySpinner = styled(EuiLoadingSpinner)`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
@ -56,32 +67,29 @@ const MySpinner = styled(EuiLoadingSpinner)`
|
|||
z-index: 99;
|
||||
`;
|
||||
|
||||
const initialCaseValue: CasePostRequest = {
|
||||
const initialCaseValue: FormProps = {
|
||||
description: '',
|
||||
tags: [],
|
||||
title: '',
|
||||
connectorId: 'none',
|
||||
};
|
||||
|
||||
export const Create = React.memo(() => {
|
||||
const history = useHistory();
|
||||
const { caseData, isLoading, postCase } = usePostCase();
|
||||
const { form } = useForm<CasePostRequest>({
|
||||
defaultValue: initialCaseValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
|
||||
const fieldName = 'description';
|
||||
const { submit, setFieldValue } = form;
|
||||
const [{ description }] = useFormData({ form, watch: [fieldName] });
|
||||
|
||||
const { loading: isLoadingConnectors, connectors } = useConnectors();
|
||||
const { connector: configureConnector, loading: isLoadingCaseConfigure } = useCaseConfigure();
|
||||
const { tags: tagOptions } = useGetTags();
|
||||
|
||||
const [connector, setConnector] = useState<ActionConnector | null>(null);
|
||||
const [options, setOptions] = useState(
|
||||
tagOptions.map((label) => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
|
||||
// This values uses useEffect to update, not useMemo,
|
||||
// because we need to setState on it from the jsx
|
||||
useEffect(
|
||||
() =>
|
||||
setOptions(
|
||||
|
@ -92,7 +100,39 @@ export const Create = React.memo(() => {
|
|||
[tagOptions]
|
||||
);
|
||||
|
||||
const onDescriptionChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
|
||||
const [fields, setFields] = useState<ConnectorFields>(null);
|
||||
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialCaseValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
const currentConnectorId = useMemo(
|
||||
() =>
|
||||
!isLoadingCaseConfigure
|
||||
? normalizeCaseConnector(connectors, configureConnector)?.id ?? 'none'
|
||||
: null,
|
||||
[configureConnector, connectors, isLoadingCaseConfigure]
|
||||
);
|
||||
const { submit, setFieldValue } = form;
|
||||
const [{ description }] = useFormData<{
|
||||
description: string;
|
||||
}>({
|
||||
form,
|
||||
watch: ['description'],
|
||||
});
|
||||
const onChangeConnector = useCallback(
|
||||
(newConnectorId) => {
|
||||
if (connector == null || connector.id !== newConnectorId) {
|
||||
setConnector(getConnectorById(newConnectorId, connectors) ?? null);
|
||||
// Reset setting fields when changing connector
|
||||
setFields(null);
|
||||
}
|
||||
},
|
||||
[connector, connectors]
|
||||
);
|
||||
|
||||
const onDescriptionChange = useCallback((newValue) => setFieldValue('description', newValue), [
|
||||
setFieldValue,
|
||||
]);
|
||||
|
||||
|
@ -106,15 +146,145 @@ export const Create = React.memo(() => {
|
|||
const onSubmit = useCallback(async () => {
|
||||
const { isValid, data } = await submit();
|
||||
if (isValid) {
|
||||
// `postCase`'s type is incorrect, it actually returns a promise
|
||||
await postCase(data);
|
||||
const { connectorId: dataConnectorId, ...dataWithoutConnectorId } = data;
|
||||
const caseConnector = getConnectorById(dataConnectorId, connectors);
|
||||
const connectorToUpdate = caseConnector
|
||||
? normalizeActionConnector(caseConnector, fields)
|
||||
: getNoneConnector();
|
||||
|
||||
await postCase({ ...dataWithoutConnectorId, connector: connectorToUpdate });
|
||||
}
|
||||
}, [submit, postCase]);
|
||||
}, [submit, postCase, fields, connectors]);
|
||||
|
||||
const handleSetIsCancel = useCallback(() => {
|
||||
history.push('/');
|
||||
}, [history]);
|
||||
|
||||
const firstStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_ONE_TITLE,
|
||||
children: (
|
||||
<>
|
||||
<CommonUseField
|
||||
path="title"
|
||||
componentProps={{
|
||||
idAria: 'caseTitle',
|
||||
'data-test-subj': 'caseTitle',
|
||||
euiFieldProps: {
|
||||
fullWidth: false,
|
||||
disabled: isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Container>
|
||||
<CommonUseField
|
||||
path="tags"
|
||||
componentProps={{
|
||||
idAria: 'caseTags',
|
||||
'data-test-subj': 'caseTags',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
placeholder: '',
|
||||
disabled: isLoading,
|
||||
options,
|
||||
noSuggestions: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FormDataProvider pathsToWatch="tags">
|
||||
{({ tags: anotherTags }) => {
|
||||
const current: string[] = options.map((opt) => opt.label);
|
||||
const newOptions = anotherTags.reduce((acc: string[], item: string) => {
|
||||
if (!acc.includes(item)) {
|
||||
return [...acc, item];
|
||||
}
|
||||
return acc;
|
||||
}, current);
|
||||
if (!isEqual(current, newOptions)) {
|
||||
setOptions(
|
||||
newOptions.map((label: string) => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
</Container>
|
||||
<Container big>
|
||||
<UseField
|
||||
path={'description'}
|
||||
component={MarkdownEditorForm}
|
||||
componentProps={{
|
||||
dataTestSubj: 'caseDescription',
|
||||
idAria: 'caseDescription',
|
||||
isDisabled: isLoading,
|
||||
onClickTimeline: handleTimelineClick,
|
||||
onCursorPositionUpdate: handleCursorChange,
|
||||
topRightContent: (
|
||||
<InsertTimelinePopover
|
||||
hideUntitled={true}
|
||||
isDisabled={isLoading}
|
||||
onTimelineChange={handleOnTimelineChange}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
[isLoading, options, handleCursorChange, handleTimelineClick, handleOnTimelineChange]
|
||||
);
|
||||
|
||||
const secondStep = useMemo(
|
||||
() => ({
|
||||
title: i18n.STEP_TWO_TITLE,
|
||||
children: (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<Container>
|
||||
<UseField
|
||||
path="connectorId"
|
||||
component={ConnectorSelector}
|
||||
componentProps={{
|
||||
connectors,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
defaultValue: currentConnectorId,
|
||||
disabled: isLoadingConnectors,
|
||||
idAria: 'caseConnectors',
|
||||
isLoading,
|
||||
}}
|
||||
onChange={onChangeConnector}
|
||||
/>
|
||||
</Container>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Container>
|
||||
<SettingFieldsForm
|
||||
connector={connector}
|
||||
fields={fields}
|
||||
isEdit={true}
|
||||
onChange={setFields}
|
||||
/>
|
||||
</Container>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}),
|
||||
[
|
||||
connector,
|
||||
connectors,
|
||||
currentConnectorId,
|
||||
fields,
|
||||
isLoading,
|
||||
isLoadingConnectors,
|
||||
onChangeConnector,
|
||||
]
|
||||
);
|
||||
|
||||
const allSteps = useMemo(() => [firstStep, secondStep], [firstStep, secondStep]);
|
||||
|
||||
if (caseData != null && caseData.id) {
|
||||
history.push(getCaseDetailsUrl({ id: caseData.id }));
|
||||
return null;
|
||||
|
@ -124,72 +294,7 @@ export const Create = React.memo(() => {
|
|||
<EuiPanel>
|
||||
{isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />}
|
||||
<Form form={form}>
|
||||
<CommonUseField
|
||||
path="title"
|
||||
componentProps={{
|
||||
idAria: 'caseTitle',
|
||||
'data-test-subj': 'caseTitle',
|
||||
euiFieldProps: {
|
||||
fullWidth: false,
|
||||
disabled: isLoading,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Container>
|
||||
<CommonUseField
|
||||
path="tags"
|
||||
componentProps={{
|
||||
idAria: 'caseTags',
|
||||
'data-test-subj': 'caseTags',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
placeholder: '',
|
||||
disabled: isLoading,
|
||||
options,
|
||||
noSuggestions: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
<ContainerBig>
|
||||
<UseField
|
||||
path={fieldName}
|
||||
component={MarkdownEditorForm}
|
||||
componentProps={{
|
||||
dataTestSubj: 'caseDescription',
|
||||
idAria: 'caseDescription',
|
||||
isDisabled: isLoading,
|
||||
onClickTimeline: handleTimelineClick,
|
||||
onCursorPositionUpdate: handleCursorChange,
|
||||
topRightContent: (
|
||||
<InsertTimelinePopover
|
||||
hideUntitled={true}
|
||||
isDisabled={isLoading}
|
||||
onTimelineChange={handleOnTimelineChange}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ContainerBig>
|
||||
<FormDataProvider pathsToWatch="tags">
|
||||
{({ tags: anotherTags }) => {
|
||||
const current: string[] = options.map((opt) => opt.label);
|
||||
const newOptions = anotherTags.reduce((acc: string[], item: string) => {
|
||||
if (!acc.includes(item)) {
|
||||
return [...acc, item];
|
||||
}
|
||||
return acc;
|
||||
}, current);
|
||||
if (!isEqual(current, newOptions)) {
|
||||
setOptions(
|
||||
newOptions.map((label: string) => ({
|
||||
label,
|
||||
}))
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
</FormDataProvider>
|
||||
<EuiSteps headingElement="h2" steps={allSteps} />
|
||||
</Form>
|
||||
<Container>
|
||||
<EuiFlexGroup
|
||||
|
|
|
@ -18,7 +18,9 @@ export const schemaTags = {
|
|||
labelAppend: OptionalFieldLabel,
|
||||
};
|
||||
|
||||
export const schema: FormSchema<CasePostRequest> = {
|
||||
export type FormProps = Omit<CasePostRequest, 'connector'> & { connectorId: string };
|
||||
|
||||
export const schema: FormSchema<FormProps> = {
|
||||
title: {
|
||||
type: FIELD_TYPES.TEXT,
|
||||
label: i18n.NAME,
|
||||
|
@ -37,4 +39,9 @@ export const schema: FormSchema<CasePostRequest> = {
|
|||
],
|
||||
},
|
||||
tags: schemaTags,
|
||||
connectorId: {
|
||||
type: FIELD_TYPES.SUPER_SELECT,
|
||||
label: i18n.CONNECTORS,
|
||||
defaultValue: 'none',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 STEP_ONE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.components.create.stepOneTitle',
|
||||
{
|
||||
defaultMessage: 'Case fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const STEP_TWO_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.components.create.stepTwoTitle',
|
||||
{
|
||||
defaultMessage: 'External incident management system fields',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { CaseUserActions } from '../../containers/types';
|
||||
|
||||
export const getConnectorFieldsFromUserActions = (id: string, userActions: CaseUserActions[]) => {
|
||||
try {
|
||||
for (const action of [...userActions].reverse()) {
|
||||
if (action.actionField.length === 1 && action.actionField[0] === 'connector') {
|
||||
if (action.oldValue && action.newValue) {
|
||||
const oldValue = JSON.parse(action.oldValue);
|
||||
const newValue = JSON.parse(action.newValue);
|
||||
|
||||
if (newValue.id === id) {
|
||||
return newValue.fields;
|
||||
}
|
||||
|
||||
if (oldValue.id === id) {
|
||||
return oldValue.fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -12,10 +12,12 @@ import { getFormMock, useFormMock } from '../__mock__/form';
|
|||
import { TestProviders } from '../../../common/mock';
|
||||
import { connectorsMock } from '../../containers/configure/mock';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { caseUserActions } from '../../containers/mock';
|
||||
|
||||
jest.mock(
|
||||
'../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
|
||||
);
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
const defaultProps = {
|
||||
connectors: connectorsMock,
|
||||
|
@ -23,22 +25,26 @@ const defaultProps = {
|
|||
isLoading: false,
|
||||
onSubmit,
|
||||
selectedConnector: 'none',
|
||||
caseFields: null,
|
||||
userActions: caseUserActions,
|
||||
};
|
||||
|
||||
describe('EditConnector ', () => {
|
||||
const sampleConnector = '123';
|
||||
const formHookMock = getFormMock({ connector: sampleConnector });
|
||||
const formHookMock = getFormMock({ connectorId: sampleConnector });
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
useFormMock.mockImplementation(() => ({ form: formHookMock }));
|
||||
});
|
||||
it('Renders no connector, and then edit', () => {
|
||||
|
||||
it('Renders no connector, and then edit', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EditConnector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
|
||||
|
||||
expect(
|
||||
wrapper.find(`span[data-test-subj="dropdown-connector-no-connector"]`).last().exists()
|
||||
|
@ -46,8 +52,8 @@ describe('EditConnector ', () => {
|
|||
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
await waitFor(() => wrapper.update());
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy();
|
||||
});
|
||||
|
@ -58,10 +64,11 @@ describe('EditConnector ', () => {
|
|||
<EditConnector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
|
||||
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy();
|
||||
|
@ -79,10 +86,11 @@ describe('EditConnector ', () => {
|
|||
<EditConnector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
|
||||
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().exists()).toBeTruthy();
|
||||
|
@ -90,7 +98,7 @@ describe('EditConnector ', () => {
|
|||
wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click');
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none');
|
||||
expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connectorId', 'none');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -103,29 +111,32 @@ describe('EditConnector ', () => {
|
|||
<EditConnector {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper.find('[data-test-subj="connector-edit"] button').simulate('click');
|
||||
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.update();
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click');
|
||||
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click');
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(formHookMock.setFieldValue).toBeCalledWith(
|
||||
'connector',
|
||||
'connectorId',
|
||||
defaultProps.selectedConnector
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders loading spinner', () => {
|
||||
it('Renders loading spinner', async () => {
|
||||
const props = { ...defaultProps, isLoading: true };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EditConnector {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(`[data-test-subj="connector-loading"]`).last().exists()).toBeTruthy();
|
||||
await waitFor(() =>
|
||||
expect(wrapper.find(`[data-test-subj="connector-loading"]`).last().exists()).toBeTruthy()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useReducer } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import {
|
||||
EuiText,
|
||||
EuiHorizontalRule,
|
||||
|
@ -13,80 +14,192 @@ import {
|
|||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiLoadingSpinner,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import styled, { css } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
import { noop } from 'lodash/fp';
|
||||
|
||||
import * as i18n from '../../translations';
|
||||
import { Form, UseField, useForm } from '../../../shared_imports';
|
||||
import { schema } from './schema';
|
||||
import { ConnectorTypeFields } from '../../../../../case/common/api/connectors';
|
||||
import { ConnectorSelector } from '../connector_selector/form';
|
||||
import { Connector } from '../../../../../case/common/api/cases';
|
||||
import { ActionConnector } from '../../../../../case/common/api/cases';
|
||||
import { SettingFieldsForm } from '../settings/fields_form';
|
||||
import { getConnectorById } from '../configure_cases/utils';
|
||||
import { CaseUserActions } from '../../containers/types';
|
||||
import { schema } from './schema';
|
||||
import { getConnectorFieldsFromUserActions } from './helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface EditConnectorProps {
|
||||
connectors: Connector[];
|
||||
caseFields: ConnectorTypeFields['fields'];
|
||||
connectors: ActionConnector[];
|
||||
disabled?: boolean;
|
||||
isLoading: boolean;
|
||||
onSubmit: (a: string, onSuccess: () => void, onError: () => void) => void;
|
||||
onSubmit: (
|
||||
connectorId: string,
|
||||
connectorFields: ConnectorTypeFields['fields'],
|
||||
onError: () => void,
|
||||
onSuccess: () => void
|
||||
) => void;
|
||||
selectedConnector: string;
|
||||
userActions: CaseUserActions[];
|
||||
}
|
||||
|
||||
const MyFlexGroup = styled(EuiFlexGroup)`
|
||||
${({ theme }) => css`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.eui.euiSizeM};
|
||||
p {
|
||||
font-size: ${theme.eui.euiSizeM};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
const DisappearingFlexItem = styled(EuiFlexItem)`
|
||||
${({ $isHidden }: { $isHidden: boolean }) =>
|
||||
$isHidden &&
|
||||
`
|
||||
margin: 0 !important;
|
||||
`}
|
||||
`;
|
||||
|
||||
interface State {
|
||||
currentConnector: ActionConnector | null;
|
||||
fields: ConnectorTypeFields['fields'];
|
||||
editConnector: boolean;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_CURRENT_CONNECTOR'; payload: State['currentConnector'] }
|
||||
| { type: 'SET_FIELDS'; payload: State['fields'] }
|
||||
| { type: 'SET_EDIT_CONNECTOR'; payload: State['editConnector'] };
|
||||
const editConnectorReducer = (state: State, action: Action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_CURRENT_CONNECTOR':
|
||||
return {
|
||||
...state,
|
||||
currentConnector: action.payload,
|
||||
};
|
||||
case 'SET_FIELDS':
|
||||
return {
|
||||
...state,
|
||||
fields: action.payload,
|
||||
};
|
||||
case 'SET_EDIT_CONNECTOR':
|
||||
return {
|
||||
...state,
|
||||
editConnector: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
currentConnector: null,
|
||||
fields: null,
|
||||
editConnector: false,
|
||||
};
|
||||
|
||||
export const EditConnector = React.memo(
|
||||
({
|
||||
caseFields,
|
||||
connectors,
|
||||
disabled = false,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
selectedConnector,
|
||||
userActions,
|
||||
}: EditConnectorProps) => {
|
||||
const initialState: {
|
||||
connectors: Connector[];
|
||||
connector: string | undefined;
|
||||
} = {
|
||||
connectors,
|
||||
connector: undefined,
|
||||
};
|
||||
const { form } = useForm({
|
||||
defaultValue: initialState,
|
||||
defaultValue: { connectorId: selectedConnector },
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
});
|
||||
|
||||
const { setFieldValue, submit } = form;
|
||||
const [connectorHasChanged, setConnectorHasChanged] = useState(false);
|
||||
|
||||
const [{ currentConnector, fields, editConnector }, dispatch] = useReducer(
|
||||
editConnectorReducer,
|
||||
{ ...initialState, fields: caseFields }
|
||||
);
|
||||
|
||||
const onChangeConnector = useCallback(
|
||||
(connectorId) => {
|
||||
setConnectorHasChanged(selectedConnector !== connectorId);
|
||||
(newConnectorId) => {
|
||||
// Init
|
||||
if (currentConnector == null) {
|
||||
dispatch({
|
||||
type: 'SET_CURRENT_CONNECTOR',
|
||||
payload: getConnectorById(newConnectorId, connectors),
|
||||
});
|
||||
}
|
||||
// change connect on dropdown action
|
||||
else if (currentConnector.id !== newConnectorId) {
|
||||
dispatch({
|
||||
type: 'SET_CURRENT_CONNECTOR',
|
||||
payload: getConnectorById(newConnectorId, connectors),
|
||||
});
|
||||
dispatch({
|
||||
type: 'SET_FIELDS',
|
||||
payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []),
|
||||
});
|
||||
} else if (fields === null) {
|
||||
dispatch({
|
||||
type: 'SET_FIELDS',
|
||||
payload: getConnectorFieldsFromUserActions(newConnectorId, userActions ?? []),
|
||||
});
|
||||
}
|
||||
},
|
||||
[selectedConnector]
|
||||
[currentConnector, fields, userActions, connectors]
|
||||
);
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(newFields) => {
|
||||
if (!deepEqual(newFields, fields)) {
|
||||
dispatch({
|
||||
type: 'SET_FIELDS',
|
||||
payload: newFields,
|
||||
});
|
||||
}
|
||||
},
|
||||
[fields, dispatch]
|
||||
);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
setFieldValue('connector', selectedConnector);
|
||||
setConnectorHasChanged(false);
|
||||
}, [setFieldValue, selectedConnector]);
|
||||
setFieldValue('connectorId', selectedConnector);
|
||||
dispatch({
|
||||
type: 'SET_EDIT_CONNECTOR',
|
||||
payload: false,
|
||||
});
|
||||
}, [dispatch, setFieldValue, selectedConnector]);
|
||||
|
||||
const onCancelConnector = useCallback(() => {
|
||||
setFieldValue('connector', selectedConnector);
|
||||
setConnectorHasChanged(false);
|
||||
}, [selectedConnector, setFieldValue]);
|
||||
setFieldValue('connectorId', selectedConnector);
|
||||
dispatch({
|
||||
type: 'SET_FIELDS',
|
||||
payload: caseFields,
|
||||
});
|
||||
dispatch({
|
||||
type: 'SET_EDIT_CONNECTOR',
|
||||
payload: false,
|
||||
});
|
||||
}, [dispatch, selectedConnector, setFieldValue, caseFields]);
|
||||
|
||||
const onSubmitConnector = useCallback(async () => {
|
||||
const { isValid, data: newData } = await submit();
|
||||
if (isValid && newData.connector) {
|
||||
onSubmit(newData.connector, noop, onError);
|
||||
setConnectorHasChanged(false);
|
||||
if (isValid && newData.connectorId) {
|
||||
onSubmit(newData.connectorId, fields, onError, noop);
|
||||
dispatch({
|
||||
type: 'SET_EDIT_CONNECTOR',
|
||||
payload: false,
|
||||
});
|
||||
}
|
||||
}, [submit, onSubmit, onError]);
|
||||
}, [dispatch, submit, fields, onSubmit, onError]);
|
||||
|
||||
const onEditClick = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'SET_EDIT_CONNECTOR',
|
||||
payload: true,
|
||||
});
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<EuiText>
|
||||
<MyFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween">
|
||||
|
@ -94,32 +207,59 @@ export const EditConnector = React.memo(
|
|||
<h4>{i18n.CONNECTORS}</h4>
|
||||
</EuiFlexItem>
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="connector-loading" />}
|
||||
{!isLoading && !editConnector && (
|
||||
<EuiFlexItem data-test-subj="connector-edit" grow={false}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="connector-edit-button"
|
||||
isDisabled={disabled}
|
||||
aria-label={i18n.EDIT_CONNECTOR_ARIA}
|
||||
iconType={'pencil'}
|
||||
onClick={onEditClick}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</MyFlexGroup>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<MyFlexGroup gutterSize="xs">
|
||||
<MyFlexGroup gutterSize="none">
|
||||
<EuiFlexGroup data-test-subj="edit-connectors" direction="column">
|
||||
<EuiFlexItem>
|
||||
<DisappearingFlexItem $isHidden={!editConnector}>
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup gutterSize="none" direction="row">
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="connector"
|
||||
path="connectorId"
|
||||
component={ConnectorSelector}
|
||||
componentProps={{
|
||||
connectors,
|
||||
dataTestSubj: 'caseConnectors',
|
||||
idAria: 'caseConnectors',
|
||||
isLoading,
|
||||
disabled,
|
||||
defaultValue: selectedConnector,
|
||||
disabled,
|
||||
idAria: 'caseConnectors',
|
||||
isEdit: editConnector,
|
||||
isLoading,
|
||||
}}
|
||||
onChange={onChangeConnector}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
</DisappearingFlexItem>
|
||||
<EuiFlexItem data-test-subj="edit-connector-settings-fields-form-flex-item">
|
||||
{(currentConnector == null || currentConnector?.id === 'none') && // Connector is none or not defined.
|
||||
!(currentConnector === null && selectedConnector !== 'none') && // Connector has not been deleted.
|
||||
!editConnector && (
|
||||
<EuiText size="s">
|
||||
<span>{i18n.NO_CONNECTOR}</span>
|
||||
</EuiText>
|
||||
)}
|
||||
<SettingFieldsForm
|
||||
connector={currentConnector}
|
||||
fields={fields}
|
||||
isEdit={editConnector}
|
||||
onChange={onFieldsChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{connectorHasChanged && (
|
||||
{editConnector && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -3,10 +3,15 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { FormSchema } from '../../../shared_imports';
|
||||
|
||||
export const schema: FormSchema = {
|
||||
connector: {
|
||||
defaultValue: 'none',
|
||||
import { FormSchema, FIELD_TYPES } from '../../../shared_imports';
|
||||
|
||||
export interface FormProps {
|
||||
connectorId: string;
|
||||
}
|
||||
|
||||
export const schema: FormSchema<FormProps> = {
|
||||
connectorId: {
|
||||
type: FIELD_TYPES.SUPER_SELECT,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export * from '../../translations';
|
||||
|
||||
export const EDIT_CONNECTOR_ARIA = i18n.translate(
|
||||
'xpack.securitySolution.case.editConnector.editConnectorLinkAria',
|
||||
{
|
||||
defaultMessage: 'click to edit connector',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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, { memo, useMemo } from 'react';
|
||||
import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { connectorsConfiguration } from '../../../common/lib/connectors/config';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
|
||||
interface ConnectorCardProps {
|
||||
connectorType: ConnectorTypes;
|
||||
title: string;
|
||||
listItems: Array<{ title: string; description: React.ReactNode }>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const StyledText = styled.span`
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const ConnectorCardDisplay: React.FC<ConnectorCardProps> = ({
|
||||
connectorType,
|
||||
title,
|
||||
listItems,
|
||||
isLoading,
|
||||
}) => {
|
||||
const description = useMemo(
|
||||
() => (
|
||||
<StyledText>
|
||||
{listItems.length > 0 &&
|
||||
listItems.map((item, i) => (
|
||||
<span key={`${item.title}-${i}`}>
|
||||
<strong>{`${item.title}: `}</strong>
|
||||
{item.description}
|
||||
</span>
|
||||
))}
|
||||
</StyledText>
|
||||
),
|
||||
[listItems]
|
||||
);
|
||||
const icon = useMemo(
|
||||
() => <EuiIcon size="xl" type={connectorsConfiguration[`${connectorType}`]?.logo ?? ''} />,
|
||||
[connectorType]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isLoading && <EuiLoadingSpinner data-test-subj="settings-connector-card-loading" />}
|
||||
{!isLoading && (
|
||||
<EuiCard
|
||||
data-test-subj={`settings-connector-card`}
|
||||
description={description}
|
||||
display="plain"
|
||||
icon={icon}
|
||||
layout="horizontal"
|
||||
paddingSize="none"
|
||||
title={title}
|
||||
titleSize="xs"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectorCard = memo(ConnectorCardDisplay);
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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, { memo, Suspense, useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { CaseSettingsConnector, SettingFieldsProps } from './types';
|
||||
import { getCaseSettings } from '.';
|
||||
import { ConnectorTypeFields } from '../../../../../case/common/api/connectors';
|
||||
|
||||
interface Props extends Omit<SettingFieldsProps<ConnectorTypeFields['fields']>, 'connector'> {
|
||||
connector: CaseSettingsConnector | null;
|
||||
}
|
||||
|
||||
const SettingFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => {
|
||||
const { caseSettingsRegistry } = getCaseSettings();
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(newFields) => {
|
||||
onChange(newFields);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { caseSettingFieldsComponent: FieldsComponent } = caseSettingsRegistry.get(
|
||||
connector.actionTypeId
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{FieldsComponent != null ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<FieldsComponent
|
||||
isEdit={isEdit}
|
||||
fields={fields}
|
||||
connector={connector}
|
||||
onChange={onFieldsChange}
|
||||
/>
|
||||
</Suspense>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingFieldsForm = memo(SettingFieldsFormComponent);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { CaseSettingsRegistry } from './types';
|
||||
import { createCaseSettingsRegistry } from './settings_registry';
|
||||
import { getCaseSetting as getJiraCaseSetting } from './jira';
|
||||
import { getCaseSetting as getResilientCaseSetting } from './resilient';
|
||||
import { getCaseSetting as getServiceNowCaseSetting } from './servicenow';
|
||||
import {
|
||||
JiraFieldsType,
|
||||
ServiceNowFieldsType,
|
||||
ResilientFieldsType,
|
||||
} from '../../../../../case/common/api/connectors';
|
||||
|
||||
interface GetCaseSettingReturn {
|
||||
caseSettingsRegistry: CaseSettingsRegistry;
|
||||
}
|
||||
|
||||
class CaseSettings {
|
||||
private caseSettingsRegistry: CaseSettingsRegistry;
|
||||
|
||||
constructor() {
|
||||
this.caseSettingsRegistry = createCaseSettingsRegistry();
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.caseSettingsRegistry.register<JiraFieldsType>(getJiraCaseSetting());
|
||||
this.caseSettingsRegistry.register<ResilientFieldsType>(getResilientCaseSetting());
|
||||
this.caseSettingsRegistry.register<ServiceNowFieldsType>(getServiceNowCaseSetting());
|
||||
}
|
||||
|
||||
registry(): CaseSettingsRegistry {
|
||||
return this.caseSettingsRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
const caseSettings = new CaseSettings();
|
||||
|
||||
export const getCaseSettings = (): GetCaseSettingReturn => {
|
||||
return {
|
||||
caseSettingsRegistry: caseSettings.registry(),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { GetIssueTypesProps, GetFieldsByIssueTypeProps } from '../api';
|
||||
import { IssueTypes, Fields } from '../types';
|
||||
|
||||
const issueTypes = [
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
];
|
||||
|
||||
const fieldsByIssueType = {
|
||||
summary: { allowedValues: [], defaultValue: {} },
|
||||
priority: {
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: { name: 'Medium', id: '3' },
|
||||
},
|
||||
};
|
||||
|
||||
export const getIssueTypes = async (props: GetIssueTypesProps): Promise<{ data: IssueTypes }> =>
|
||||
Promise.resolve({ data: issueTypes });
|
||||
|
||||
export const getFieldsByIssueType = async (
|
||||
props: GetFieldsByIssueTypeProps
|
||||
): Promise<{ data: Fields }> => Promise.resolve({ data: fieldsByIssueType });
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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 { httpServiceMock } from '../../../../../../../../src/core/public/mocks';
|
||||
import { getIssueTypes, getFieldsByIssueType, getIssues, getIssue } from './api';
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const issueResponse = {
|
||||
id: '10267',
|
||||
key: 'RJ-107',
|
||||
fields: { summary: 'Test title' },
|
||||
};
|
||||
|
||||
const issuesResponse = [issueResponse];
|
||||
|
||||
describe('Jira API', () => {
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('getIssueTypes', () => {
|
||||
test('should call get issue types API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(issueTypesResponse);
|
||||
const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'test' });
|
||||
|
||||
expect(res).toEqual(issueTypesResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldsByIssueType', () => {
|
||||
test('should call get fields API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(fieldsResponse);
|
||||
const res = await getFieldsByIssueType({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
connectorId: 'test',
|
||||
id: '10006',
|
||||
});
|
||||
|
||||
expect(res).toEqual(fieldsResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIssues', () => {
|
||||
test('should call get fields API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(issuesResponse);
|
||||
const res = await getIssues({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
connectorId: 'test',
|
||||
title: 'test issue',
|
||||
});
|
||||
|
||||
expect(res).toEqual(issuesResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIssue', () => {
|
||||
test('should call get fields API', async () => {
|
||||
const abortCtrl = new AbortController();
|
||||
http.post.mockResolvedValueOnce(issuesResponse);
|
||||
const res = await getIssue({
|
||||
http,
|
||||
signal: abortCtrl.signal,
|
||||
connectorId: 'test',
|
||||
id: 'RJ-107',
|
||||
});
|
||||
|
||||
expect(res).toEqual(issuesResponse);
|
||||
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
|
||||
body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { ActionTypeExecutorResult } from '../../../../../../case/common/api';
|
||||
import { IssueTypes, Fields, Issues, Issue } from './types';
|
||||
|
||||
export const BASE_ACTION_API_PATH = '/api/actions';
|
||||
|
||||
export interface GetIssueTypesProps {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
}
|
||||
|
||||
export async function getIssueTypes({ http, signal, connectorId }: GetIssueTypesProps) {
|
||||
return http.post<ActionTypeExecutorResult<IssueTypes>>(
|
||||
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'issueTypes', subActionParams: {} },
|
||||
}),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export interface GetFieldsByIssueTypeProps {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function getFieldsByIssueType({
|
||||
http,
|
||||
signal,
|
||||
connectorId,
|
||||
id,
|
||||
}: GetFieldsByIssueTypeProps): Promise<ActionTypeExecutorResult<Fields>> {
|
||||
return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'fieldsByIssueType', subActionParams: { id } },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export interface GetIssuesTypeProps {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export async function getIssues({
|
||||
http,
|
||||
signal,
|
||||
connectorId,
|
||||
title,
|
||||
}: GetIssuesTypeProps): Promise<ActionTypeExecutorResult<Issues>> {
|
||||
return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'issues', subActionParams: { title } },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
export interface GetIssueTypeProps {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function getIssue({
|
||||
http,
|
||||
signal,
|
||||
connectorId,
|
||||
id,
|
||||
}: GetIssueTypeProps): Promise<ActionTypeExecutorResult<Issue>> {
|
||||
return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'issue', subActionParams: { id } },
|
||||
}),
|
||||
signal,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import { omit } from 'lodash/fp';
|
||||
|
||||
import { connector } from '../mock';
|
||||
import { useGetIssueTypes } from './use_get_issue_types';
|
||||
import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
|
||||
import Fields from './fields';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
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;
|
||||
|
||||
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',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
defaultValue: { name: 'Medium', id: '3' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
issueType: '10006',
|
||||
priority: 'High',
|
||||
parent: null,
|
||||
};
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
|
||||
useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('all params fields are rendered', () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={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'
|
||||
);
|
||||
});
|
||||
|
||||
test('it disabled the fields when loading issue types', () => {
|
||||
useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true });
|
||||
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled')
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it disabled the fields when loading fields', () => {
|
||||
useGetFieldsByIssueTypeMock.mockReturnValue({
|
||||
...useGetFieldsByIssueTypeResponse,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled')
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it hides the priority if not supported', () => {
|
||||
const response = omit('fields.priority', useGetFieldsByIssueTypeResponse);
|
||||
|
||||
useGetFieldsByIssueTypeMock.mockReturnValue(response);
|
||||
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('it sets issue type correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="issueTypeSelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '10007' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ issueType: '10007', parent: null, priority: null });
|
||||
});
|
||||
|
||||
test('it sets priority correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="prioritySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '2' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ issueType: '10006', parent: null, priority: '2' });
|
||||
});
|
||||
|
||||
test('it resets priority when changing issue type', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
wrapper
|
||||
.find('select[data-test-subj="issueTypeSelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '10007' },
|
||||
});
|
||||
|
||||
expect(onChange).toBeCalledWith({ issueType: '10007', parent: null, priority: null });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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 { map } from 'lodash/fp';
|
||||
import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
import { ConnectorTypes, JiraFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { SettingFieldsProps } from '../types';
|
||||
import { useGetIssueTypes } from './use_get_issue_types';
|
||||
import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
|
||||
import { SearchIssues } from './search_issues';
|
||||
import { ConnectorCard } from '../card';
|
||||
|
||||
const JiraSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps<JiraFieldsType>> = ({
|
||||
connector,
|
||||
fields,
|
||||
isEdit = true,
|
||||
onChange,
|
||||
}) => {
|
||||
const { issueType = null, priority = null, parent = null } = fields ?? {};
|
||||
const { http, notifications } = useKibana().services;
|
||||
|
||||
const handleIssueType = useCallback(
|
||||
(issueTypeSelectOptions: Array<{ value: string; text: string }>) => {
|
||||
if (issueType == null && issueTypeSelectOptions.length > 0) {
|
||||
// if there is no issue type set in the edit view, set it to default
|
||||
if (isEdit) {
|
||||
onChange({
|
||||
issueType: issueTypeSelectOptions[0].value,
|
||||
parent,
|
||||
priority,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[isEdit, issueType, onChange, parent, priority]
|
||||
);
|
||||
const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({
|
||||
connector,
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
handleIssueType,
|
||||
});
|
||||
|
||||
const issueTypesSelectOptions = useMemo(
|
||||
() =>
|
||||
issueTypes.map((type) => ({
|
||||
text: type.name ?? '',
|
||||
value: type.id ?? '',
|
||||
})),
|
||||
[issueTypes]
|
||||
);
|
||||
|
||||
const currentIssueType = useMemo(() => {
|
||||
if (!issueType && issueTypesSelectOptions.length > 0) {
|
||||
return issueTypesSelectOptions[0].value;
|
||||
} else if (
|
||||
issueTypesSelectOptions.length > 0 &&
|
||||
!issueTypesSelectOptions.some(({ value }) => value === issueType)
|
||||
) {
|
||||
return issueTypesSelectOptions[0].value;
|
||||
}
|
||||
return issueType;
|
||||
}, [issueType, issueTypesSelectOptions]);
|
||||
|
||||
const { isLoading: isLoadingFields, fields: fieldsByIssueType } = useGetFieldsByIssueType({
|
||||
connector,
|
||||
http,
|
||||
issueType: currentIssueType,
|
||||
toastNotifications: notifications.toasts,
|
||||
});
|
||||
|
||||
const hasPriority = useMemo(() => fieldsByIssueType.priority != null, [fieldsByIssueType]);
|
||||
|
||||
const hasParent = useMemo(() => fieldsByIssueType.parent != null, [fieldsByIssueType]);
|
||||
|
||||
const prioritiesSelectOptions = useMemo(() => {
|
||||
const priorities = fieldsByIssueType.priority?.allowedValues ?? [];
|
||||
return map(
|
||||
(p) => ({
|
||||
text: p.name,
|
||||
value: p.name,
|
||||
}),
|
||||
priorities
|
||||
);
|
||||
}, [fieldsByIssueType]);
|
||||
|
||||
const listItems = useMemo(
|
||||
() => [
|
||||
...(issueType != null && issueType.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.ISSUE_TYPE,
|
||||
description: issueTypes.find((issue) => issue.id === issueType)?.name ?? '',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(parent != null && parent.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.PARENT_ISSUE,
|
||||
description: parent,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(priority != null && priority.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.PRIORITY,
|
||||
description: priority,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[issueType, issueTypes, parent, priority]
|
||||
);
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
(key, value) => {
|
||||
if (key === 'issueType') {
|
||||
return onChange({ ...fields, issueType: value, priority: null, parent: null });
|
||||
}
|
||||
return onChange({
|
||||
...fields,
|
||||
issueType: currentIssueType,
|
||||
parent,
|
||||
priority,
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
[currentIssueType, fields, onChange, parent, priority]
|
||||
);
|
||||
return isEdit ? (
|
||||
<span data-test-subj={'connector-settings-jira'}>
|
||||
<EuiFormRow fullWidth label={i18n.ISSUE_TYPE}>
|
||||
<EuiSelect
|
||||
data-test-subj="issueTypeSelect"
|
||||
disabled={isLoadingIssueTypes || isLoadingFields}
|
||||
fullWidth
|
||||
isLoading={isLoadingIssueTypes}
|
||||
onChange={(e) => onFieldChange('issueType', e.target.value)}
|
||||
options={issueTypesSelectOptions}
|
||||
value={currentIssueType ?? ''}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<>
|
||||
{hasParent && (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.PARENT_ISSUE}>
|
||||
<SearchIssues
|
||||
actionConnector={connector}
|
||||
onChange={(parentIssueKey) => onFieldChange('parent', parentIssueKey)}
|
||||
selectedValue={parent}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
{hasPriority && (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.PRIORITY}>
|
||||
<EuiSelect
|
||||
data-test-subj="prioritySelect"
|
||||
disabled={isLoadingIssueTypes || isLoadingFields}
|
||||
fullWidth
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingFields}
|
||||
onChange={(e) => onFieldChange('priority', e.target.value)}
|
||||
options={prioritiesSelectOptions}
|
||||
value={priority ?? ''}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</span>
|
||||
) : (
|
||||
<ConnectorCard
|
||||
connectorType={ConnectorTypes.jira}
|
||||
isLoading={isLoadingIssueTypes || isLoadingFields}
|
||||
listItems={listItems}
|
||||
title={connector.name}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { JiraSettingFieldsComponent as default };
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { CaseSetting } from '../types';
|
||||
import { JiraFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export * from './types';
|
||||
|
||||
export const getCaseSetting = (): CaseSetting<JiraFieldsType> => {
|
||||
return {
|
||||
id: '.jira',
|
||||
caseSettingFieldsComponent: lazy(() => import('./fields')),
|
||||
};
|
||||
};
|
||||
|
||||
export const fieldLabels = {
|
||||
issueType: i18n.ISSUE_TYPE,
|
||||
priority: i18n.PRIORITY,
|
||||
parent: i18n.PARENT_ISSUE,
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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, useEffect, useCallback, useState, memo } from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionConnector } from '../../../containers/types';
|
||||
import { useGetIssues } from './use_get_issues';
|
||||
import { useGetSingleIssue } from './use_get_single_issue';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
selectedValue: string | null;
|
||||
actionConnector?: ActionConnector;
|
||||
onChange: (parentIssueKey: string) => void;
|
||||
}
|
||||
|
||||
const SearchIssuesComponent: React.FC<Props> = ({ selectedValue, actionConnector, onChange }) => {
|
||||
const [query, setQuery] = useState<string | null>(null);
|
||||
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
|
||||
[]
|
||||
);
|
||||
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
|
||||
const { http, notifications } = useKibana().services;
|
||||
|
||||
const { isLoading: isLoadingIssues, issues } = useGetIssues({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
actionConnector,
|
||||
query,
|
||||
});
|
||||
|
||||
const { isLoading: isLoadingSingleIssue, issue: singleIssue } = useGetSingleIssue({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
actionConnector,
|
||||
id: selectedValue,
|
||||
});
|
||||
|
||||
useEffect(() => setOptions(issues.map((issue) => ({ label: issue.title, value: issue.key }))), [
|
||||
issues,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingSingleIssue || singleIssue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const singleIssueAsOptions = [{ label: singleIssue.title, value: singleIssue.key }];
|
||||
setOptions(singleIssueAsOptions);
|
||||
setSelectedOptions(singleIssueAsOptions);
|
||||
}, [singleIssue, isLoadingSingleIssue]);
|
||||
|
||||
const onSearchChange = useCallback((searchVal: string) => {
|
||||
setQuery(searchVal);
|
||||
}, []);
|
||||
|
||||
const onChangeComboBox = useCallback(
|
||||
(changedOptions) => {
|
||||
setSelectedOptions(changedOptions);
|
||||
onChange(changedOptions[0].value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const inputPlaceholder = useMemo(
|
||||
(): string =>
|
||||
isLoadingIssues || isLoadingSingleIssue
|
||||
? i18n.SEARCH_ISSUES_LOADING
|
||||
: i18n.SEARCH_ISSUES_PLACEHOLDER,
|
||||
[isLoadingIssues, isLoadingSingleIssue]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiComboBox
|
||||
singleSelection
|
||||
fullWidth
|
||||
placeholder={inputPlaceholder}
|
||||
data-test-sub={'search-parent-issues'}
|
||||
aria-label={i18n.SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL}
|
||||
options={options}
|
||||
isLoading={isLoadingIssues || isLoadingSingleIssue}
|
||||
onSearchChange={onSearchChange}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChangeComboBox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchIssues = memo(SearchIssuesComponent);
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 ISSUE_TYPES_API_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get issue types',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIELDS_API_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const ISSUES_API_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get issues',
|
||||
}
|
||||
);
|
||||
|
||||
export const GET_ISSUE_API_ERROR = (id: string) =>
|
||||
i18n.translate('xpack.securitySolution.components.settings.jira.unableToGetIssueMessage', {
|
||||
defaultMessage: 'Unable to get issue with id {id}',
|
||||
values: { id },
|
||||
});
|
||||
|
||||
export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Select parent issue',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select parent issue',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEARCH_ISSUES_LOADING = i18n.translate(
|
||||
'xpack.securitySolution.components.settings.jira.searchIssuesLoading',
|
||||
{
|
||||
defaultMessage: 'Loading...',
|
||||
}
|
||||
);
|
||||
|
||||
export const PRIORITY = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Priority',
|
||||
}
|
||||
);
|
||||
|
||||
export const ISSUE_TYPE = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Issue type',
|
||||
}
|
||||
);
|
||||
|
||||
export const PARENT_ISSUE = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.jira.parentIssueSearchLabel',
|
||||
{
|
||||
defaultMessage: 'Parent issue',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 type IssueTypes = Array<{ id: string; name: string }>;
|
||||
export interface Fields {
|
||||
[key: string]: {
|
||||
allowedValues: Array<{ name: string; id: string }> | [];
|
||||
defaultValue: { name: string; id: string } | {};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Issue {
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type Issues = Issue[];
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { connector } from '../mock';
|
||||
import { useGetFieldsByIssueType, UseGetFieldsByIssueType } from './use_get_fields_by_issue_type';
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./api');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
describe('useGetFieldsByIssueType', () => {
|
||||
const { http, notifications } = useKibanaMock().services;
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
|
||||
useGetFieldsByIssueType({ http, toastNotifications: notifications.toasts, issueType: null })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({ isLoading: true, fields: {} });
|
||||
});
|
||||
});
|
||||
|
||||
test('does not fetch when issueType is not provided', async () => {
|
||||
const spyOnGetFieldsByIssueType = jest.spyOn(api, 'getFieldsByIssueType');
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
|
||||
useGetFieldsByIssueType({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
issueType: null,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(spyOnGetFieldsByIssueType).not.toHaveBeenCalled();
|
||||
expect(result.current).toEqual({ isLoading: false, fields: {} });
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch fields', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
|
||||
useGetFieldsByIssueType({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
issueType: 'Task',
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
fields: {
|
||||
summary: { allowedValues: [], defaultValue: {} },
|
||||
priority: {
|
||||
allowedValues: [
|
||||
{
|
||||
name: 'Medium',
|
||||
id: '3',
|
||||
},
|
||||
],
|
||||
defaultValue: { name: 'Medium', id: '3' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('unhappy path', async () => {
|
||||
const spyOnGetCaseConfigure = jest.spyOn(api, 'getFieldsByIssueType');
|
||||
spyOnGetCaseConfigure.mockImplementation(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
|
||||
useGetFieldsByIssueType({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
issueType: null,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({ isLoading: false, fields: {} });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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, useRef } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../containers/types';
|
||||
import { getFieldsByIssueType } from './api';
|
||||
import { Fields } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
issueType: string | null;
|
||||
connector?: ActionConnector;
|
||||
}
|
||||
|
||||
export interface UseGetFieldsByIssueType {
|
||||
fields: Fields;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetFieldsByIssueType = ({
|
||||
http,
|
||||
toastNotifications,
|
||||
connector,
|
||||
issueType,
|
||||
}: Props): UseGetFieldsByIssueType => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [fields, setFields] = useState<Fields>({});
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = async () => {
|
||||
if (!connector || !issueType) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getFieldsByIssueType({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: connector.id,
|
||||
id: issueType,
|
||||
});
|
||||
|
||||
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) {
|
||||
setIsLoading(false);
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.FIELDS_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, connector, issueType, toastNotifications]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
fields,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { connector } from '../mock';
|
||||
import { useGetIssueTypes, UseGetIssueTypes } from './use_get_issue_types';
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./api');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
describe('useGetIssueTypes', () => {
|
||||
const { http, notifications } = useKibanaMock().services;
|
||||
const handleIssueType = jest.fn();
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
|
||||
useGetIssueTypes({ http, toastNotifications: notifications.toasts, handleIssueType })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({ isLoading: true, issueTypes: [] });
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch issue types', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
|
||||
useGetIssueTypes({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
handleIssueType,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
issueTypes: [
|
||||
{
|
||||
id: '10006',
|
||||
name: 'Task',
|
||||
},
|
||||
{
|
||||
id: '10007',
|
||||
name: 'Bug',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('handleIssueType is called', async () => {
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
|
||||
useGetIssueTypes({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
handleIssueType,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(handleIssueType).toHaveBeenCalledWith([
|
||||
{ text: 'Task', value: '10006' },
|
||||
{ text: 'Bug', value: '10007' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('unhappy path', async () => {
|
||||
const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssueTypes');
|
||||
spyOnGetCaseConfigure.mockImplementation(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
|
||||
useGetIssueTypes({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
handleIssueType,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({ isLoading: false, issueTypes: [] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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, useRef } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../containers/types';
|
||||
import { getIssueTypes } from './api';
|
||||
import { IssueTypes } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
connector?: ActionConnector;
|
||||
handleIssueType: (options: Array<{ value: string; text: string }>) => void;
|
||||
}
|
||||
|
||||
export interface UseGetIssueTypes {
|
||||
issueTypes: IssueTypes;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetIssueTypes = ({
|
||||
http,
|
||||
connector,
|
||||
toastNotifications,
|
||||
handleIssueType,
|
||||
}: Props): UseGetIssueTypes => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [issueTypes, setIssueTypes] = useState<IssueTypes>([]);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = async () => {
|
||||
if (!connector) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await getIssueTypes({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: connector.id,
|
||||
});
|
||||
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
const asOptions = (res.data ?? []).map((type) => ({
|
||||
text: type.name ?? '',
|
||||
value: type.id ?? '',
|
||||
}));
|
||||
setIssueTypes(res.data ?? []);
|
||||
handleIssueType(asOptions);
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.ISSUE_TYPES_API_ERROR,
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.ISSUE_TYPES_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, connector, toastNotifications, handleIssueType]);
|
||||
|
||||
return {
|
||||
issueTypes,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { isEmpty, debounce } from 'lodash/fp';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../containers/types';
|
||||
import { getIssues } from './api';
|
||||
import { Issues } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
actionConnector?: ActionConnector;
|
||||
query: string | null;
|
||||
}
|
||||
|
||||
export interface UseGetIssues {
|
||||
issues: Issues;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetIssues = ({
|
||||
http,
|
||||
actionConnector,
|
||||
toastNotifications,
|
||||
query,
|
||||
}: Props): UseGetIssues => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [issues, setIssues] = useState<Issues>([]);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = debounce(500, async () => {
|
||||
if (!actionConnector || isEmpty(query)) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await getIssues({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: actionConnector.id,
|
||||
title: query ?? '',
|
||||
});
|
||||
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
setIssues(res.data ?? []);
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.ISSUES_API_ERROR,
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.ISSUES_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, actionConnector, toastNotifications, query]);
|
||||
|
||||
return {
|
||||
issues,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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, useRef } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../containers/types';
|
||||
import { getIssue } from './api';
|
||||
import { Issue } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
id: string | null;
|
||||
actionConnector?: ActionConnector;
|
||||
}
|
||||
|
||||
export interface UseGetSingleIssue {
|
||||
issue: Issue | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetSingleIssue = ({
|
||||
http,
|
||||
toastNotifications,
|
||||
actionConnector,
|
||||
id,
|
||||
}: Props): UseGetSingleIssue => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [issue, setIssue] = useState<Issue | null>(null);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = async () => {
|
||||
if (!actionConnector || !id) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getIssue({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: actionConnector.id,
|
||||
id,
|
||||
});
|
||||
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
setIssue(res.data ?? null);
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.GET_ISSUE_API_ERROR(id),
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.GET_ISSUE_API_ERROR(id),
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, actionConnector, id, toastNotifications]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
issue,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 const connector = {
|
||||
id: '123',
|
||||
name: 'My connector',
|
||||
actionTypeId: '.jira',
|
||||
config: {},
|
||||
isPreconfigured: false,
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Props } from '../api';
|
||||
import { ResilientIncidentTypes, ResilientSeverity } from '../types';
|
||||
|
||||
const severity = [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
];
|
||||
|
||||
const incidentTypes = [
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
];
|
||||
|
||||
export const getIncidentTypes = async (props: Props): Promise<{ data: ResilientIncidentTypes }> =>
|
||||
Promise.resolve({ data: incidentTypes });
|
||||
|
||||
export const getSeverity = async (props: Props): Promise<{ data: ResilientSeverity }> =>
|
||||
Promise.resolve({ data: severity });
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { ActionTypeExecutorResult } from '../../../../../../case/common/api';
|
||||
import { ResilientIncidentTypes, ResilientSeverity } from './types';
|
||||
|
||||
export const BASE_ACTION_API_PATH = '/api/actions';
|
||||
|
||||
export interface Props {
|
||||
http: HttpSetup;
|
||||
signal: AbortSignal;
|
||||
connectorId: string;
|
||||
}
|
||||
|
||||
export async function getIncidentTypes({ http, signal, connectorId }: Props) {
|
||||
return http.post<ActionTypeExecutorResult<ResilientIncidentTypes>>(
|
||||
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'incidentTypes', subActionParams: {} },
|
||||
}),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSeverity({ http, signal, connectorId }: Props) {
|
||||
return http.post<ActionTypeExecutorResult<ResilientSeverity>>(
|
||||
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
params: { subAction: 'severity', subActionParams: {} },
|
||||
}),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 { mount } from 'enzyme';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { connector } from '../mock';
|
||||
import { useGetIncidentTypes } from './use_get_incident_types';
|
||||
import { useGetSeverity } from './use_get_severity';
|
||||
import Fields from './fields';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./use_get_incident_types');
|
||||
jest.mock('./use_get_severity');
|
||||
|
||||
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
|
||||
const useGetSeverityMock = useGetSeverity as jest.Mock;
|
||||
|
||||
describe('ResilientParamsFields renders', () => {
|
||||
const useGetIncidentTypesResponse = {
|
||||
isLoading: false,
|
||||
incidentTypes: [
|
||||
{
|
||||
id: 19,
|
||||
name: 'Malware',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: 'Denial of Service',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const useGetSeverityResponse = {
|
||||
isLoading: false,
|
||||
severity: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fields = {
|
||||
severityCode: '6',
|
||||
incidentTypes: ['19'],
|
||||
};
|
||||
|
||||
const onChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
|
||||
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('all params fields are rendered', () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('options')).toEqual(
|
||||
[
|
||||
{ label: 'Malware', value: '19' },
|
||||
{ label: 'Denial of Service', value: '21' },
|
||||
]
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('selectedOptions')
|
||||
).toEqual([{ label: 'Malware', value: '19' }]);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual(
|
||||
'6'
|
||||
);
|
||||
});
|
||||
|
||||
test('it disabled the fields when loading incident types', () => {
|
||||
useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true });
|
||||
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it disabled the fields when loading severity', () => {
|
||||
useGetSeverityMock.mockReturnValue({
|
||||
...useGetSeverityResponse,
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('it sets issue type correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
await waitFor(() => {
|
||||
((wrapper.find(EuiComboBox).props() as unknown) as {
|
||||
onChange: (a: EuiComboBoxOptionOption[]) => void;
|
||||
}).onChange([{ value: '19', label: 'Denial of Service' }]);
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '6' });
|
||||
});
|
||||
|
||||
test('it sets severity correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="severitySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '4' },
|
||||
});
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '4' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiSelectOption,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { SettingFieldsProps } from '../types';
|
||||
|
||||
import { useGetIncidentTypes } from './use_get_incident_types';
|
||||
import { useGetSeverity } from './use_get_severity';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { ConnectorTypes, ResilientFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
import { ConnectorCard } from '../card';
|
||||
|
||||
const ResilientSettingFieldsComponent: React.FunctionComponent<SettingFieldsProps<
|
||||
ResilientFieldsType
|
||||
>> = ({ isEdit = true, fields, connector, onChange }) => {
|
||||
const { incidentTypes = null, severityCode = null } = fields ?? {};
|
||||
|
||||
const { http, notifications } = useKibana().services;
|
||||
|
||||
const {
|
||||
isLoading: isLoadingIncidentTypes,
|
||||
incidentTypes: allIncidentTypes,
|
||||
} = useGetIncidentTypes({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
});
|
||||
|
||||
const { isLoading: isLoadingSeverity, severity } = useGetSeverity({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
});
|
||||
|
||||
const severitySelectOptions: EuiSelectOption[] = useMemo(
|
||||
() =>
|
||||
severity.map((s) => ({
|
||||
value: s.id.toString(),
|
||||
text: s.name,
|
||||
})),
|
||||
[severity]
|
||||
);
|
||||
|
||||
const incidentTypesComboBoxOptions: Array<EuiComboBoxOptionOption<string>> = useMemo(
|
||||
() =>
|
||||
allIncidentTypes
|
||||
? allIncidentTypes.map((type: { id: number; name: string }) => ({
|
||||
label: type.name,
|
||||
value: type.id.toString(),
|
||||
}))
|
||||
: [],
|
||||
[allIncidentTypes]
|
||||
);
|
||||
const listItems = useMemo(
|
||||
() => [
|
||||
...(incidentTypes != null && incidentTypes.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.INCIDENT_TYPES_LABEL,
|
||||
description: allIncidentTypes
|
||||
.filter((type) => incidentTypes.includes(type.id.toString()))
|
||||
.map((type) => type.name)
|
||||
.join(', '),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(severityCode != null && severityCode.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.SEVERITY_LABEL,
|
||||
description:
|
||||
severity.find((severityObj) => severityObj.id.toString() === severityCode)?.name ??
|
||||
'',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[incidentTypes, severityCode, allIncidentTypes, severity]
|
||||
);
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
(key, value) => {
|
||||
onChange({
|
||||
...fields,
|
||||
incidentTypes,
|
||||
severityCode,
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
[incidentTypes, severityCode, onChange, fields]
|
||||
);
|
||||
|
||||
const selectedIncidentTypesComboBoxOptionsMemo = useMemo(() => {
|
||||
const allIncidentTypesAsObject = allIncidentTypes.reduce(
|
||||
(acc, type) => ({ ...acc, [type.id.toString()]: type.name }),
|
||||
{} as Record<string, string>
|
||||
);
|
||||
return incidentTypes
|
||||
? incidentTypes
|
||||
.map((type) => ({
|
||||
label: allIncidentTypesAsObject[type.toString()],
|
||||
value: type.toString(),
|
||||
}))
|
||||
.filter((type) => type.label != null)
|
||||
: [];
|
||||
}, [allIncidentTypes, incidentTypes]);
|
||||
|
||||
const onIncidentChange = useCallback(
|
||||
(selectedOptions: Array<{ label: string; value?: string }>) => {
|
||||
onFieldChange(
|
||||
'incidentTypes',
|
||||
selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label)
|
||||
);
|
||||
},
|
||||
[onFieldChange]
|
||||
);
|
||||
|
||||
const onIncidentBlur = useCallback(() => {
|
||||
if (!incidentTypes) {
|
||||
onFieldChange('incidentTypes', []);
|
||||
}
|
||||
}, [incidentTypes, onFieldChange]);
|
||||
|
||||
// We need to set them up at initialization
|
||||
useEffect(() => {
|
||||
onChange({ incidentTypes, severityCode });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return isEdit ? (
|
||||
<span data-test-subj={'connector-settings-resilient'}>
|
||||
<EuiFormRow fullWidth label={i18n.INCIDENT_TYPES_LABEL}>
|
||||
<EuiComboBox
|
||||
data-test-subj="incidentTypeComboBox"
|
||||
fullWidth
|
||||
isClearable={true}
|
||||
isDisabled={isLoadingIncidentTypes}
|
||||
isLoading={isLoadingIncidentTypes}
|
||||
onBlur={onIncidentBlur}
|
||||
onChange={onIncidentChange}
|
||||
options={incidentTypesComboBoxOptions}
|
||||
placeholder={i18n.INCIDENT_TYPES_PLACEHOLDER}
|
||||
selectedOptions={selectedIncidentTypesComboBoxOptionsMemo}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow fullWidth label={i18n.SEVERITY_LABEL}>
|
||||
<EuiSelect
|
||||
data-test-subj="severitySelect"
|
||||
disabled={isLoadingSeverity}
|
||||
fullWidth
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingSeverity}
|
||||
onChange={(e) => onFieldChange('severityCode', e.target.value)}
|
||||
options={severitySelectOptions}
|
||||
value={severityCode ?? undefined}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
</span>
|
||||
) : (
|
||||
<ConnectorCard
|
||||
connectorType={ConnectorTypes.resilient}
|
||||
isLoading={isLoadingIncidentTypes || isLoadingSeverity}
|
||||
listItems={listItems}
|
||||
title={connector.name}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { ResilientSettingFieldsComponent as default };
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { CaseSetting } from '../types';
|
||||
import { ResilientFieldsType } from '../../../../../../case/common/api/connectors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export * from './types';
|
||||
|
||||
export const getCaseSetting = (): CaseSetting<ResilientFieldsType> => {
|
||||
return {
|
||||
id: '.resilient',
|
||||
caseSettingFieldsComponent: lazy(() => import('./fields')),
|
||||
};
|
||||
};
|
||||
|
||||
export const fieldLabels = {
|
||||
incidentTypes: i18n.INCIDENT_TYPES_LABEL,
|
||||
severityCode: i18n.SEVERITY_LABEL,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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_TYPES_API_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get incident types',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEVERITY_API_ERROR = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to get severity',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCIDENT_TYPES_PLACEHOLDER = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Choose types',
|
||||
}
|
||||
);
|
||||
|
||||
export const INCIDENT_TYPES_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.resilient.incidentTypesLabel',
|
||||
{
|
||||
defaultMessage: 'Incident Types',
|
||||
}
|
||||
);
|
||||
|
||||
export const SEVERITY_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.case.settings.resilient.severityLabel',
|
||||
{
|
||||
defaultMessage: 'Severity',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export type ResilientIncidentTypes = Array<{ id: number; name: string }>;
|
||||
export type ResilientSeverity = ResilientIncidentTypes;
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { connector } from '../mock';
|
||||
import { useGetIncidentTypes, UseGetIncidentTypes } from './use_get_incident_types';
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./api');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
describe('useGetIncidentTypes', () => {
|
||||
const { http, notifications } = useKibanaMock().services;
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetIncidentTypes>(() =>
|
||||
useGetIncidentTypes({ http, toastNotifications: notifications.toasts })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({ isLoading: true, incidentTypes: [] });
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch incident types', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetIncidentTypes>(() =>
|
||||
useGetIncidentTypes({
|
||||
http,
|
||||
toastNotifications: notifications.toasts,
|
||||
connector,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
incidentTypes: [
|
||||
{ id: 17, name: 'Communication error (fax; email)' },
|
||||
{ id: 1001, name: 'Custom type' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('unhappy path', async () => {
|
||||
const spyOnGetCaseConfigure = jest.spyOn(api, 'getIncidentTypes');
|
||||
spyOnGetCaseConfigure.mockImplementation(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetIncidentTypes>(() =>
|
||||
useGetIncidentTypes({ http, toastNotifications: notifications.toasts, connector })
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({ isLoading: false, incidentTypes: [] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { useState, useEffect, useRef } from 'react';
|
||||
import { HttpSetup, ToastsApi } from 'kibana/public';
|
||||
import { ActionConnector } from '../../../containers/types';
|
||||
import { getIncidentTypes } from './api';
|
||||
import * as i18n from './translations';
|
||||
|
||||
type IncidentTypes = Array<{ id: number; name: string }>;
|
||||
|
||||
interface Props {
|
||||
http: HttpSetup;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
>;
|
||||
connector?: ActionConnector;
|
||||
}
|
||||
|
||||
export interface UseGetIncidentTypes {
|
||||
incidentTypes: IncidentTypes;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const useGetIncidentTypes = ({
|
||||
http,
|
||||
toastNotifications,
|
||||
connector,
|
||||
}: Props): UseGetIncidentTypes => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [incidentTypes, setIncidentTypes] = useState<IncidentTypes>([]);
|
||||
const abortCtrl = useRef(new AbortController());
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const fetchData = async () => {
|
||||
if (!connector) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
abortCtrl.current = new AbortController();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await getIncidentTypes({
|
||||
http,
|
||||
signal: abortCtrl.current.signal,
|
||||
connectorId: connector.id,
|
||||
});
|
||||
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
setIncidentTypes(res.data ?? []);
|
||||
if (res.status && res.status === 'error') {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.INCIDENT_TYPES_API_ERROR,
|
||||
text: `${res.serviceMessage ?? res.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!didCancel) {
|
||||
setIsLoading(false);
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.INCIDENT_TYPES_API_ERROR,
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
abortCtrl.current.abort();
|
||||
fetchData();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
setIsLoading(false);
|
||||
abortCtrl.current.abort();
|
||||
};
|
||||
}, [http, connector, toastNotifications]);
|
||||
|
||||
return {
|
||||
incidentTypes,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { renderHook, act } from '@testing-library/react-hooks';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { connector } from '../mock';
|
||||
import { useGetSeverity, UseGetSeverity } from './use_get_severity';
|
||||
import * as api from './api';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('./api');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
describe('useGetSeverity', () => {
|
||||
const { http, notifications } = useKibanaMock().services;
|
||||
|
||||
test('init', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetSeverity>(() =>
|
||||
useGetSeverity({ http, toastNotifications: notifications.toasts })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({ isLoading: true, severity: [] });
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch severity', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetSeverity>(() =>
|
||||
useGetSeverity({ http, toastNotifications: notifications.toasts, connector })
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
severity: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'High',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('unhappy path', async () => {
|
||||
const spyOnGetCaseConfigure = jest.spyOn(api, 'getSeverity');
|
||||
spyOnGetCaseConfigure.mockImplementation(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, UseGetSeverity>(() =>
|
||||
useGetSeverity({ http, toastNotifications: notifications.toasts, connector })
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({ isLoading: false, severity: [] });
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue