[Security Solution][Case] ServiceNow ITSM: Add category & subcategory fields (#90547)
This commit is contained in:
parent
01b3d07590
commit
a9f2c91673
36 changed files with 666 additions and 129 deletions
|
@ -595,6 +595,8 @@ The following table describes the properties of the `incident` object.
|
|||
| severity | The name of the severity in ServiceNow. | string _(optional)_ |
|
||||
| urgency | The name of the urgency in ServiceNow. | string _(optional)_ |
|
||||
| impact | The name of the impact in ServiceNow. | string _(optional)_ |
|
||||
| category | The name of the category in ServiceNow. | string _(optional)_ |
|
||||
| subcategory | The name of the subcategory in ServiceNow. | string _(optional)_ |
|
||||
|
||||
#### `subActionParams (getFields)`
|
||||
|
||||
|
|
|
@ -88,6 +88,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
caller_id: 'elastic',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
@ -111,6 +113,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
comments: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
@ -123,6 +127,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
comments: 'Another comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
@ -146,6 +152,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
work_notes: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
@ -158,6 +166,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
work_notes: 'Another comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
@ -229,6 +239,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
|
@ -251,6 +263,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
|
@ -262,6 +276,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
comments: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
@ -285,6 +301,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
},
|
||||
|
@ -296,6 +314,8 @@ describe('api', () => {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
work_notes: 'A comment',
|
||||
description: 'Incident description',
|
||||
short_description: 'Incident title',
|
||||
|
|
|
@ -112,6 +112,8 @@ const executorParams: ExecutorSubActionPushParams = {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
|
|
|
@ -45,6 +45,8 @@ const CommonAttributes = {
|
|||
short_description: schema.string(),
|
||||
description: schema.nullable(schema.string()),
|
||||
externalId: schema.nullable(schema.string()),
|
||||
category: schema.nullable(schema.string()),
|
||||
subcategory: schema.nullable(schema.string()),
|
||||
};
|
||||
|
||||
// Schema for ServiceNow Incident Management (ITSM)
|
||||
|
@ -62,13 +64,11 @@ export const ExecutorSubActionPushParamsSchemaITSM = schema.object({
|
|||
export const ExecutorSubActionPushParamsSchemaSIR = schema.object({
|
||||
incident: schema.object({
|
||||
...CommonAttributes,
|
||||
category: schema.nullable(schema.string()),
|
||||
dest_ip: schema.nullable(schema.string()),
|
||||
malware_hash: schema.nullable(schema.string()),
|
||||
malware_url: schema.nullable(schema.string()),
|
||||
priority: schema.nullable(schema.string()),
|
||||
source_ip: schema.nullable(schema.string()),
|
||||
subcategory: schema.nullable(schema.string()),
|
||||
priority: schema.nullable(schema.string()),
|
||||
}),
|
||||
comments: CommentsSchema,
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts
|
||||
export const JiraFieldsRT = rt.type({
|
||||
issueType: rt.union([rt.string, rt.null]),
|
||||
priority: rt.union([rt.string, rt.null]),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts
|
||||
export const ResilientFieldsRT = rt.type({
|
||||
incidentTypes: rt.union([rt.array(rt.string), rt.null]),
|
||||
severityCode: rt.union([rt.string, rt.null]),
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts
|
||||
export const ServiceNowITSMFieldsRT = rt.type({
|
||||
impact: rt.union([rt.string, rt.null]),
|
||||
severity: rt.union([rt.string, rt.null]),
|
||||
urgency: rt.union([rt.string, rt.null]),
|
||||
category: rt.union([rt.string, rt.null]),
|
||||
subcategory: rt.union([rt.string, rt.null]),
|
||||
});
|
||||
|
||||
export type ServiceNowITSMFieldsType = rt.TypeOf<typeof ServiceNowITSMFieldsRT>;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
|
||||
// New fields should also be added at: x-pack/plugins/case/server/connectors/case/schema.ts
|
||||
export const ServiceNowSIRFieldsRT = rt.type({
|
||||
category: rt.union([rt.string, rt.null]),
|
||||
destIp: rt.union([rt.boolean, rt.null]),
|
||||
|
|
|
@ -153,6 +153,8 @@ describe('case connector', () => {
|
|||
impact: 'Medium',
|
||||
severity: 'Medium',
|
||||
urgency: 'Medium',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
|
@ -218,7 +220,13 @@ describe('case connector', () => {
|
|||
id: 'servicenow',
|
||||
name: 'Servicenow',
|
||||
type: '.servicenow',
|
||||
fields: { impact: null, severity: null, urgency: null },
|
||||
fields: {
|
||||
impact: null,
|
||||
severity: null,
|
||||
urgency: null,
|
||||
category: null,
|
||||
subcategory: null,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
|
@ -293,6 +301,8 @@ describe('case connector', () => {
|
|||
impact: 'Medium',
|
||||
severity: 'Medium',
|
||||
urgency: 'Medium',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
excess: null,
|
||||
},
|
||||
},
|
||||
|
@ -470,6 +480,8 @@ describe('case connector', () => {
|
|||
impact: 'Medium',
|
||||
severity: 'Medium',
|
||||
urgency: 'Medium',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -517,7 +529,13 @@ describe('case connector', () => {
|
|||
id: 'servicenow',
|
||||
name: 'Servicenow',
|
||||
type: '.servicenow',
|
||||
fields: { impact: null, severity: null, urgency: null },
|
||||
fields: {
|
||||
impact: null,
|
||||
severity: null,
|
||||
urgency: null,
|
||||
category: null,
|
||||
subcategory: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -590,6 +608,8 @@ describe('case connector', () => {
|
|||
impact: 'Medium',
|
||||
severity: 'Medium',
|
||||
urgency: 'Medium',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
excess: null,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -53,6 +53,8 @@ const ServiceNowFieldsSchema = schema.object({
|
|||
impact: schema.nullable(schema.string()),
|
||||
severity: schema.nullable(schema.string()),
|
||||
urgency: schema.nullable(schema.string()),
|
||||
category: schema.nullable(schema.string()),
|
||||
subcategory: schema.nullable(schema.string()),
|
||||
});
|
||||
|
||||
const NoneFieldsSchema = schema.nullable(schema.object({}));
|
||||
|
|
|
@ -9,9 +9,9 @@ import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../
|
|||
import { ExternalServiceFormatter } from '../types';
|
||||
|
||||
const format: ExternalServiceFormatter<ServiceNowITSMFieldsType>['format'] = (theCase) => {
|
||||
const { severity = null, urgency = null, impact = null } =
|
||||
const { severity = null, urgency = null, impact = null, category = null, subcategory = null } =
|
||||
(theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {};
|
||||
return { severity, urgency, impact };
|
||||
return { severity, urgency, impact, category, subcategory };
|
||||
};
|
||||
|
||||
export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter<ServiceNowITSMFieldsType> = {
|
||||
|
|
|
@ -10,7 +10,9 @@ import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter';
|
|||
|
||||
describe('ITSM formatter', () => {
|
||||
const theCase = {
|
||||
connector: { fields: { severity: '2', urgency: '2', impact: '2' } },
|
||||
connector: {
|
||||
fields: { severity: '2', urgency: '2', impact: '2', category: 'software', subcategory: 'os' },
|
||||
},
|
||||
} as CaseResponse;
|
||||
|
||||
it('it formats correctly', async () => {
|
||||
|
@ -21,6 +23,12 @@ describe('ITSM formatter', () => {
|
|||
it('it formats correctly when fields do not exist ', async () => {
|
||||
const invalidFields = { connector: { fields: null } } as CaseResponse;
|
||||
const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []);
|
||||
expect(res).toEqual({ severity: null, urgency: null, impact: null });
|
||||
expect(res).toEqual({
|
||||
severity: null,
|
||||
urgency: null,
|
||||
impact: null,
|
||||
category: null,
|
||||
subcategory: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -153,6 +153,18 @@ export const executeResponses = {
|
|||
value: 'inbound_ddos',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Software',
|
||||
value: 'software',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: 'software',
|
||||
label: 'Operation System',
|
||||
value: 'os',
|
||||
element: 'subcategory',
|
||||
},
|
||||
...['severity', 'urgency', 'impact', 'priority']
|
||||
.map((element) => [
|
||||
{
|
||||
|
|
|
@ -92,6 +92,6 @@ export const fillIbmResilientConnectorOptions = (
|
|||
ibmResilientConnector.incidentTypes.forEach((incidentType) => {
|
||||
cy.get(SELECT_INCIDENT_TYPE).type(`${incidentType}{enter}`, { force: true });
|
||||
});
|
||||
cy.get(CONNECTOR_RESILIENT).click();
|
||||
cy.get(CONNECTOR_RESILIENT).click({ force: true });
|
||||
cy.get(SELECT_SEVERITY).select(ibmResilientConnector.severity);
|
||||
};
|
||||
|
|
|
@ -58,6 +58,18 @@ export const choices = [
|
|||
value: 'inbound_ddos',
|
||||
element: 'subcategory',
|
||||
},
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Software',
|
||||
value: 'software',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: 'software',
|
||||
label: 'Operation System',
|
||||
value: 'os',
|
||||
element: 'subcategory',
|
||||
},
|
||||
...['severity', 'urgency', 'impact', 'priority']
|
||||
.map((element) => [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { EuiSelectOption } from '@elastic/eui';
|
||||
import { Choice } from './types';
|
||||
|
||||
export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
|
@ -20,12 +20,18 @@ jest.mock('../../../../common/lib/kibana');
|
|||
jest.mock('./use_get_choices', () => ({
|
||||
useGetChoices: (args: { onSuccess: () => void }) => {
|
||||
onChoicesSuccess = args.onSuccess;
|
||||
return { isLoading: false, mockChoices };
|
||||
return { isLoading: false, choices: mockChoices };
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ServiceNowITSM Fields', () => {
|
||||
const fields = { severity: '1', urgency: '2', impact: '3' };
|
||||
const fields = {
|
||||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -37,6 +43,8 @@ describe('ServiceNowITSM Fields', () => {
|
|||
expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('all params fields are rendered - isEdit: false', () => {
|
||||
|
@ -58,6 +66,42 @@ describe('ServiceNowITSM Fields', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('it transforms the categories to options correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
act(() => {
|
||||
onChoicesSuccess(mockChoices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([
|
||||
{ value: 'Priviledge Escalation', text: 'Priviledge Escalation' },
|
||||
{
|
||||
value: 'Criminal activity/investigation',
|
||||
text: 'Criminal activity/investigation',
|
||||
},
|
||||
{ value: 'Denial of Service', text: 'Denial of Service' },
|
||||
{
|
||||
value: 'software',
|
||||
text: 'Software',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the subcategories to options correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
act(() => {
|
||||
onChoicesSuccess(mockChoices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([
|
||||
{
|
||||
text: 'Operation System',
|
||||
value: 'os',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('it transforms the options correctly', async () => {
|
||||
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
|
||||
act(() => {
|
||||
|
@ -81,7 +125,7 @@ describe('ServiceNowITSM Fields', () => {
|
|||
|
||||
expect(onChange).toHaveBeenCalledWith(fields);
|
||||
|
||||
const testers = ['severity', 'urgency', 'impact'];
|
||||
const testers = ['severity', 'urgency', 'impact', 'subcategory'];
|
||||
testers.forEach((subj) =>
|
||||
test(`${subj.toUpperCase()}`, async () => {
|
||||
await waitFor(() => {
|
||||
|
@ -99,5 +143,22 @@ describe('ServiceNowITSM Fields', () => {
|
|||
});
|
||||
})
|
||||
);
|
||||
|
||||
test('it should set subcategory to null when changing category', async () => {
|
||||
await waitFor(() => {
|
||||
const select = wrapper.find(EuiSelect).filter(`[data-test-subj="categorySelect"]`)!;
|
||||
select.prop('onChange')!({
|
||||
target: {
|
||||
value: 'network',
|
||||
},
|
||||
} as React.ChangeEvent<HTMLSelectElement>);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...fields,
|
||||
subcategory: null,
|
||||
category: 'network',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,22 +17,39 @@ import {
|
|||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ConnectorCard } from '../card';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import { Options, Choice } from './types';
|
||||
import { Fields, Choice } from './types';
|
||||
import { choicesToEuiOptions } from './helpers';
|
||||
|
||||
const useGetChoicesFields = ['urgency', 'severity', 'impact'];
|
||||
const defaultOptions: Options = {
|
||||
const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory'];
|
||||
const defaultFields: Fields = {
|
||||
urgency: [],
|
||||
severity: [],
|
||||
impact: [],
|
||||
category: [],
|
||||
subcategory: [],
|
||||
};
|
||||
|
||||
const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
||||
ConnectorFieldsProps<ServiceNowITSMFieldsType>
|
||||
> = ({ isEdit = true, fields, connector, onChange }) => {
|
||||
const init = useRef(true);
|
||||
const { severity = null, urgency = null, impact = null } = fields ?? {};
|
||||
const { severity = null, urgency = null, impact = null, category = null, subcategory = null } =
|
||||
fields ?? {};
|
||||
const { http, notifications } = useKibana().services;
|
||||
const [options, setOptions] = useState<Options>(defaultOptions);
|
||||
const [choices, setChoices] = useState<Fields>(defaultFields);
|
||||
|
||||
const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]);
|
||||
const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]);
|
||||
const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]);
|
||||
const impactOptions = useMemo(() => choicesToEuiOptions(choices.impact), [choices.impact]);
|
||||
|
||||
const subcategoryOptions = useMemo(
|
||||
() =>
|
||||
choicesToEuiOptions(
|
||||
choices.subcategory.filter((choice) => choice.dependent_value === category)
|
||||
),
|
||||
[choices.subcategory, category]
|
||||
);
|
||||
|
||||
const listItems = useMemo(
|
||||
() => [
|
||||
|
@ -40,7 +57,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
? [
|
||||
{
|
||||
title: i18n.URGENCY,
|
||||
description: options.urgency.find((option) => `${option.value}` === urgency)?.text,
|
||||
description: urgencyOptions.find((option) => `${option.value}` === urgency)?.text,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
@ -48,7 +65,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
? [
|
||||
{
|
||||
title: i18n.SEVERITY,
|
||||
description: options.severity.find((option) => `${option.value}` === severity)?.text,
|
||||
description: severityOptions.find((option) => `${option.value}` === severity)?.text,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
@ -56,27 +73,53 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
? [
|
||||
{
|
||||
title: i18n.IMPACT,
|
||||
description: options.impact.find((option) => `${option.value}` === impact)?.text,
|
||||
description: impactOptions.find((option) => `${option.value}` === impact)?.text,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(category != null && category.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.CATEGORY,
|
||||
description: categoryOptions.find((option) => `${option.value}` === category)?.text,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(subcategory != null && subcategory.length > 0
|
||||
? [
|
||||
{
|
||||
title: i18n.SUBCATEGORY,
|
||||
description: subcategoryOptions.find((option) => `${option.value}` === subcategory)
|
||||
?.text,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[urgency, options.urgency, options.severity, options.impact, severity, impact]
|
||||
[
|
||||
category,
|
||||
categoryOptions,
|
||||
impact,
|
||||
impactOptions,
|
||||
severity,
|
||||
severityOptions,
|
||||
subcategory,
|
||||
subcategoryOptions,
|
||||
urgency,
|
||||
urgencyOptions,
|
||||
]
|
||||
);
|
||||
|
||||
const onChoicesSuccess = (choices: Choice[]) =>
|
||||
setOptions(
|
||||
choices.reduce(
|
||||
(acc, choice) => ({
|
||||
const onChoicesSuccess = (values: Choice[]) => {
|
||||
setChoices(
|
||||
values.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[choice.element]: [
|
||||
...(acc[choice.element] != null ? acc[choice.element] : []),
|
||||
{ value: choice.value, text: choice.label },
|
||||
],
|
||||
[value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value],
|
||||
}),
|
||||
defaultOptions
|
||||
defaultFields
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const { isLoading: isLoadingChoices } = useGetChoices({
|
||||
http,
|
||||
|
@ -100,17 +143,17 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
useEffect(() => {
|
||||
if (init.current) {
|
||||
init.current = false;
|
||||
onChange({ urgency, severity, impact });
|
||||
onChange({ urgency, severity, impact, category, subcategory });
|
||||
}
|
||||
}, [impact, onChange, severity, urgency]);
|
||||
}, [category, impact, onChange, severity, subcategory, urgency]);
|
||||
|
||||
return isEdit ? (
|
||||
<div data-test-subj={'connector-fields-sn'}>
|
||||
<div data-test-subj={'connector-fields-sn-itsm'}>
|
||||
<EuiFormRow fullWidth label={i18n.URGENCY}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="urgencySelect"
|
||||
options={options.urgency}
|
||||
options={urgencyOptions}
|
||||
value={urgency ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
|
@ -125,7 +168,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="severitySelect"
|
||||
options={options.severity}
|
||||
options={severityOptions}
|
||||
value={severity ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
|
@ -139,7 +182,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="impactSelect"
|
||||
options={options.impact}
|
||||
options={impactOptions}
|
||||
value={impact ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
|
@ -149,6 +192,37 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent<
|
|||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CATEGORY}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="categorySelect"
|
||||
options={categoryOptions}
|
||||
value={category ?? undefined}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
hasNoInitialSelection
|
||||
onChange={(e) => onChange({ ...fields, category: e.target.value, subcategory: null })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.SUBCATEGORY}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="subcategorySelect"
|
||||
options={subcategoryOptions}
|
||||
// Needs an empty string instead of undefined to select the blank option when changing categories
|
||||
value={subcategory ?? ''}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
hasNoInitialSelection
|
||||
onChange={(e) => onChangeCb('subcategory', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
) : (
|
||||
<ConnectorCard
|
||||
|
|
|
@ -97,6 +97,10 @@ describe('ServiceNowSIR Fields', () => {
|
|||
text: 'Criminal activity/investigation',
|
||||
},
|
||||
{ value: 'Denial of Service', text: 'Denial of Service' },
|
||||
{
|
||||
text: 'Software',
|
||||
value: 'software',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -176,7 +180,7 @@ describe('ServiceNowSIR Fields', () => {
|
|||
})
|
||||
);
|
||||
|
||||
const testers = ['priority', 'category', 'subcategory'];
|
||||
const testers = ['priority', 'subcategory'];
|
||||
testers.forEach((subj) =>
|
||||
test(`${subj.toUpperCase()}`, async () => {
|
||||
await waitFor(() => {
|
||||
|
@ -194,5 +198,24 @@ describe('ServiceNowSIR Fields', () => {
|
|||
});
|
||||
})
|
||||
);
|
||||
|
||||
test('it should set subcategory to null when changing category', async () => {
|
||||
const select = wrapper.find(EuiSelect).filter(`[data-test-subj="categorySelect"]`)!;
|
||||
select.prop('onChange')!({
|
||||
target: {
|
||||
value: 'network',
|
||||
},
|
||||
} as React.ChangeEvent<HTMLSelectElement>);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...fields,
|
||||
subcategory: null,
|
||||
category: 'network',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSelectOption,
|
||||
EuiCheckbox,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
ConnectorTypes,
|
||||
|
@ -24,6 +17,7 @@ import { ConnectorFieldsProps } from '../types';
|
|||
import { ConnectorCard } from '../card';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import { Choice, Fields } from './types';
|
||||
import { choicesToEuiOptions } from './helpers';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -34,9 +28,6 @@ const defaultFields: Fields = {
|
|||
priority: [],
|
||||
};
|
||||
|
||||
const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
||||
|
||||
const ServiceNowSIRFieldsComponent: React.FunctionComponent<
|
||||
ConnectorFieldsProps<ServiceNowSIRFieldsType>
|
||||
> = ({ isEdit = true, fields, connector, onChange }) => {
|
||||
|
@ -179,7 +170,7 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent<
|
|||
}, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]);
|
||||
|
||||
return isEdit ? (
|
||||
<div data-test-subj={'connector-fields-sn'}>
|
||||
<div data-test-subj={'connector-fields-sn-sir'}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.ALERT_FIELDS_LABEL}>
|
||||
|
@ -259,7 +250,7 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent<
|
|||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
hasNoInitialSelection
|
||||
onChange={(e) => onChangeCb('category', e.target.value)}
|
||||
onChange={(e) => onChange({ ...fields, category: e.target.value, subcategory: null })}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
@ -269,7 +260,8 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent<
|
|||
fullWidth
|
||||
data-test-subj="subcategorySelect"
|
||||
options={subcategoryOptions}
|
||||
value={subcategory ?? undefined}
|
||||
// Needs an empty string instead of undefined to select the blank option when changing categories
|
||||
value={subcategory ?? ''}
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
hasNoInitialSelection
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
export interface Choice {
|
||||
value: string;
|
||||
label: string;
|
||||
|
@ -15,4 +13,3 @@ export interface Choice {
|
|||
}
|
||||
|
||||
export type Fields = Record<string, Choice[]>;
|
||||
export type Options = Record<string, EuiSelectOption[]>;
|
||||
|
|
|
@ -96,10 +96,10 @@ describe('Connector', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// await waitFor(() => {
|
||||
// wrapper.update();
|
||||
// expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy();
|
||||
// });
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('it is loading when fetching connectors', async () => {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { connectorsMock } from '../../containers/configure/mock';
|
|||
import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types';
|
||||
import { useGetSeverity } from '../connectors/resilient/use_get_severity';
|
||||
import { useGetIssueTypes } from '../connectors/jira/use_get_issue_types';
|
||||
import { useGetChoices } from '../connectors/servicenow/use_get_choices';
|
||||
import { useGetFieldsByIssueType } from '../connectors/jira/use_get_fields_by_issue_type';
|
||||
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
|
||||
import {
|
||||
|
@ -30,6 +31,7 @@ import {
|
|||
useGetSeverityResponse,
|
||||
useGetIssueTypesResponse,
|
||||
useGetFieldsByIssueTypeResponse,
|
||||
useGetChoicesResponse,
|
||||
} from './mock';
|
||||
import { FormContext } from './form_context';
|
||||
import { CreateCaseForm } from './form';
|
||||
|
@ -49,6 +51,7 @@ jest.mock('../connectors/jira/use_get_issue_types');
|
|||
jest.mock('../connectors/jira/use_get_fields_by_issue_type');
|
||||
jest.mock('../connectors/jira/use_get_single_issue');
|
||||
jest.mock('../connectors/jira/use_get_issues');
|
||||
jest.mock('../connectors/servicenow/use_get_choices');
|
||||
|
||||
const useConnectorsMock = useConnectors as jest.Mock;
|
||||
const useCaseConfigureMock = useCaseConfigure as jest.Mock;
|
||||
|
@ -58,6 +61,7 @@ const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
|
|||
const useGetSeverityMock = useGetSeverity as jest.Mock;
|
||||
const useGetIssueTypesMock = useGetIssueTypes as jest.Mock;
|
||||
const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock;
|
||||
const useGetChoicesMock = useGetChoices as jest.Mock;
|
||||
const postCase = jest.fn();
|
||||
const pushCaseToExternalService = jest.fn();
|
||||
|
||||
|
@ -109,6 +113,7 @@ describe('Create case', () => {
|
|||
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
|
||||
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
|
||||
useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
|
||||
useGetChoicesMock.mockReturnValue(useGetChoicesResponse);
|
||||
|
||||
(useGetTags as jest.Mock).mockImplementation(() => ({
|
||||
tags: sampleTags,
|
||||
|
@ -219,6 +224,8 @@ describe('Create case', () => {
|
|||
impact: null,
|
||||
severity: null,
|
||||
urgency: null,
|
||||
category: null,
|
||||
subcategory: null,
|
||||
},
|
||||
id: 'servicenow-1',
|
||||
name: 'My Connector',
|
||||
|
@ -399,7 +406,7 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it(`it should submit and push to servicenow connector`, async () => {
|
||||
it(`it should submit and push to servicenow itsm connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
|
@ -415,10 +422,14 @@ describe('Create case', () => {
|
|||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeFalsy();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click');
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy();
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => {
|
||||
wrapper
|
||||
|
@ -429,6 +440,20 @@ describe('Create case', () => {
|
|||
});
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="categorySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: 'software' },
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="subcategorySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: 'os' },
|
||||
});
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -438,7 +463,13 @@ describe('Create case', () => {
|
|||
id: 'servicenow-1',
|
||||
name: 'My Connector',
|
||||
type: '.servicenow',
|
||||
fields: { impact: '2', severity: '2', urgency: '2' },
|
||||
fields: {
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
urgency: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -448,7 +479,110 @@ describe('Create case', () => {
|
|||
id: 'servicenow-1',
|
||||
name: 'My Connector',
|
||||
type: '.servicenow',
|
||||
fields: { impact: '2', severity: '2', urgency: '2' },
|
||||
fields: {
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
urgency: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(onFormSubmitSuccess).toHaveBeenCalledWith({
|
||||
id: sampleId,
|
||||
...sampleData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`it should submit and push to servicenow sir connector`, async () => {
|
||||
useConnectorsMock.mockReturnValue({
|
||||
...sampleConnectorData,
|
||||
connectors: connectorsMock,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<FormContext onSuccess={onFormSubmitSuccess}>
|
||||
<CreateCaseForm />
|
||||
<SubmitCaseButton />
|
||||
</FormContext>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fillForm(wrapper);
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeFalsy();
|
||||
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
|
||||
wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-sir"]`).simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find(`[data-test-subj="connector-fields-sn-sir"]`).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="destIpCheckbox"] input')
|
||||
.first()
|
||||
.simulate('change', { target: { checked: false } });
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="prioritySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '1' },
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="categorySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: 'Denial of Service' },
|
||||
});
|
||||
|
||||
wrapper
|
||||
.find('select[data-test-subj="subcategorySelect"]')
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { value: '26' },
|
||||
});
|
||||
|
||||
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(postCase).toBeCalledWith({
|
||||
...sampleData,
|
||||
connector: {
|
||||
id: 'servicenow-sir',
|
||||
name: 'My Connector SIR',
|
||||
type: '.servicenow-sir',
|
||||
fields: {
|
||||
destIp: false,
|
||||
sourceIp: true,
|
||||
malwareHash: true,
|
||||
malwareUrl: true,
|
||||
priority: '1',
|
||||
category: 'Denial of Service',
|
||||
subcategory: '26',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(pushCaseToExternalService).toHaveBeenCalledWith({
|
||||
caseId: sampleId,
|
||||
connector: {
|
||||
id: 'servicenow-sir',
|
||||
name: 'My Connector SIR',
|
||||
type: '.servicenow-sir',
|
||||
fields: {
|
||||
destIp: false,
|
||||
sourceIp: true,
|
||||
malwareHash: true,
|
||||
malwareUrl: true,
|
||||
priority: '1',
|
||||
category: 'Denial of Service',
|
||||
subcategory: '26',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { CasePostRequest } from '../../../../../case/common/api';
|
||||
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
|
||||
import { choices } from '../connectors/mock';
|
||||
|
||||
export const sampleTags = ['coke', 'pepsi'];
|
||||
export const sampleData: CasePostRequest = {
|
||||
|
@ -93,3 +94,8 @@ export const useGetFieldsByIssueTypeResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const useGetChoicesResponse = {
|
||||
isLoading: false,
|
||||
choices,
|
||||
};
|
||||
|
|
|
@ -61,6 +61,15 @@ export const connectorsMock: ActionConnector[] = [
|
|||
},
|
||||
isPreconfigured: false,
|
||||
},
|
||||
{
|
||||
id: 'servicenow-sir',
|
||||
actionTypeId: '.servicenow-sir',
|
||||
name: 'My Connector SIR',
|
||||
config: {
|
||||
apiUrl: 'https://instance1.service-now.com',
|
||||
},
|
||||
isPreconfigured: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const actionTypesMock: ActionTypeConnector[] = [
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 { EuiSelectOption } from '@elastic/eui';
|
||||
import { Choice } from './types';
|
||||
|
||||
export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
|
@ -28,6 +28,8 @@ const actionParams = {
|
|||
severity: '1',
|
||||
urgency: '2',
|
||||
impact: '3',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
externalId: null,
|
||||
},
|
||||
comments: [],
|
||||
|
@ -55,7 +57,20 @@ const defaultProps = {
|
|||
|
||||
const useGetChoicesResponse = {
|
||||
isLoading: false,
|
||||
choices: ['severity', 'urgency', 'impact']
|
||||
choices: [
|
||||
{
|
||||
dependent_value: '',
|
||||
label: 'Software',
|
||||
value: 'software',
|
||||
element: 'category',
|
||||
},
|
||||
{
|
||||
dependent_value: 'software',
|
||||
label: 'Operation System',
|
||||
value: 'os',
|
||||
element: 'subcategory',
|
||||
},
|
||||
...['severity', 'urgency', 'impact']
|
||||
.map((element) => [
|
||||
{
|
||||
dependent_value: '',
|
||||
|
@ -83,6 +98,7 @@ const useGetChoicesResponse = {
|
|||
},
|
||||
])
|
||||
.flat(),
|
||||
],
|
||||
};
|
||||
|
||||
describe('ServiceNowITSMParamsFields renders', () => {
|
||||
|
@ -101,6 +117,8 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="short_descriptionInput"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy();
|
||||
|
@ -153,6 +171,36 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('it transforms the categories to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoices(useGetChoicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([
|
||||
{
|
||||
value: 'software',
|
||||
text: 'Software',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the subcategories to options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
onChoices(useGetChoicesResponse.choices);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([
|
||||
{
|
||||
text: 'Operation System',
|
||||
value: 'os',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it transforms the options correctly', async () => {
|
||||
const wrapper = mount(<ServiceNowITSMParamsFields {...defaultProps} />);
|
||||
act(() => {
|
||||
|
@ -179,6 +227,8 @@ describe('ServiceNowITSMParamsFields renders', () => {
|
|||
{ dataTestSubj: '[data-test-subj="urgencySelect"]', key: 'urgency' },
|
||||
{ dataTestSubj: '[data-test-subj="severitySelect"]', key: 'severity' },
|
||||
{ dataTestSubj: '[data-test-subj="impactSelect"]', key: 'impact' },
|
||||
{ dataTestSubj: '[data-test-subj="categorySelect"]', key: 'category' },
|
||||
{ dataTestSubj: '[data-test-subj="subcategorySelect"]', key: 'subcategory' },
|
||||
];
|
||||
|
||||
simpleFields.forEach((field) =>
|
||||
|
|
|
@ -16,17 +16,22 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
import { ServiceNowITSMActionParams, Choice, Options } from './types';
|
||||
import { ServiceNowITSMActionParams, Choice, Fields } from './types';
|
||||
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
|
||||
import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import { choicesToEuiOptions } from './helpers';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const useGetChoicesFields = ['urgency', 'severity', 'impact'];
|
||||
const defaultOptions: Options = {
|
||||
const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory'];
|
||||
const defaultFields: Fields = {
|
||||
category: [],
|
||||
subcategory: [],
|
||||
urgency: [],
|
||||
severity: [],
|
||||
impact: [],
|
||||
priority: [],
|
||||
};
|
||||
|
||||
const ServiceNowParamsFields: React.FunctionComponent<
|
||||
|
@ -48,7 +53,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
[actionParams.subActionParams]
|
||||
);
|
||||
|
||||
const [options, setOptions] = useState<Options>(defaultOptions);
|
||||
const [choices, setChoices] = useState<Fields>(defaultFields);
|
||||
|
||||
const editSubActionProperty = useCallback(
|
||||
(key: string, value: any) => {
|
||||
|
@ -73,19 +78,32 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
[editSubActionProperty]
|
||||
);
|
||||
|
||||
const onChoicesSuccess = (choices: Choice[]) =>
|
||||
setOptions(
|
||||
choices.reduce(
|
||||
(acc, choice) => ({
|
||||
const onChoicesSuccess = useCallback((values: Choice[]) => {
|
||||
setChoices(
|
||||
values.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[choice.element]: [
|
||||
...(acc[choice.element] != null ? acc[choice.element] : []),
|
||||
{ value: choice.value, text: choice.label },
|
||||
],
|
||||
[value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value],
|
||||
}),
|
||||
defaultOptions
|
||||
defaultFields
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]);
|
||||
const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]);
|
||||
const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]);
|
||||
const impactOptions = useMemo(() => choicesToEuiOptions(choices.impact), [choices.impact]);
|
||||
|
||||
const subcategoryOptions = useMemo(
|
||||
() =>
|
||||
choicesToEuiOptions(
|
||||
choices.subcategory.filter(
|
||||
(subcategory) => subcategory.dependent_value === incident.category
|
||||
)
|
||||
),
|
||||
[choices.subcategory, incident.category]
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingChoices } = useGetChoices({
|
||||
http,
|
||||
|
@ -140,7 +158,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={options.urgency}
|
||||
options={urgencyOptions}
|
||||
value={incident.urgency ?? ''}
|
||||
onChange={(e) => editSubActionProperty('urgency', e.target.value)}
|
||||
/>
|
||||
|
@ -155,7 +173,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={options.severity}
|
||||
options={severityOptions}
|
||||
value={incident.severity ?? ''}
|
||||
onChange={(e) => editSubActionProperty('severity', e.target.value)}
|
||||
/>
|
||||
|
@ -169,7 +187,7 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={options.impact}
|
||||
options={impactOptions}
|
||||
value={incident.impact ?? ''}
|
||||
onChange={(e) => editSubActionProperty('impact', e.target.value)}
|
||||
/>
|
||||
|
@ -177,6 +195,47 @@ const ServiceNowParamsFields: React.FunctionComponent<
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.CATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="categorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={categoryOptions}
|
||||
value={incident.category ?? undefined}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { ...incident, category: e.target.value, subcategory: null },
|
||||
comments,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth label={i18n.SUBCATEGORY_LABEL}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
data-test-subj="subcategorySelect"
|
||||
hasNoInitialSelection
|
||||
isLoading={isLoadingChoices}
|
||||
disabled={isLoadingChoices}
|
||||
options={subcategoryOptions}
|
||||
// Needs an empty string instead of undefined to select the blank option when changing categories
|
||||
value={incident.subcategory ?? ''}
|
||||
onChange={(e) => editSubActionProperty('subcategory', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
error={errors['subActionParams.incident.short_description']}
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiSelectOption,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { ActionParamsProps } from '../../../../types';
|
||||
|
@ -23,6 +22,7 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var
|
|||
import * as i18n from './translations';
|
||||
import { useGetChoices } from './use_get_choices';
|
||||
import { ServiceNowSIRActionParams, Fields, Choice } from './types';
|
||||
import { choicesToEuiOptions } from './helpers';
|
||||
|
||||
const useGetChoicesFields = ['category', 'subcategory', 'priority'];
|
||||
const defaultFields: Fields = {
|
||||
|
@ -31,9 +31,6 @@ const defaultFields: Fields = {
|
|||
priority: [],
|
||||
};
|
||||
|
||||
const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] =>
|
||||
choices.map((choice) => ({ value: choice.value, text: choice.label }));
|
||||
|
||||
const ServiceNowSIRParamsFields: React.FunctionComponent<
|
||||
ActionParamsProps<ServiceNowSIRActionParams>
|
||||
> = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => {
|
||||
|
@ -218,16 +215,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent<
|
|||
disabled={isLoadingChoices}
|
||||
options={priorityOptions}
|
||||
value={incident.priority ?? undefined}
|
||||
onChange={(e) => {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
incident: { ...incident, priority: e.target.value },
|
||||
comments,
|
||||
},
|
||||
index
|
||||
);
|
||||
}}
|
||||
onChange={(e) => editSubActionProperty('priority', e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
import { UserConfiguredActionConnector } from '../../../../types';
|
||||
import {
|
||||
ExecutorSubActionPushParamsITSM,
|
||||
|
@ -45,4 +44,3 @@ export interface Choice {
|
|||
}
|
||||
|
||||
export type Fields = Record<string, Choice[]>;
|
||||
export type Options = Record<string, EuiSelectOption[]>;
|
||||
|
|
|
@ -119,6 +119,8 @@ function getServiceNowActionParams(): ServiceNowActionParams {
|
|||
impact: '2',
|
||||
severity: '2',
|
||||
urgency: '2',
|
||||
category: null,
|
||||
subcategory: null,
|
||||
externalId: null,
|
||||
},
|
||||
comments: [],
|
||||
|
|
|
@ -30,6 +30,8 @@ export function initPlugin(router: IRouter, path: string) {
|
|||
severity: schema.string({ defaultValue: '1' }),
|
||||
urgency: schema.string({ defaultValue: '1' }),
|
||||
impact: schema.string({ defaultValue: '1' }),
|
||||
category: schema.maybe(schema.string()),
|
||||
subcategory: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -40,6 +40,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
severity: '1',
|
||||
short_description: 'a title',
|
||||
urgency: '1',
|
||||
category: 'software',
|
||||
subcategory: 'software',
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
|
|
|
@ -80,7 +80,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
id: connector.id,
|
||||
name: connector.name,
|
||||
type: connector.actionTypeId,
|
||||
fields: { urgency: '2', impact: '2', severity: '2' },
|
||||
fields: {
|
||||
urgency: '2',
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
}).connector,
|
||||
})
|
||||
.expect(200);
|
||||
|
@ -143,7 +149,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
id: connector.id,
|
||||
name: connector.name,
|
||||
type: connector.actionTypeId,
|
||||
fields: { urgency: '2', impact: '2', severity: '2' },
|
||||
fields: {
|
||||
urgency: '2',
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
}).connector,
|
||||
})
|
||||
.expect(200);
|
||||
|
@ -196,7 +208,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
id: connector.id,
|
||||
name: connector.name,
|
||||
type: connector.actionTypeId,
|
||||
fields: { urgency: '2', impact: '2', severity: '2' },
|
||||
fields: {
|
||||
urgency: '2',
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
}).connector,
|
||||
})
|
||||
.expect(200);
|
||||
|
@ -268,7 +286,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
id: connector.id,
|
||||
name: connector.name,
|
||||
type: connector.actionTypeId,
|
||||
fields: { urgency: '2', impact: '2', severity: '2' },
|
||||
fields: {
|
||||
urgency: '2',
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
}).connector,
|
||||
})
|
||||
.expect(200);
|
||||
|
|
|
@ -359,7 +359,13 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
id: connector.id,
|
||||
name: connector.name,
|
||||
type: connector.actionTypeId,
|
||||
fields: { urgency: '2', impact: '2', severity: '2' },
|
||||
fields: {
|
||||
urgency: '2',
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
category: 'software',
|
||||
subcategory: 'os',
|
||||
},
|
||||
}).connector,
|
||||
})
|
||||
.expect(200);
|
||||
|
|
|
@ -476,6 +476,8 @@ export default ({ getService }: FtrProviderContext): void => {
|
|||
impact: null,
|
||||
severity: null,
|
||||
urgency: null,
|
||||
category: null,
|
||||
subcategory: null,
|
||||
},
|
||||
},
|
||||
created_by: {
|
||||
|
|
Loading…
Reference in a new issue