[Security Solution] Webhook - Case Management Connector Follow Ups (#137227)

This commit is contained in:
Steph Milovic 2022-07-29 13:59:31 -06:00 committed by GitHub
parent bebec37f04
commit 2d3d930756
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 861 additions and 453 deletions

View file

@ -31,7 +31,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
comments: [
{
commentId: 'case-comment-1',
@ -57,7 +57,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});
@ -109,7 +109,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
comments: [
{
commentId: 'case-comment-1',
@ -135,7 +135,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});

View file

@ -24,7 +24,7 @@ const createMock = (): jest.Mocked<ExternalService> => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
})
),
updateIncident: jest.fn().mockImplementation(() =>
@ -32,7 +32,7 @@ const createMock = (): jest.Mocked<ExternalService> => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
})
),
createComment: jest.fn(),

View file

@ -25,7 +25,7 @@ export const ExternalIncidentServiceConfiguration = {
getIncidentResponseCreatedDateKey: schema.string(),
getIncidentResponseExternalTitleKey: schema.string(),
getIncidentResponseUpdatedDateKey: schema.string(),
incidentViewUrl: schema.string(),
viewIncidentUrl: schema.string(),
updateIncidentUrl: schema.string(),
updateIncidentMethod: schema.oneOf(
[

View file

@ -30,24 +30,23 @@ const configurationUtilities = actionsConfigMock.create();
const config: CasesWebhookPublicConfigurationType = {
createCommentJson: '{"body":{{{case.comment}}}}',
createCommentMethod: CasesWebhookMethods.POST,
createCommentUrl:
'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createCommentUrl: 'https://coolsite.net/issue/{{{external.system.id}}}/comment',
createIncidentJson:
'{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
createIncidentMethod: CasesWebhookMethods.POST,
createIncidentResponseKey: 'id',
createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
createIncidentUrl: 'https://coolsite.net/issue',
getIncidentResponseCreatedDateKey: 'fields.created',
getIncidentResponseExternalTitleKey: 'key',
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: { ['content-type']: 'application/json' },
incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://coolsite.net/issue/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"title":{{{case.title}}},"description":{{{case.description}}},"tags":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
updateIncidentMethod: CasesWebhookMethods.PUT,
updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentUrl: 'https://coolsite.net/issue/{{{external.system.id}}}',
};
const secrets = {
user: 'user',
@ -76,7 +75,7 @@ describe('Cases webhook service', () => {
describe('createExternalService', () => {
const requiredUrls = [
'createIncidentUrl',
'incidentViewUrl',
'viewIncidentUrl',
'getIncidentUrl',
'updateIncidentUrl',
];
@ -154,7 +153,7 @@ describe('Cases webhook service', () => {
await service.getIncident('1');
expect(requestMock).toHaveBeenCalledWith({
axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
url: 'https://coolsite.net/issue/1',
logger,
configurationUtilities,
});
@ -231,7 +230,7 @@ describe('Cases webhook service', () => {
title: 'CK-1',
id: '1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});
@ -260,7 +259,7 @@ describe('Cases webhook service', () => {
expect(requestMock.mock.calls[0][0]).toEqual({
axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
url: 'https://coolsite.net/issue',
logger,
method: CasesWebhookMethods.POST,
configurationUtilities,
@ -326,7 +325,7 @@ describe('Cases webhook service', () => {
title: 'CK-1',
id: '1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});
@ -348,7 +347,7 @@ describe('Cases webhook service', () => {
logger,
method: CasesWebhookMethods.PUT,
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
url: 'https://coolsite.net/issue/1',
data: JSON.stringify({
fields: {
title: 'title',
@ -426,7 +425,7 @@ describe('Cases webhook service', () => {
logger,
method: CasesWebhookMethods.POST,
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment',
url: 'https://coolsite.net/issue/1/comment',
data: `{"body":"comment"}`,
});
});
@ -664,7 +663,7 @@ describe('Cases webhook service', () => {
test('getIncident- escapes url', async () => {
await service.getIncident('../../malicious-app/malicious-endpoint/');
expect(requestMock.mock.calls[0][0].url).toEqual(
'https://siem-kibana.atlassian.net/rest/api/2/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F'
'https://coolsite.net/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F'
);
});
@ -681,7 +680,7 @@ describe('Cases webhook service', () => {
};
const res = await service.createIncident(incident);
expect(res.url).toEqual(
'https://siem-kibana.atlassian.net/browse/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F'
'https://coolsite.net/browse/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F'
);
});
@ -700,7 +699,7 @@ describe('Cases webhook service', () => {
await service.updateIncident(incident);
expect(requestMock.mock.calls[0][0].url).toEqual(
'https://siem-kibana.atlassian.net/rest/api/2/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F'
'https://coolsite.net/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F'
);
});
test('createComment- escapes url', async () => {
@ -714,7 +713,7 @@ describe('Cases webhook service', () => {
await service.createComment(commentReq);
expect(requestMock.mock.calls[0][0].url).toEqual(
'https://siem-kibana.atlassian.net/rest/api/2/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F/comment'
'https://coolsite.net/issue/..%2F..%2Fmalicious-app%2Fmalicious-endpoint%2F/comment'
);
});
});

View file

@ -55,7 +55,7 @@ export const createExternalService = (
getIncidentUrl,
hasAuth,
headers,
incidentViewUrl,
viewIncidentUrl,
updateIncidentJson,
updateIncidentMethod,
updateIncidentUrl,
@ -64,7 +64,7 @@ export const createExternalService = (
if (
!getIncidentUrl ||
!createIncidentUrlConfig ||
!incidentViewUrl ||
!viewIncidentUrl ||
!updateIncidentUrl ||
(hasAuth && (!password || !user))
) {
@ -163,7 +163,7 @@ export const createExternalService = (
logger.debug(`response from webhook action "${actionId}": [HTTP ${status}] ${statusText}`);
const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, {
const viewUrl = renderMustacheStringNoEscape(viewIncidentUrl, {
external: {
system: {
id: encodeURIComponent(externalId),
@ -233,7 +233,7 @@ export const createExternalService = (
res,
});
const updatedIncident = await getIncident(incidentId as string);
const viewUrl = renderMustacheStringNoEscape(incidentViewUrl, {
const viewUrl = renderMustacheStringNoEscape(viewIncidentUrl, {
external: {
system: {
id: encodeURIComponent(incidentId),

View file

@ -20,7 +20,7 @@ const validateConfig = (
const {
createCommentUrl,
createIncidentUrl,
incidentViewUrl,
viewIncidentUrl,
getIncidentUrl,
updateIncidentUrl,
} = configObject;
@ -28,7 +28,7 @@ const validateConfig = (
const urls = [
createIncidentUrl,
createCommentUrl,
incidentViewUrl,
viewIncidentUrl,
getIncidentUrl,
updateIncidentUrl,
];

View file

@ -31,7 +31,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
comments: [
{
commentId: 'case-comment-1',
@ -57,7 +57,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});
@ -150,7 +150,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
comments: [
{
commentId: 'case-comment-1',
@ -176,7 +176,7 @@ describe('api', () => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});

View file

@ -24,7 +24,7 @@ const createMock = (): jest.Mocked<ExternalService> => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
})
),
updateIncident: jest.fn().mockImplementation(() =>
@ -32,7 +32,7 @@ const createMock = (): jest.Mocked<ExternalService> => {
id: 'incident-1',
title: 'CK-1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
})
),
createComment: jest.fn(),

View file

@ -114,9 +114,8 @@ const mockNewAPI = () =>
data: {
capabilities: {
'list-project-issuetypes':
'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
'list-issuetype-fields':
'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
'list-issuetype-fields': 'https://coolsite.net/rest/capabilities/list-issuetype-fields',
},
},
})
@ -127,7 +126,7 @@ const mockOldAPI = () =>
createAxiosResponse({
data: {
capabilities: {
navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
navigation: 'https://coolsite.net/rest/capabilities/navigation',
},
},
})
@ -141,7 +140,7 @@ describe('Jira service', () => {
{
// The trailing slash at the end of the url is intended.
// All API calls need to have the trailing slash removed.
config: { apiUrl: 'https://siem-kibana.atlassian.net/', projectKey: 'CK' },
config: { apiUrl: 'https://coolsite.net/', projectKey: 'CK' },
secrets: { apiToken: 'token', email: 'elastic@elastic.com' },
},
logger,
@ -240,7 +239,7 @@ describe('Jira service', () => {
await service.getIncident('1');
expect(requestMock).toHaveBeenCalledWith({
axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
url: 'https://coolsite.net/rest/api/2/issue/1',
logger,
configurationUtilities,
});
@ -313,7 +312,7 @@ describe('Jira service', () => {
title: 'CK-1',
id: '1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});
@ -329,7 +328,7 @@ describe('Jira service', () => {
createAxiosResponse({
data: {
capabilities: {
navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
navigation: 'https://coolsite.net/rest/capabilities/navigation',
},
},
})
@ -365,12 +364,12 @@ describe('Jira service', () => {
title: 'CK-1',
id: '1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
expect(requestMock).toHaveBeenCalledWith({
axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
url: 'https://coolsite.net/rest/api/2/issue',
logger,
method: 'post',
configurationUtilities,
@ -392,7 +391,7 @@ describe('Jira service', () => {
createAxiosResponse({
data: {
capabilities: {
navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
navigation: 'https://coolsite.net/rest/capabilities/navigation',
},
},
})
@ -427,7 +426,7 @@ describe('Jira service', () => {
expect(requestMock).toHaveBeenCalledWith({
axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
url: 'https://coolsite.net/rest/api/2/issue',
logger,
method: 'post',
configurationUtilities,
@ -459,7 +458,7 @@ describe('Jira service', () => {
expect(requestMock).toHaveBeenCalledWith({
axios,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
url: 'https://coolsite.net/rest/api/2/issue',
logger,
method: 'post',
configurationUtilities,
@ -538,7 +537,7 @@ describe('Jira service', () => {
title: 'CK-1',
id: '1',
pushedDate: '2020-04-27T10:59:46.202Z',
url: 'https://siem-kibana.atlassian.net/browse/CK-1',
url: 'https://coolsite.net/browse/CK-1',
});
});
@ -560,7 +559,7 @@ describe('Jira service', () => {
logger,
method: 'put',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1',
url: 'https://coolsite.net/rest/api/2/issue/1',
data: {
fields: {
summary: 'title',
@ -644,7 +643,7 @@ describe('Jira service', () => {
logger,
method: 'post',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment',
url: 'https://coolsite.net/rest/api/2/issue/1/comment',
data: { body: 'comment' },
});
});
@ -686,7 +685,7 @@ describe('Jira service', () => {
const res = await service.getCapabilities();
expect(res).toEqual({
capabilities: {
navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation',
navigation: 'https://coolsite.net/rest/capabilities/navigation',
},
});
});
@ -701,7 +700,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/capabilities',
url: 'https://coolsite.net/rest/capabilities',
});
});
@ -782,7 +781,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields',
url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields',
});
});
@ -856,7 +855,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes',
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
});
});
@ -931,7 +930,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields',
url: 'https://coolsite.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields',
});
});
@ -1044,7 +1043,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
url: 'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
});
});
@ -1112,7 +1111,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: `https://siem-kibana.atlassian.net/rest/api/2/search?jql=project%3D%22CK%22%20and%20summary%20~%22Test%20title%22`,
url: `https://coolsite.net/rest/api/2/search?jql=project%3D%22CK%22%20and%20summary%20~%22Test%20title%22`,
});
});
@ -1171,7 +1170,7 @@ describe('Jira service', () => {
logger,
method: 'get',
configurationUtilities,
url: `https://siem-kibana.atlassian.net/rest/api/2/issue/RJ-107`,
url: `https://coolsite.net/rest/api/2/issue/RJ-107`,
});
});
@ -1206,9 +1205,9 @@ describe('Jira service', () => {
data: {
capabilities: {
'list-project-issuetypes':
'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
'list-issuetype-fields':
'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
'https://coolsite.net/rest/capabilities/list-issuetype-fields',
},
},
})
@ -1225,9 +1224,9 @@ describe('Jira service', () => {
data: {
capabilities: {
'list-project-issuetypes':
'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
'list-issuetype-fields':
'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
'https://coolsite.net/rest/capabilities/list-issuetype-fields',
},
},
})
@ -1237,9 +1236,9 @@ describe('Jira service', () => {
data: {
capabilities: {
'list-project-issuetypes':
'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes',
'https://coolsite.net/rest/capabilities/list-project-issuetypes',
'list-issuetype-fields':
'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields',
'https://coolsite.net/rest/capabilities/list-issuetype-fields',
},
},
})
@ -1289,12 +1288,12 @@ describe('Jira service', () => {
callMocks();
await service.getFields();
const callUrls = [
'https://siem-kibana.atlassian.net/rest/capabilities',
'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes',
'https://siem-kibana.atlassian.net/rest/capabilities',
'https://siem-kibana.atlassian.net/rest/capabilities',
'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10007',
'https://coolsite.net/rest/capabilities',
'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes',
'https://coolsite.net/rest/capabilities',
'https://coolsite.net/rest/capabilities',
'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10006',
'https://coolsite.net/rest/api/2/issue/createmeta/CK/issuetypes/10007',
];
requestMock.mock.calls.forEach((call, i) => {
expect(call[0].url).toEqual(callUrls[i]);

View file

@ -131,7 +131,7 @@ describe('mustache_renderer', () => {
const summary = 'A cool good summary';
const description = 'A cool good description';
const tags = ['cool', 'neat', 'nice'];
const str = 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}';
const str = 'https://coolsite.net/browse/{{{external.system.title}}}';
const objStr =
'{\n' +
@ -177,7 +177,7 @@ describe('mustache_renderer', () => {
},
};
expect(renderMustacheStringNoEscape(str, urlVariables)).toBe(
`https://siem-kibana.atlassian.net/browse/cool_title`
`https://coolsite.net/browse/cool_title`
);
});
it('Inserts variables into url with quotes whens stringified', () => {
@ -190,7 +190,7 @@ describe('mustache_renderer', () => {
},
};
expect(renderMustacheStringNoEscape(str, urlVariablesStr)).toBe(
`https://siem-kibana.atlassian.net/browse/"cool_title"`
`https://coolsite.net/browse/"cool_title"`
);
});
it('Inserts variables into JSON non-escaped when triple brackets and JSON.stringified variables', () => {

View file

@ -1,4 +1,4 @@
{"attributes":{"actionTypeId":".jira","config":{"apiUrl":"https://siem-kibana.atlassian.net/","projectKey":"CASES"},"isMissingSecrets":true,"name":"Jira test"},"coreMigrationVersion":"7.16.0","id":"018d0110-46d1-11ec-a89e-8339c89698d8","migrationVersion":{"action":"7.16.0"},"references":[],"type":"action","updated_at":"2021-11-16T11:33:22.994Z","version":"WzIwNjYsMV0="}
{"attributes":{"actionTypeId":".jira","config":{"apiUrl":"https://coolsite.net/","projectKey":"CASES"},"isMissingSecrets":true,"name":"Jira test"},"coreMigrationVersion":"7.16.0","id":"018d0110-46d1-11ec-a89e-8339c89698d8","migrationVersion":{"action":"7.16.0"},"references":[],"type":"action","updated_at":"2021-11-16T11:33:22.994Z","version":"WzIwNjYsMV0="}
{"attributes":{"closed_at":null,"closed_by":null,"connector":{"fields":[{"key":"issueType","value":"10001"},{"key":"parent","value":null},{"key":"priority","value":null}],"name":"Jira test","type":".jira"},"created_at":"2021-11-16T11:21:19.242Z","created_by":{"email":null,"full_name":"glo@test.co","username":"glo"},"description":"This is the description of the 7.16 case that I'm going to import in future versions.","external_service":null,"owner":"securitySolution","settings":{"syncAlerts":false},"status":"in-progress","tags":["export case"],"title":"7.16 case to export","type":"individual","updated_at":"2021-11-16T11:33:44.787Z","updated_by":{"email":"","full_name":"","username":"test"}},"coreMigrationVersion":"7.16.0","id":"5228e870-46cf-11ec-a89e-8339c89698d8","migrationVersion":{"cases":"7.15.0"},"references":[{"id":"018d0110-46d1-11ec-a89e-8339c89698d8","name":"connectorId","type":"action"}],"type":"cases","updated_at":"2021-11-16T11:33:44.788Z","version":"WzIwNzMsMV0="}
{"attributes":{"action":"update","action_at":"2021-11-16T11:33:44.787Z","action_by":{"email":"","full_name":"","username":"test"},"action_field":["connector"],"new_value":"{\"name\":\"Jira test\",\"type\":\".jira\",\"fields\":{\"issueType\":\"10001\",\"parent\":null,\"priority\":null}}","old_value":"{\"name\":\"none\",\"type\":\".none\",\"fields\":null}","owner":"securitySolution"},"coreMigrationVersion":"7.16.0","id":"0ef96fa0-46d1-11ec-a89e-8339c89698d8","migrationVersion":{"cases-user-actions":"7.16.0"},"references":[{"id":"5228e870-46cf-11ec-a89e-8339c89698d8","name":"associated-cases","type":"cases"},{"id":"018d0110-46d1-11ec-a89e-8339c89698d8","name":"connectorId","type":"action"}],"score":null,"sort":[1637062424787,4305],"type":"cases-user-actions","updated_at":"2021-11-16T11:33:45.498Z","version":"WzIwNzQsMV0="}
{"attributes":{"action":"create","action_at":"2021-11-16T11:21:19.242Z","action_by":{"email":null,"full_name":"glo@test.co","username":"glo"},"action_field":["description","status","tags","title","connector","settings","owner"],"new_value":"{\"type\":\"individual\",\"title\":\"7.16 case to export\",\"tags\":[\"export case\"],\"description\":\"This is the description of the 7.16 case that I'm going to import in future versions.\",\"connector\":{\"name\":\"none\",\"type\":\".none\",\"fields\":null},\"settings\":{\"syncAlerts\":false},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"7.16.0","id":"52b87e40-46cf-11ec-a89e-8339c89698d8","migrationVersion":{"cases-user-actions":"7.16.0"},"references":[{"id":"5228e870-46cf-11ec-a89e-8339c89698d8","name":"associated-cases","type":"cases"}],"score":null,"sort":[1637061679242,4228],"type":"cases-user-actions","updated_at":"2021-11-16T11:21:20.164Z","version":"WzE5MjMsMV0="}
@ -12,4 +12,4 @@
{"attributes":{"action":"create","action_at":"2021-11-16T11:24:09.128Z","action_by":{"email":null,"full_name":"glo@test.co","username":"glo"},"action_field":["comment"],"new_value":"{\"comment\":\"!{lens{\\\"timeRange\\\":{\\\"from\\\":\\\"now-7d\\\",\\\"to\\\":\\\"now\\\",\\\"mode\\\":\\\"relative\\\"},\\\"attributes\\\":{\\\"title\\\":\\\"\\\",\\\"description\\\":\\\"Events acknowledged by the output (includes events dropped by the output). (From beat.stats.libbeat.output.events.acked)\\\",\\\"visualizationType\\\":\\\"lnsXY\\\",\\\"type\\\":\\\"lens\\\",\\\"references\\\":[{\\\"type\\\":\\\"index-pattern\\\",\\\"id\\\":\\\"92888d80-46cf-11ec-a89e-8339c89698d8\\\",\\\"name\\\":\\\"indexpattern-datasource-current-indexpattern\\\"},{\\\"type\\\":\\\"index-pattern\\\",\\\"id\\\":\\\"92888d80-46cf-11ec-a89e-8339c89698d8\\\",\\\"name\\\":\\\"indexpattern-datasource-layer-03c31209-08e8-4917-b7d5-9d77ecf40dd1\\\"}],\\\"state\\\":{\\\"visualization\\\":{\\\"legend\\\":{\\\"isVisible\\\":true,\\\"position\\\":\\\"right\\\"},\\\"valueLabels\\\":\\\"hide\\\",\\\"fittingFunction\\\":\\\"None\\\",\\\"yLeftExtent\\\":{\\\"mode\\\":\\\"full\\\"},\\\"yRightExtent\\\":{\\\"mode\\\":\\\"full\\\"},\\\"axisTitlesVisibilitySettings\\\":{\\\"x\\\":true,\\\"yRight\\\":true,\\\"yLeft\\\":true},\\\"tickLabelsVisibilitySettings\\\":{\\\"x\\\":true,\\\"yRight\\\":true,\\\"yLeft\\\":true},\\\"labelsOrientation\\\":{\\\"x\\\":0,\\\"yRight\\\":0,\\\"yLeft\\\":0},\\\"gridlinesVisibilitySettings\\\":{\\\"x\\\":true,\\\"yRight\\\":true,\\\"yLeft\\\":true},\\\"preferredSeriesType\\\":\\\"line\\\",\\\"layers\\\":[{\\\"layerId\\\":\\\"03c31209-08e8-4917-b7d5-9d77ecf40dd1\\\",\\\"seriesType\\\":\\\"line\\\",\\\"xAccessor\\\":\\\"bd01502a-3d64-470e-8277-928d6a9399e2\\\",\\\"accessors\\\":[\\\"97dfd130-3c4d-477a-8e24-adc95b5a5e86\\\"],\\\"layerType\\\":\\\"data\\\"}]},\\\"query\\\":{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"},\\\"filters\\\":[],\\\"datasourceStates\\\":{\\\"indexpattern\\\":{\\\"layers\\\":{\\\"03c31209-08e8-4917-b7d5-9d77ecf40dd1\\\":{\\\"columns\\\":{\\\"bd01502a-3d64-470e-8277-928d6a9399e2\\\":{\\\"label\\\":\\\"@timestamp\\\",\\\"dataType\\\":\\\"date\\\",\\\"operationType\\\":\\\"date_histogram\\\",\\\"sourceField\\\":\\\"@timestamp\\\",\\\"isBucketed\\\":true,\\\"scale\\\":\\\"interval\\\",\\\"params\\\":{\\\"interval\\\":\\\"auto\\\"}},\\\"97dfd130-3c4d-477a-8e24-adc95b5a5e86\\\":{\\\"label\\\":\\\"Count of records\\\",\\\"dataType\\\":\\\"number\\\",\\\"operationType\\\":\\\"count\\\",\\\"isBucketed\\\":false,\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"Records\\\"}},\\\"columnOrder\\\":[\\\"bd01502a-3d64-470e-8277-928d6a9399e2\\\",\\\"97dfd130-3c4d-477a-8e24-adc95b5a5e86\\\"],\\\"incompleteColumns\\\":{}}}}}}}}}\",\"type\":\"user\",\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"7.16.0","id":"b8076630-46cf-11ec-a89e-8339c89698d8","migrationVersion":{"cases-user-actions":"7.16.0"},"references":[{"id":"5228e870-46cf-11ec-a89e-8339c89698d8","name":"associated-cases","type":"cases"},{"id":"b76d4910-46cf-11ec-a89e-8339c89698d8","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1637061849128,4260],"type":"cases-user-actions","updated_at":"2021-11-16T11:24:10.131Z","version":"WzE5ODcsMV0="}
{"attributes":{"alertId":"f339b9b0e9763b98bcdb7c4a65a10701aaa97a99e232cfd2dab2d8680f7c6c3a","associationType":"case","created_at":"2021-11-16T11:24:42.043Z","created_by":{"email":null,"full_name":"glo@test.co","username":"glo"},"index":".siem-signals-default-000001","owner":"securitySolution","pushed_at":null,"pushed_by":null,"rule":{"id":"f45fd050-46ce-11ec-a89e-8339c89698d8","name":"This is a test"},"type":"alert","updated_at":null,"updated_by":null},"coreMigrationVersion":"7.16.0","id":"cb0acce0-46cf-11ec-a89e-8339c89698d8","migrationVersion":{"cases-comments":"7.16.0"},"references":[{"id":"5228e870-46cf-11ec-a89e-8339c89698d8","name":"associated-cases","type":"cases"}],"score":null,"sort":[1637061882043,6107],"type":"cases-comments","updated_at":"2021-11-16T11:24:42.048Z","version":"WzE5OTYsMV0="}
{"attributes":{"action":"create","action_at":"2021-11-16T11:24:42.043Z","action_by":{"email":null,"full_name":"glo@test.co","username":"glo"},"action_field":["comment"],"new_value":"{\"type\":\"alert\",\"alertId\":\"f339b9b0e9763b98bcdb7c4a65a10701aaa97a99e232cfd2dab2d8680f7c6c3a\",\"index\":\".siem-signals-default-000001\",\"rule\":{\"id\":\"f45fd050-46ce-11ec-a89e-8339c89698d8\",\"name\":\"This is a test\"},\"owner\":\"securitySolution\"}","old_value":null,"owner":"securitySolution"},"coreMigrationVersion":"7.16.0","id":"cb634d20-46cf-11ec-a89e-8339c89698d8","migrationVersion":{"cases-user-actions":"7.16.0"},"references":[{"id":"5228e870-46cf-11ec-a89e-8339c89698d8","name":"associated-cases","type":"cases"},{"id":"cb0acce0-46cf-11ec-a89e-8339c89698d8","name":"associated-cases-comments","type":"cases-comments"}],"score":null,"sort":[1637061882043,4263],"type":"cases-user-actions","updated_at":"2021-11-16T11:24:42.610Z","version":"WzE5OTgsMV0="}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":14,"missingRefCount":0,"missingReferences":[]}
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":14,"missingRefCount":0,"missingReferences":[]}

View file

@ -106,7 +106,7 @@ export const getMockConnectorsResponse = () => [
actionTypeId: '.jira',
name: 'Jira',
config: {
apiUrl: 'https://siem-kibana.atlassian.net',
apiUrl: 'https://coolsite.net',
projectKey: 'RJ',
},
isPreconfigured: false,
@ -235,11 +235,11 @@ export const getExecuteResponses = () => ({
issuetype: {
allowedValues: [
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/issuetype/10006',
self: 'https://coolsite.net/rest/api/2/issuetype/10006',
id: '10006',
description: 'A small, distinct piece of work.',
iconUrl:
'https://siem-kibana.atlassian.net/secure/viewavatar?size=medium&avatarId=10318&avatarType=issuetype',
'https://coolsite.net/secure/viewavatar?size=medium&avatarId=10318&avatarType=issuetype',
name: 'Task',
subtask: false,
avatarId: 10318,
@ -253,21 +253,20 @@ export const getExecuteResponses = () => ({
project: {
allowedValues: [
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/project/10011',
self: 'https://coolsite.net/rest/api/2/project/10011',
id: '10011',
key: 'RJ',
name: 'Refactor Jira',
projectTypeKey: 'business',
simplified: false,
avatarUrls: {
'48x48':
'https://siem-kibana.atlassian.net/secure/projectavatar?pid=10011&avatarId=10423',
'48x48': 'https://coolsite.net/secure/projectavatar?pid=10011&avatarId=10423',
'24x24':
'https://siem-kibana.atlassian.net/secure/projectavatar?size=small&s=small&pid=10011&avatarId=10423',
'https://coolsite.net/secure/projectavatar?size=small&s=small&pid=10011&avatarId=10423',
'16x16':
'https://siem-kibana.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10011&avatarId=10423',
'https://coolsite.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10011&avatarId=10423',
'32x32':
'https://siem-kibana.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10011&avatarId=10423',
'https://coolsite.net/secure/projectavatar?size=medium&s=medium&pid=10011&avatarId=10423',
},
},
],
@ -277,39 +276,39 @@ export const getExecuteResponses = () => ({
priority: {
allowedValues: [
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/1',
iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/highest.svg',
self: 'https://coolsite.net/rest/api/2/priority/1',
iconUrl: 'https://coolsite.net/images/icons/priorities/highest.svg',
name: 'Highest',
id: '1',
},
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/2',
iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/high.svg',
self: 'https://coolsite.net/rest/api/2/priority/2',
iconUrl: 'https://coolsite.net/images/icons/priorities/high.svg',
name: 'High',
id: '2',
},
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/3',
iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/medium.svg',
self: 'https://coolsite.net/rest/api/2/priority/3',
iconUrl: 'https://coolsite.net/images/icons/priorities/medium.svg',
name: 'Medium',
id: '3',
},
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/4',
iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/low.svg',
self: 'https://coolsite.net/rest/api/2/priority/4',
iconUrl: 'https://coolsite.net/images/icons/priorities/low.svg',
name: 'Low',
id: '4',
},
{
self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/5',
iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/lowest.svg',
self: 'https://coolsite.net/rest/api/2/priority/5',
iconUrl: 'https://coolsite.net/images/icons/priorities/lowest.svg',
name: 'Lowest',
id: '5',
},
],
defaultValue: {
self: 'https://siem-kibana.atlassian.net/rest/api/2/priority/3',
iconUrl: 'https://siem-kibana.atlassian.net/images/icons/priorities/medium.svg',
self: 'https://coolsite.net/rest/api/2/priority/3',
iconUrl: 'https://coolsite.net/images/icons/priorities/medium.svg',
name: 'Medium',
id: '3',
},

View file

@ -121,7 +121,7 @@ export const GetStep: FunctionComponent<Props> = ({ display, readOnly }) => (
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="config.incidentViewUrl"
path="config.viewIncidentUrl"
config={{
label: i18n.EXTERNAL_INCIDENT_VIEW_URL,
validations: [
@ -136,9 +136,9 @@ export const GetStep: FunctionComponent<Props> = ({ display, readOnly }) => (
componentProps={{
euiFieldProps: {
readOnly,
'data-test-subj': 'incidentViewUrlText',
'data-test-subj': 'viewIncidentUrlText',
messageVariables: urlVarsExt,
paramsProperty: 'incidentViewUrl',
paramsProperty: 'viewIncidentUrl',
buttonTitle: i18n.ADD_CASES_VARIABLE,
showButtonTitle: true,
},

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { css } from '@emotion/react';
export const styles = {
method: css`
.euiFormRow__labelWrapper {
margin-bottom: 12px;
}
`,
};

View file

@ -15,6 +15,7 @@ import { MustacheTextFieldWrapper } from '../../../mustache_text_field_wrapper';
import { casesVars, commentVars, urlVars } from '../action_variables';
import { JsonFieldWrapper } from '../../../json_field_wrapper';
import { HTTP_VERBS } from '../webhook_connectors';
import { styles } from './update.styles';
import * as i18n from '../translations';
const { emptyField, urlField } = fieldValidators;
@ -47,6 +48,7 @@ export const UpdateStep: FunctionComponent<Props> = ({ display, readOnly }) => (
},
],
}}
css={styles.method}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookUpdateMethodSelect',
@ -137,6 +139,7 @@ export const UpdateStep: FunctionComponent<Props> = ({ display, readOnly }) => (
},
],
}}
css={styles.method}
componentProps={{
euiFieldProps: {
'data-test-subj': 'webhookCreateCommentMethodSelect',

View file

@ -275,13 +275,13 @@ export const GET_INCIDENT_UPDATED_KEY_HELP = i18n.translate(
);
export const EXTERNAL_INCIDENT_VIEW_URL = i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlTextFieldLabel',
'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.viewIncidentUrlTextFieldLabel',
{
defaultMessage: 'External Case View URL',
}
);
export const EXTERNAL_INCIDENT_VIEW_URL_HELP = i18n.translate(
'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.incidentViewUrlHelp',
'xpack.triggersActionsUI.components.builtinActionTypes.casesWebhookAction.viewIncidentUrlHelp',
{
defaultMessage:
'URL to view case in external system. Use the variable selector to add external system id or external system title to the url.',

View file

@ -41,24 +41,23 @@ const invalidJsonBoth = `{"fields":{"summary":"wrong","description":"wrong","pro
const config = {
createCommentJson: '{"body":{{{case.comment}}}}',
createCommentMethod: 'post',
createCommentUrl:
'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createCommentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
createIncidentMethod: 'post',
createIncidentResponseKey: 'id',
createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
createIncidentUrl: 'https://coolsite.net/rest/api/2/issue',
getIncidentResponseCreatedDateKey: 'fields.created',
getIncidentResponseExternalTitleKey: 'key',
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: [{ key: 'content-type', value: 'text' }],
incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
updateIncidentMethod: 'put',
updateIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}',
};
const actionConnector = {
secrets: {
@ -97,7 +96,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
expect(getByTestId('getIncidentResponseExternalTitleKeyText')).toBeInTheDocument();
expect(getByTestId('getIncidentResponseCreatedDateKeyText')).toBeInTheDocument();
expect(getByTestId('getIncidentResponseUpdatedDateKeyText')).toBeInTheDocument();
expect(getByTestId('incidentViewUrlInput')).toBeInTheDocument();
expect(getByTestId('viewIncidentUrlInput')).toBeInTheDocument();
expect(getByTestId('webhookUpdateMethodSelect')).toBeInTheDocument();
expect(getByTestId('updateIncidentUrlInput')).toBeInTheDocument();
expect(getByTestId('webhookUpdateIncidentJson')).toBeInTheDocument();
@ -340,7 +339,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
['webhookCreateUrlText', 'not-valid'],
['webhookUserInput', ''],
['webhookPasswordInput', ''],
['incidentViewUrlInput', 'https://missingexternalid.com'],
['viewIncidentUrlInput', 'https://missingexternalid.com'],
['createIncidentResponseKeyText', ''],
['getIncidentUrlInput', 'https://missingexternalid.com'],
['getIncidentResponseExternalTitleKeyText', ''],
@ -357,7 +356,7 @@ describe('CasesWebhookActionConnectorFields renders', () => {
['updateIncidentJson', invalidJsonBoth, ['{{{case.title}}}', '{{{case.description}}}']],
['createCommentJson', invalidJsonBoth, ['{{{case.comment}}}']],
[
'incidentViewUrl',
'viewIncidentUrl',
'https://missingexternalid.com',
['{{{external.system.id}}}', '{{{external.system.title}}}'],
],

View file

@ -42,7 +42,7 @@ const fields = {
'config.getIncidentResponseExternalTitleKey',
'config.getIncidentResponseCreatedDateKey',
'config.getIncidentResponseUpdatedDateKey',
'config.incidentViewUrl',
'config.viewIncidentUrl',
],
step4: [
'config.updateIncidentMethod',

View file

@ -180,173 +180,181 @@ describe('action_form', () => {
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('action_form in alert', () => {
async function setup(customActions?: RuleAction[], customRecoveredActionGroup?: string) {
const actionTypeRegistry = actionTypeRegistryMock.create();
async function setup(
customActions?: RuleAction[],
customRecoveredActionGroup?: string,
isExperimental?: boolean
) {
const actionTypeRegistry = actionTypeRegistryMock.create();
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce(allActions);
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
actions: {
show: true,
save: true,
delete: true,
},
};
actionTypeRegistry.list.mockReturnValue([
actionType,
disabledByConfigActionType,
disabledByLicenseActionType,
disabledByActionType,
preconfiguredOnly,
]);
actionTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.get.mockReturnValue(actionType);
const initialAlert = {
name: 'test',
params: {},
consumer: 'alerts',
alertTypeId: alertType.id,
schedule: {
interval: '1m',
},
actions: customActions
? customActions
: [
{
group: 'default',
id: 'test',
actionTypeId: actionType.id,
params: {
message: '',
},
},
],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
} as unknown as Rule;
loadActionTypes.mockResolvedValue([
{
id: actionType.id,
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: '.index',
name: 'Index',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: 'preconfigured',
name: 'Preconfigured only',
enabled: true,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: 'disabled-by-config',
name: 'Disabled by config',
enabled: false,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
{
id: 'disabled-by-license',
name: 'Disabled by license',
enabled: false,
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
{
id: '.jira',
name: 'Disabled by action type',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
const defaultActionMessage = 'Alert [{{context.metadata.name}}] has exceeded the threshold';
const wrapper = mountWithIntl(
<ActionForm
actions={initialAlert.actions}
messageVariables={{
params: [
{ name: 'testVar1', description: 'test var1' },
{ name: 'testVar2', description: 'test var2' },
],
state: [],
context: [{ name: 'contextVar', description: 'context var1' }],
}}
featureId="alerting"
defaultActionGroupId={'default'}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => {
const recoveryActionGroupId = customRecoveredActionGroup
? customRecoveredActionGroup
: 'recovered';
return isActionGroupDisabledForActionTypeId(
actionGroupId === recoveryActionGroupId ? RecoveredActionGroup.id : actionGroupId,
actionTypeId
);
}}
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
actionGroups={[
{ id: 'default', name: 'Default', defaultActionMessage },
const { loadAllActions } = jest.requireMock('../../lib/action_connector_api');
loadAllActions.mockResolvedValueOnce(allActions);
const mocks = coreMock.createSetup();
const [
{
application: { capabilities },
},
] = await mocks.getStartServices();
// eslint-disable-next-line react-hooks/rules-of-hooks
useKibanaMock().services.application.capabilities = {
...capabilities,
actions: {
show: true,
save: true,
delete: true,
},
};
const newActionType = {
...actionType,
isExperimental,
};
actionTypeRegistry.list.mockReturnValue([
newActionType,
disabledByConfigActionType,
disabledByLicenseActionType,
disabledByActionType,
preconfiguredOnly,
]);
actionTypeRegistry.has.mockReturnValue(true);
actionTypeRegistry.get.mockReturnValue(newActionType);
const initialAlert = {
name: 'test',
params: {},
consumer: 'alerts',
alertTypeId: alertType.id,
schedule: {
interval: '1m',
},
actions: customActions
? customActions
: [
{
id: customRecoveredActionGroup ? customRecoveredActionGroup : 'recovered',
name: customRecoveredActionGroup ? 'I feel better' : 'Recovered',
group: 'default',
id: 'test',
actionTypeId: newActionType.id,
params: {
message: '',
},
},
]}
setActionGroupIdByIndex={(group: string, index: number) => {
initialAlert.actions[index].group = group;
}}
setActions={(_updatedActions: RuleAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
}
actionTypeRegistry={actionTypeRegistry}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
/>
);
],
tags: [],
muteAll: false,
enabled: false,
mutedInstanceIds: [],
} as unknown as Rule;
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
loadActionTypes.mockResolvedValue([
{
id: newActionType.id,
name: 'Test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: '.index',
name: 'Index',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: 'preconfigured',
name: 'Preconfigured only',
enabled: true,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
{
id: 'disabled-by-config',
name: 'Disabled by config',
enabled: false,
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
{
id: 'disabled-by-license',
name: 'Disabled by license',
enabled: false,
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
{
id: '.jira',
name: 'Disabled by action type',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
return wrapper;
}
const defaultActionMessage = 'Alert [{{context.metadata.name}}] has exceeded the threshold';
const wrapper = mountWithIntl(
<ActionForm
actions={initialAlert.actions}
messageVariables={{
params: [
{ name: 'testVar1', description: 'test var1' },
{ name: 'testVar2', description: 'test var2' },
],
state: [],
context: [{ name: 'contextVar', description: 'context var1' }],
}}
featureId="alerting"
defaultActionGroupId={'default'}
isActionGroupDisabledForActionType={(actionGroupId: string, actionTypeId: string) => {
const recoveryActionGroupId = customRecoveredActionGroup
? customRecoveredActionGroup
: 'recovered';
return isActionGroupDisabledForActionTypeId(
actionGroupId === recoveryActionGroupId ? RecoveredActionGroup.id : actionGroupId,
actionTypeId
);
}}
setActionIdByIndex={(id: string, index: number) => {
initialAlert.actions[index].id = id;
}}
actionGroups={[
{ id: 'default', name: 'Default', defaultActionMessage },
{
id: customRecoveredActionGroup ? customRecoveredActionGroup : 'recovered',
name: customRecoveredActionGroup ? 'I feel better' : 'Recovered',
},
]}
setActionGroupIdByIndex={(group: string, index: number) => {
initialAlert.actions[index].group = group;
}}
setActions={(_updatedActions: RuleAction[]) => {}}
setActionParamsProperty={(key: string, value: any, index: number) =>
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
}
actionTypeRegistry={actionTypeRegistry}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
/>
);
// Wait for active space to resolve before requesting the component to update
await act(async () => {
await nextTick();
wrapper.update();
});
return wrapper;
}
describe('action_form in alert', () => {
it('renders available action cards', async () => {
const wrapper = await setup();
const actionOption = wrapper.find(
@ -605,4 +613,28 @@ describe('action_form', () => {
).toHaveLength(2);
});
});
describe('beta badge (action_type_form)', () => {
it(`does not render beta badge when isExperimental=undefined`, async () => {
const wrapper = await setup();
expect(wrapper.find('EuiKeyPadMenuItem EuiBetaBadge').exists()).toBeFalsy();
expect(
wrapper.find('EuiBetaBadge[data-test-subj="action-type-form-beta-badge"]').exists()
).toBeFalsy();
});
it(`does not render beta badge when isExperimental=false`, async () => {
const wrapper = await setup(undefined, undefined, false);
expect(wrapper.find('EuiKeyPadMenuItem EuiBetaBadge').exists()).toBeFalsy();
expect(
wrapper.find('EuiBetaBadge[data-test-subj="action-type-form-beta-badge"]').exists()
).toBeFalsy();
});
it(`renders beta badge when isExperimental=true`, async () => {
const wrapper = await setup(undefined, undefined, true);
expect(wrapper.find('EuiKeyPadMenuItem EuiBetaBadge').exists()).toBeTruthy();
expect(
wrapper.find('EuiBetaBadge[data-test-subj="action-type-form-beta-badge"]').exists()
).toBeTruthy();
});
});
});

View file

@ -20,6 +20,7 @@ import {
EuiLink,
} from '@elastic/eui';
import { ActionGroup, RuleActionParam } from '@kbn/alerting-plugin/common';
import { betaBadgeProps } from './beta_badge_props';
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
import {
ActionTypeModel,
@ -256,6 +257,10 @@ export const ActionForm = ({
isDisabled={!checkEnabledResult.isEnabled}
data-test-subj={`${item.id}-${featureId}-ActionTypeSelectOption`}
label={actionTypesIndex[item.id].name}
betaBadgeLabel={item.isExperimental ? betaBadgeProps.label : undefined}
betaBadgeTooltipContent={
item.isExperimental ? betaBadgeProps.tooltipContent : undefined
}
onClick={() => addActionType(item)}
>
<EuiIcon

View file

@ -24,9 +24,11 @@ import {
EuiBadge,
EuiErrorBoundary,
EuiToolTip,
EuiBetaBadge,
} from '@elastic/eui';
import { isEmpty, partition, some } from 'lodash';
import { ActionVariable, RuleActionParam } from '@kbn/alerting-plugin/common';
import { betaBadgeProps } from './beta_badge_props';
import {
IErrorObject,
RuleAction,
@ -341,6 +343,15 @@ export const ActionTypeForm = ({
</div>
</EuiText>
</EuiFlexItem>
{actionTypeRegistered && actionTypeRegistered.isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
data-test-subj="action-type-form-beta-badge"
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
}
extraAction={

View file

@ -41,121 +41,255 @@ describe('connector_add_flyout', () => {
},
};
});
it('renders action type menu with proper EuiCards for registered action types', async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy();
afterEach(() => {
actionTypeRegistry.get.mockReset();
jest.clearAllMocks();
});
it(`doesn't renders action types that are disabled via config`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
describe('rendering', () => {
it('renders action type menu with proper EuiCards for registered action types', async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: true,
name: 'Test',
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeTruthy();
});
expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeFalsy();
it(`doesn't renders action types that are disabled via config`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: false,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('[data-test-subj="my-action-type-card"]').exists()).toBeFalsy();
});
it(`renders action types as disabled when disabled by license`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
wrapper.find('EuiToolTip [data-test-subj="my-action-type-card"]').exists()
).toBeTruthy();
});
});
it(`renders action types as disabled when disabled by license`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
describe('beta badge', () => {
it(`does not render beta badge when isExperimental=undefined`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
wrapper.find('EuiToolTip [data-test-subj="my-action-type-card"] EuiBetaBadge').exists()
).toBeFalsy();
});
it(`does not render beta badge when isExperimental=false`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
isExperimental: false,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
wrapper.find('EuiToolTip [data-test-subj="my-action-type-card"] EuiBetaBadge').exists()
).toBeFalsy();
});
expect(wrapper.find('EuiToolTip [data-test-subj="my-action-type-card"]').exists()).toBeTruthy();
it(`renders beta badge when isExperimental=true`, async () => {
const onActionTypeChange = jest.fn();
const actionType = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
isExperimental: true,
});
actionTypeRegistry.get.mockReturnValueOnce(actionType);
loadActionTypes.mockResolvedValueOnce([
{
id: actionType.id,
enabled: false,
name: 'Test',
enabledInConfig: true,
enabledInLicense: false,
minimumLicenseRequired: 'gold',
supportedFeatureIds: ['alerting'],
},
]);
const wrapper = mountWithIntl(
<ActionTypeMenu
onActionTypeChange={onActionTypeChange}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(
wrapper.find('EuiToolTip [data-test-subj="my-action-type-card"] EuiBetaBadge').exists()
).toBeTruthy();
});
});
});

View file

@ -22,7 +22,9 @@ import {
EuiFormRow,
EuiButtonEmpty,
EuiIconTip,
EuiBetaBadge,
} from '@elastic/eui';
import { betaBadgeProps } from './beta_badge_props';
import { RuleAction, ActionTypeIndex, ActionConnector } from '../../../types';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionAccordionFormProps } from './action_form';
@ -179,6 +181,14 @@ export const AddConnectorInline = ({
/>
</EuiFlexItem>
)}
{actionTypeRegistered && actionTypeRegistered.isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
}
extraAction={

View file

@ -75,4 +75,84 @@ describe('connector_add_modal', () => {
expect(wrapper.exists('.euiModalHeader')).toBeTruthy();
expect(wrapper.exists('[data-test-subj="saveActionButtonModal"]')).toBeTruthy();
});
describe('beta badge', () => {
it(`does not render beta badge when isExperimental=false`, async () => {
const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
isExperimental: false,
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValue(actionTypeModel);
actionTypeRegistry.has.mockReturnValue(true);
const actionType: ActionType = {
id: 'my-action-type',
name: 'test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
};
const wrapper = mountWithIntl(
<ConnectorAddModal
onClose={() => {}}
actionType={actionType}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiBetaBadge').exists()).toBeFalsy();
});
it(`renders beta badge when isExperimental=true`, async () => {
const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({
id: 'my-action-type',
iconClass: 'test',
isExperimental: true,
selectMessage: 'test',
validateParams: (): Promise<GenericValidationResult<unknown>> => {
const validationResult = { errors: {} };
return Promise.resolve(validationResult);
},
actionConnectorFields: null,
});
actionTypeRegistry.get.mockReturnValue(actionTypeModel);
actionTypeRegistry.has.mockReturnValue(true);
const actionType: ActionType = {
id: 'my-action-type',
name: 'test',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'basic',
supportedFeatureIds: ['alerting'],
};
const wrapper = mountWithIntl(
<ConnectorAddModal
onClose={() => {}}
actionType={actionType}
actionTypeRegistry={actionTypeRegistry}
/>
);
await act(async () => {
await nextTick();
wrapper.update();
});
expect(wrapper.find('EuiBetaBadge').exists()).toBeTruthy();
});
});
});

View file

@ -19,9 +19,11 @@ import {
EuiFlexItem,
EuiIcon,
EuiFlexGroup,
EuiBetaBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import './connector_add_modal.scss';
import { betaBadgeProps } from './beta_badge_props';
import { hasSaveActionsCapability } from '../../lib/capabilities';
import { ActionType, ActionConnector, ActionTypeRegistryContract } from '../../../types';
import { useKibana } from '../../../common/lib/kibana';
@ -140,18 +142,30 @@ const ConnectorAddModal = ({
<EuiIcon type={actionTypeModel.iconClass} size="xl" />
</EuiFlexItem>
) : null}
<EuiFlexItem>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="{actionTypeName} connector"
id="xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle"
values={{
actionTypeName: actionType.name,
}}
/>
</h3>
</EuiTitle>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" justifyContent="center" alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="{actionTypeName} connector"
id="xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle"
values={{
actionTypeName: actionType.name,
}}
/>
</h3>
</EuiTitle>
</EuiFlexItem>
{actionTypeModel && actionTypeModel.isExperimental && (
<EuiFlexItem className="betaBadgeFlexItem" grow={false}>
<EuiBetaBadge
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeaderTitle>

View file

@ -45,20 +45,32 @@ const FlyoutHeaderComponent: React.FC<Props> = ({
<EuiIcon type={icon} size="xl" />
</EuiFlexItem>
) : null}
<EuiFlexItem>
<EuiFlexItem grow={false}>
{actionTypeName && actionTypeMessage ? (
<>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="{actionTypeName} connector"
id="xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle"
values={{
actionTypeName,
}}
/>
</h3>
</EuiTitle>
<EuiFlexGroup gutterSize="s" justifyContent="center" alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="{actionTypeName} connector"
id="xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle"
values={{
actionTypeName,
}}
/>
</h3>
</EuiTitle>
</EuiFlexItem>
{actionTypeName && isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiText size="s" color="subdued">
{actionTypeMessage}
</EuiText>
@ -96,14 +108,6 @@ const FlyoutHeaderComponent: React.FC<Props> = ({
</EuiTitle>
)}
</EuiFlexItem>
{actionTypeName && isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutHeader>
);

View file

@ -16,6 +16,7 @@ import {
createAppMockRenderer,
} from '../../../components/builtin_action_types/test_utils';
import CreateConnectorFlyout from '.';
import { betaBadgeProps } from '../beta_badge_props';
jest.mock('../../../lib/action_connector_api', () => ({
...(jest.requireActual('../../../lib/action_connector_api') as any),
@ -79,6 +80,7 @@ describe('CreateConnectorFlyout', () => {
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(getByTestId('create-connector-flyout')).toBeInTheDocument();
expect(getByTestId('create-connector-flyout-header')).toBeInTheDocument();
@ -294,6 +296,47 @@ describe('CreateConnectorFlyout', () => {
expect(getByText('Test connector')).toBeInTheDocument();
expect(getByText(`selectMessage-${actionTypeModel.id}`)).toBeInTheDocument();
});
it('does not show beta badge when isExperimental is undefined', async () => {
const { queryByText } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(queryByText(betaBadgeProps.label)).not.toBeInTheDocument();
});
it('does not show beta badge when isExperimental is false', async () => {
actionTypeRegistry.get.mockReturnValue({ ...actionTypeModel, isExperimental: false });
const { queryByText } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(queryByText(betaBadgeProps.label)).not.toBeInTheDocument();
});
it('shows beta badge when isExperimental is true', async () => {
actionTypeRegistry.get.mockReturnValue({ ...actionTypeModel, isExperimental: true });
const { getByText } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);
await act(() => Promise.resolve());
expect(getByText(betaBadgeProps.label)).toBeInTheDocument();
});
});
describe('Submitting', () => {

View file

@ -22,16 +22,26 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { betaBadgeProps } from '../beta_badge_props';
import { EditConnectorTabs } from '../../../../types';
const FlyoutHeaderComponent: React.FC<{
isExperimental?: boolean;
isPreconfigured: boolean;
connectorName: string;
connectorTypeDesc: string;
selectedTab: EditConnectorTabs;
setTab: () => void;
icon?: IconType | null;
}> = ({ icon, isPreconfigured, connectorName, connectorTypeDesc, selectedTab, setTab }) => {
}> = ({
icon,
isExperimental = false,
isPreconfigured,
connectorName,
connectorTypeDesc,
selectedTab,
setTab,
}) => {
const { euiTheme } = useEuiTheme();
return (
@ -42,17 +52,22 @@ const FlyoutHeaderComponent: React.FC<{
<EuiIcon type={icon} size="m" data-test-subj="edit-connector-flyout-header-icon" />
</EuiFlexItem>
) : null}
<EuiFlexItem>
<EuiFlexItem grow={false}>
{isPreconfigured ? (
<>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="{connectorName}"
id="xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle"
values={{ connectorName }}
/>
&emsp;
<EuiFlexGroup gutterSize="s" justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="{connectorName}"
id="xpack.triggersActionsUI.sections.preconfiguredConnectorForm.flyoutTitle"
values={{ connectorName }}
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label="Preconfigured"
data-test-subj="preconfiguredBadge"
@ -63,8 +78,16 @@ const FlyoutHeaderComponent: React.FC<{
}
)}
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{isExperimental && (
<EuiBetaBadge
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
<EuiText size="s">
<FormattedMessage
defaultMessage="{connectorTypeDesc}"
@ -74,14 +97,26 @@ const FlyoutHeaderComponent: React.FC<{
</EuiText>
</>
) : (
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Edit connector"
id="xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle"
/>
</h3>
</EuiTitle>
<EuiFlexGroup gutterSize="s" justifyContent="center" alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Edit connector"
id="xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
{isExperimental && (
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={betaBadgeProps.label}
tooltipContent={betaBadgeProps.tooltipContent}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -17,6 +17,7 @@ import {
} from '../../../components/builtin_action_types/test_utils';
import EditConnectorFlyout from '.';
import { ActionConnector, EditConnectorTabs, GenericValidationResult } from '../../../../types';
import { betaBadgeProps } from '../beta_badge_props';
const updateConnectorResponse = {
connector_type_id: 'test',
@ -191,6 +192,33 @@ describe('EditConnectorFlyout', () => {
expect(getByTestId('preconfiguredBadge')).toBeInTheDocument();
});
it('does not show beta badge when isExperimental is false', async () => {
const { queryByText } = appMockRenderer.render(
<EditConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
connector={{ ...connector, isPreconfigured: true }}
onConnectorUpdated={onConnectorUpdated}
/>
);
await act(() => Promise.resolve());
expect(queryByText(betaBadgeProps.label)).not.toBeInTheDocument();
});
it('shows beta badge when isExperimental is true', async () => {
actionTypeRegistry.get.mockReturnValue({ ...actionTypeModel, isExperimental: true });
const { getByText } = appMockRenderer.render(
<EditConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
connector={{ ...connector, isPreconfigured: true }}
onConnectorUpdated={onConnectorUpdated}
/>
);
await act(() => Promise.resolve());
expect(getByText(betaBadgeProps.label)).toBeInTheDocument();
});
});
describe('Tabs', () => {

View file

@ -246,6 +246,7 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
setTab={handleSetTab}
selectedTab={selectedTab}
icon={actionTypeModel?.iconClass}
isExperimental={actionTypeModel?.isExperimental}
/>
<EuiFlyoutBody>
{selectedTab === EditConnectorTabs.Configuration ? (

View file

@ -20,25 +20,23 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
const config = {
createCommentJson: '{"body":{{{case.comment}}}}',
createCommentMethod: 'post',
createCommentUrl:
'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createCommentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
createIncidentMethod: 'post',
createIncidentResponseKey: 'id',
createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
createIncidentUrl: 'https://coolsite.net/rest/api/2/issue',
getIncidentResponseCreatedDateKey: 'fields.created',
getIncidentResponseExternalTitleKey: 'key',
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: { ['content-type']: 'application/json' },
incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
updateIncidentMethod: 'put',
updateIncidentUrl:
'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}',
};
const mockCasesWebhook = {
@ -81,7 +79,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
...config,
createCommentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}/comments`,
createIncidentUrl: casesWebhookSimulatorURL,
incidentViewUrl: `${casesWebhookSimulatorURL}/{{{external.system.title}}}`,
viewIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.title}}}`,
getIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`,
updateIncidentUrl: `${casesWebhookSimulatorURL}/{{{external.system.id}}}`,
},

View file

@ -24,25 +24,23 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
const config = {
createCommentJson: '{"body":{{{case.comment}}}}',
createCommentMethod: 'post',
createCommentUrl:
'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createCommentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}/comment',
createIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
createIncidentMethod: 'post',
createIncidentResponseKey: 'id',
createIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue',
createIncidentUrl: 'https://coolsite.net/rest/api/2/issue',
getIncidentResponseCreatedDateKey: 'fields.created',
getIncidentResponseExternalTitleKey: 'key',
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: { ['content-type']: 'application/json', ['kbn-xsrf']: 'abcd' },
incidentViewUrl: 'https://siem-kibana.atlassian.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
viewIncidentUrl: 'https://coolsite.net/browse/{{{external.system.title}}}',
getIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"labels":{{{case.tags}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',
updateIncidentMethod: 'put',
updateIncidentUrl:
'https://siem-kibana.atlassian.net/rest/api/2/issue/{{{external.system.id}}}',
updateIncidentUrl: 'https://coolsite.net/rest/api/2/issue/{{{external.system.id}}}',
};
const requiredFields = [
'createIncidentJson',
@ -51,7 +49,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
'getIncidentResponseCreatedDateKey',
'getIncidentResponseExternalTitleKey',
'getIncidentResponseUpdatedDateKey',
'incidentViewUrl',
'viewIncidentUrl',
'getIncidentUrl',
'updateIncidentJson',
'updateIncidentUrl',
@ -94,7 +92,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
...mockCasesWebhook.config,
createCommentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}/comment`,
createIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue`,
incidentViewUrl: `${casesWebhookSimulatorURL}/browse/{{{external.system.title}}}`,
viewIncidentUrl: `${casesWebhookSimulatorURL}/browse/{{{external.system.title}}}`,
getIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`,
updateIncidentUrl: `${casesWebhookSimulatorURL}/rest/api/2/issue/{{{external.system.id}}}`,
};
@ -174,7 +172,7 @@ export default function casesWebhookTest({ getService }: FtrProviderContext) {
...mockCasesWebhook.config,
createCommentUrl: `${badUrl}/{{{external.system.id}}}/comments`,
createIncidentUrl: badUrl,
incidentViewUrl: `${badUrl}/{{{external.system.title}}}`,
viewIncidentUrl: `${badUrl}/{{{external.system.title}}}`,
getIncidentUrl: `${badUrl}/{{{external.system.id}}}`,
updateIncidentUrl: `${badUrl}/{{{external.system.id}}}`,
},

View file

@ -271,7 +271,7 @@ export const getCasesWebhookConnector = () => ({
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: { [`content-type`]: 'application/json' },
incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}',
viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}',
getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',

View file

@ -76,7 +76,7 @@ export default ({ getService }: FtrProviderContext): void => {
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: { [`content-type`]: 'application/json' },
incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}',
viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}',
getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',

View file

@ -110,7 +110,7 @@ export default ({ getService }: FtrProviderContext): void => {
getIncidentResponseUpdatedDateKey: 'fields.updated',
hasAuth: true,
headers: { [`content-type`]: 'application/json' },
incidentViewUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}',
viewIncidentUrl: 'http://some.non.existent.com/browse/{{{external.system.title}}}',
getIncidentUrl: 'http://some.non.existent.com/{{{external.system.id}}}',
updateIncidentJson:
'{"fields":{"summary":{{{case.title}}},"description":{{{case.description}}},"project":{"key":"ROC"},"issuetype":{"id":"10024"}}}',