Revert "[Security Solution] [Cases] Move create page components and dependencies to Cases (#94444)" (#94975)

This reverts commit c497239d55.
This commit is contained in:
Steph Milovic 2021-03-18 18:25:39 -06:00 committed by GitHub
parent d1040f0105
commit 5f4939be76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
362 changed files with 327 additions and 17518 deletions

View file

@ -108,4 +108,3 @@ pageLoadAssetSize:
fileUpload: 25664
banners: 17946
mapsEms: 26072
cases: 102558

View file

@ -7,7 +7,6 @@
export * from './cases';
export * from './connectors';
export * from './helpers';
export * from './runtime_types';
export * from './saved_object';
export * from './user';

View file

@ -5,9 +5,8 @@
* 2.0.
*/
// The DEFAULT_MAX_SIGNALS value should match the one in `x-pack/plugins/security_solution/common/constants.ts`
// If either changes, engineer should ensure both values are updated
const DEFAULT_MAX_SIGNALS = 100;
import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants';
export const APP_ID = 'cases';
/**

View file

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

View file

@ -2,13 +2,12 @@
"configPath": ["xpack", "cases"],
"id": "cases",
"kibanaVersion": "kibana",
"extraPublicDirs": ["common"],
"requiredPlugins": ["actions", "esUiShared", "kibanaReact", "triggersActionsUi"],
"requiredPlugins": ["actions", "securitySolution"],
"optionalPlugins": [
"spaces",
"security"
],
"server": true,
"ui": true,
"ui": false,
"version": "8.0.0"
}

View file

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { has } from 'lodash/fp';
export interface AppError {
name: string;
message: string;
body: {
message: string;
};
}
export interface KibanaError extends AppError {
body: {
message: string;
statusCode: number;
};
}
export interface CasesAppError extends AppError {
body: {
message: string;
status_code: number;
};
}
export const isKibanaError = (error: unknown): error is KibanaError =>
has('message', error) && has('body.message', error) && has('body.statusCode', error);
export const isCasesAppError = (error: unknown): error is CasesAppError =>
has('message', error) && has('body.message', error) && has('body.status_code', error);
export const isAppError = (error: unknown): error is AppError =>
isKibanaError(error) || isCasesAppError(error);

View file

@ -1,30 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks';
import {
createKibanaContextProviderMock,
createStartServicesMock,
createWithKibanaMock,
} from '../kibana_react.mock';
export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') };
export const useKibana = jest.fn().mockReturnValue({
services: createStartServicesMock(),
});
export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http);
export const useTimeZone = jest.fn();
export const useDateFormat = jest.fn();
export const useBasePath = jest.fn(() => '/test/base/path');
export const useToasts = jest
.fn()
.mockReturnValue(notificationServiceMock.createStartContract().toasts);
export const useCurrentUser = jest.fn();
export const withKibana = jest.fn(createWithKibanaMock());
export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock());
export const useGetUserSavedObjectPermissions = jest.fn();

View file

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

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { RecursivePartial } from '@elastic/eui/src/components/common';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { StartServices } from '../../../types';
import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common';
export const createStartServicesMock = (): StartServices =>
(coreMock.createStart() as unknown) as StartServices;
export const createWithKibanaMock = () => {
const services = createStartServicesMock();
return (Component: unknown) => (props: unknown) => {
return React.createElement(Component as string, { ...(props as object), kibana: { services } });
};
};
export const createKibanaContextProviderMock = () => {
const services = createStartServicesMock();
return ({ children }: { children: React.ReactNode }) =>
React.createElement(KibanaContextProvider, { services }, children);
};
export const getMockTheme = (partialTheme: RecursivePartial<EuiTheme>): EuiTheme =>
partialTheme as EuiTheme;

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
KibanaContextProvider,
useKibana,
} from '../../../../../../../src/plugins/kibana_react/public';
import { StartServices } from '../../../types';
const useTypedKibana = () => useKibana<StartServices>();
export { KibanaContextProvider, useTypedKibana as useKibana };

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreStart } from 'kibana/public';
type GlobalServices = Pick<CoreStart, 'http'>;
export class KibanaServices {
private static kibanaVersion?: string;
private static services?: GlobalServices;
public static init({ http, kibanaVersion }: GlobalServices & { kibanaVersion: string }) {
this.services = { http };
this.kibanaVersion = kibanaVersion;
}
public static get(): GlobalServices {
if (!this.services) {
this.throwUninitializedError();
}
return this.services;
}
public static getKibanaVersion(): string {
if (!this.kibanaVersion) {
this.throwUninitializedError();
}
return this.kibanaVersion;
}
private static throwUninitializedError(): never {
throw new Error(
'Kibana services not initialized - are you trying to import this module from outside of the Cases app?'
);
}
}

View file

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

View file

@ -1,23 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreStart } from 'kibana/public';
import { coreMock } from '../../../../../../src/core/public/mocks';
import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public/context';
export const createStartServicesMock = (): CoreStart => {
const core = coreMock.createStart();
return (core as unknown) as CoreStart;
};
export const createKibanaContextProviderMock = () => {
const services = coreMock.createStart();
return ({ children }: { children: React.ReactNode }) =>
React.createElement(KibanaContextProvider, { services }, children);
};

View file

@ -1,58 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import { I18nProvider } from '@kbn/i18n/react';
import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { ThemeProvider } from 'styled-components';
import { createKibanaContextProviderMock, createStartServicesMock } from './kibana_react.mock';
import { FieldHook } from '../shared_imports';
interface Props {
children: React.ReactNode;
}
export const kibanaObservable = new BehaviorSubject(createStartServicesMock());
window.scrollTo = jest.fn();
const MockKibanaContextProvider = createKibanaContextProviderMock();
/** A utility for wrapping children in the providers required to run most tests */
const TestProvidersComponent: React.FC<Props> = ({ children }) => (
<I18nProvider>
<MockKibanaContextProvider>
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>{children}</ThemeProvider>
</MockKibanaContextProvider>
</I18nProvider>
);
export const TestProviders = React.memo(TestProvidersComponent);
export const useFormFieldMock = <T,>(options?: Partial<FieldHook<T>>): FieldHook<T> => {
return {
path: 'path',
type: 'type',
value: ('mockedValue' as unknown) as T,
isPristine: false,
isValidating: false,
isValidated: false,
isChangingValue: false,
errors: [],
isValid: true,
getErrorsMessages: jest.fn(),
onChange: jest.fn(),
setValue: jest.fn(),
setErrors: jest.fn(),
clearErrors: jest.fn(),
validate: jest.fn(),
reset: jest.fn(),
__isIncludedInOutput: true,
__serializeValue: jest.fn(),
...options,
};
};

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export {
getUseField,
getFieldValidityAndErrorMessage,
FieldHook,
FieldValidateResponse,
FIELD_TYPES,
Form,
FormData,
FormDataProvider,
FormHook,
FormSchema,
UseField,
UseMultiFields,
useForm,
useFormContext,
useFormData,
ValidationError,
ValidationFunc,
VALIDATION_TYPES,
} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export {
Field,
SelectField,
} from '../../../../../src/plugins/es_ui_shared/static/forms/components';
export { fieldValidators } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers';
export { ERROR_CODE } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types';

View file

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* Convenience utility to remove text appended to links by EUI
*/
export const removeExternalLinkText = (str: string) =>
str.replace(/\(opens in a new tab or window\)/g, '');

View file

@ -1,252 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate(
'xpack.cases.caseSavedObjectNoPermissionsTitle',
{
defaultMessage: 'Kibana feature privileges required',
}
);
export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate(
'xpack.cases.caseSavedObjectNoPermissionsMessage',
{
defaultMessage:
'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.',
}
);
export const BACK_TO_ALL = i18n.translate('xpack.cases.caseView.backLabel', {
defaultMessage: 'Back to cases',
});
export const CANCEL = i18n.translate('xpack.cases.caseView.cancel', {
defaultMessage: 'Cancel',
});
export const DELETE_CASE = i18n.translate('xpack.cases.confirmDeleteCase.deleteCase', {
defaultMessage: 'Delete case',
});
export const DELETE_CASES = i18n.translate('xpack.cases.confirmDeleteCase.deleteCases', {
defaultMessage: 'Delete cases',
});
export const NAME = i18n.translate('xpack.cases.caseView.name', {
defaultMessage: 'Name',
});
export const OPENED_ON = i18n.translate('xpack.cases.caseView.openedOn', {
defaultMessage: 'Opened on',
});
export const CLOSED_ON = i18n.translate('xpack.cases.caseView.closedOn', {
defaultMessage: 'Closed on',
});
export const REPORTER = i18n.translate('xpack.cases.caseView.reporterLabel', {
defaultMessage: 'Reporter',
});
export const PARTICIPANTS = i18n.translate('xpack.cases.caseView.particpantsLabel', {
defaultMessage: 'Participants',
});
export const CREATE_BC_TITLE = i18n.translate('xpack.cases.caseView.breadcrumb', {
defaultMessage: 'Create',
});
export const CREATE_TITLE = i18n.translate('xpack.cases.caseView.create', {
defaultMessage: 'Create new case',
});
export const DESCRIPTION = i18n.translate('xpack.cases.caseView.description', {
defaultMessage: 'Description',
});
export const DESCRIPTION_REQUIRED = i18n.translate(
'xpack.cases.createCase.descriptionFieldRequiredError',
{
defaultMessage: 'A description is required.',
}
);
export const COMMENT_REQUIRED = i18n.translate('xpack.cases.caseView.commentFieldRequiredError', {
defaultMessage: 'A comment is required.',
});
export const REQUIRED_FIELD = i18n.translate('xpack.cases.caseView.fieldRequiredError', {
defaultMessage: 'Required field',
});
export const EDIT = i18n.translate('xpack.cases.caseView.edit', {
defaultMessage: 'Edit',
});
export const OPTIONAL = i18n.translate('xpack.cases.caseView.optional', {
defaultMessage: 'Optional',
});
export const PAGE_TITLE = i18n.translate('xpack.cases.pageTitle', {
defaultMessage: 'Cases',
});
export const CREATE_CASE = i18n.translate('xpack.cases.caseView.createCase', {
defaultMessage: 'Create case',
});
export const CLOSE_CASE = i18n.translate('xpack.cases.caseView.closeCase', {
defaultMessage: 'Close case',
});
export const MARK_CASE_IN_PROGRESS = i18n.translate('xpack.cases.caseView.markInProgress', {
defaultMessage: 'Mark in progress',
});
export const REOPEN_CASE = i18n.translate('xpack.cases.caseView.reopenCase', {
defaultMessage: 'Reopen case',
});
export const OPEN_CASE = i18n.translate('xpack.cases.caseView.openCase', {
defaultMessage: 'Open case',
});
export const CASE_NAME = i18n.translate('xpack.cases.caseView.caseName', {
defaultMessage: 'Case name',
});
export const TO = i18n.translate('xpack.cases.caseView.to', {
defaultMessage: 'to',
});
export const TAGS = i18n.translate('xpack.cases.caseView.tags', {
defaultMessage: 'Tags',
});
export const ACTIONS = i18n.translate('xpack.cases.allCases.actions', {
defaultMessage: 'Actions',
});
export const NO_TAGS_AVAILABLE = i18n.translate('xpack.cases.allCases.noTagsAvailable', {
defaultMessage: 'No tags available',
});
export const NO_REPORTERS_AVAILABLE = i18n.translate('xpack.cases.caseView.noReportersAvailable', {
defaultMessage: 'No reporters available.',
});
export const COMMENTS = i18n.translate('xpack.cases.allCases.comments', {
defaultMessage: 'Comments',
});
export const TAGS_HELP = i18n.translate('xpack.cases.createCase.fieldTagsHelpText', {
defaultMessage:
'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.',
});
export const NO_TAGS = i18n.translate('xpack.cases.caseView.noTags', {
defaultMessage: 'No tags are currently assigned to this case.',
});
export const TITLE_REQUIRED = i18n.translate('xpack.cases.createCase.titleFieldRequiredError', {
defaultMessage: 'A title is required.',
});
export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate('xpack.cases.configureCases.headerTitle', {
defaultMessage: 'Configure cases',
});
export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.cases.configureCasesButton', {
defaultMessage: 'Edit external connection',
});
export const ADD_COMMENT = i18n.translate('xpack.cases.caseView.comment.addComment', {
defaultMessage: 'Add comment',
});
export const ADD_COMMENT_HELP_TEXT = i18n.translate(
'xpack.cases.caseView.comment.addCommentHelpText',
{
defaultMessage: 'Add a new comment...',
}
);
export const SAVE = i18n.translate('xpack.cases.caseView.description.save', {
defaultMessage: 'Save',
});
export const GO_TO_DOCUMENTATION = i18n.translate('xpack.cases.caseView.goToDocumentationButton', {
defaultMessage: 'View documentation',
});
export const CONNECTORS = i18n.translate('xpack.cases.caseView.connectors', {
defaultMessage: 'External Incident Management System',
});
export const EDIT_CONNECTOR = i18n.translate('xpack.cases.caseView.editConnector', {
defaultMessage: 'Change external incident management system',
});
export const NO_CONNECTOR = i18n.translate('xpack.cases.common.noConnector', {
defaultMessage: 'No connector selected',
});
export const UNKNOWN = i18n.translate('xpack.cases.caseView.unknown', {
defaultMessage: 'Unknown',
});
export const MARKED_CASE_AS = i18n.translate('xpack.cases.caseView.markedCaseAs', {
defaultMessage: 'marked case as',
});
export const OPEN_CASES = i18n.translate('xpack.cases.caseTable.openCases', {
defaultMessage: 'Open cases',
});
export const CLOSED_CASES = i18n.translate('xpack.cases.caseTable.closedCases', {
defaultMessage: 'Closed cases',
});
export const IN_PROGRESS_CASES = i18n.translate('xpack.cases.caseTable.inProgressCases', {
defaultMessage: 'In progress cases',
});
export const SYNC_ALERTS_SWITCH_LABEL_ON = i18n.translate(
'xpack.cases.settings.syncAlertsSwitchLabelOn',
{
defaultMessage: 'On',
}
);
export const SYNC_ALERTS_SWITCH_LABEL_OFF = i18n.translate(
'xpack.cases.settings.syncAlertsSwitchLabelOff',
{
defaultMessage: 'Off',
}
);
export const SYNC_ALERTS_HELP = i18n.translate('xpack.cases.components.create.syncAlertHelpText', {
defaultMessage:
'Enabling this option will sync the status of alerts in this case with the case status.',
});
export const ALERT = i18n.translate('xpack.cases.common.alertLabel', {
defaultMessage: 'Alert',
});
export const ALERT_ADDED_TO_CASE = i18n.translate('xpack.cases.common.alertAddedToCase', {
defaultMessage: 'added to case',
});
export const SELECTABLE_MESSAGE_COLLECTIONS = i18n.translate(
'xpack.cases.common.allCases.table.selectableMessageCollections',
{
defaultMessage: 'Cases with sub-cases cannot be selected',
}
);

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useForm } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form';
import { useFormData } from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data';
jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form');
jest.mock(
'../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'
);
export const mockFormHook = {
isSubmitted: false,
isSubmitting: false,
isValid: true,
submit: jest.fn(),
subscribe: jest.fn(),
setFieldValue: jest.fn(),
setFieldErrors: jest.fn(),
getFields: jest.fn(),
getFormData: jest.fn(),
/* Returns a list of all errors in the form */
getErrors: jest.fn(),
reset: jest.fn(),
__options: {},
__formData$: {},
__addField: jest.fn(),
__removeField: jest.fn(),
__validateFields: jest.fn(),
__updateFormDataAt: jest.fn(),
__readFieldConfigFromSchema: jest.fn(),
__getFieldDefaultValue: jest.fn(),
};
export const getFormMock = (sampleData: any) => ({
...mockFormHook,
submit: () =>
Promise.resolve({
data: sampleData,
isValid: true,
}),
getFormData: () => sampleData,
});
export const useFormMock = useForm as jest.Mock;
export const useFormDataMock = useFormData as jest.Mock;

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ConnectorTypes } from '../../../../common';
import { ActionConnector } from '../../../containers/configure/types';
import { UseConnectorsResponse } from '../../../containers/configure/use_connectors';
import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure';
import { UseActionTypesResponse } from '../../../containers/configure/use_action_types';
import { connectorsMock, actionTypesMock } from '../../../containers/configure/mock';
export { mappings } from '../../../containers/configure/mock';
export const connectors: ActionConnector[] = connectorsMock;
export const searchURL =
'?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))';
export const useCaseConfigureResponse: ReturnUseCaseConfigure = {
closureType: 'close-by-user',
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
currentConfiguration: {
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
closureType: 'close-by-user',
},
firstLoad: false,
loading: false,
mappings: [],
persistCaseConfigure: jest.fn(),
persistLoading: false,
refetchCaseConfigure: jest.fn(),
setClosureType: jest.fn(),
setConnector: jest.fn(),
setCurrentConfiguration: jest.fn(),
setMappings: jest.fn(),
version: '',
};
export const useConnectorsResponse: UseConnectorsResponse = {
loading: false,
connectors,
refetchConnectors: jest.fn(),
};
export const useActionTypesResponse: UseActionTypesResponse = {
loading: false,
actionTypes: actionTypesMock,
refetchActionTypes: jest.fn(),
};

View file

@ -1,58 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { ClosureOptions, ClosureOptionsProps } from './closure_options';
import { TestProviders } from '../../common/mock';
import { ClosureOptionsRadio } from './closure_options_radio';
describe('ClosureOptions', () => {
let wrapper: ReactWrapper;
const onChangeClosureType = jest.fn();
const props: ClosureOptionsProps = {
disabled: false,
closureTypeSelected: 'close-by-user',
onChangeClosureType,
};
beforeAll(() => {
wrapper = mount(<ClosureOptions {...props} />, { wrappingComponent: TestProviders });
});
test('it shows the closure options form group', () => {
expect(
wrapper.find('[data-test-subj="case-closure-options-form-group"]').first().exists()
).toBe(true);
});
test('it shows the closure options form row', () => {
expect(wrapper.find('[data-test-subj="case-closure-options-form-row"]').first().exists()).toBe(
true
);
});
test('it shows closure options', () => {
expect(wrapper.find('[data-test-subj="case-closure-options-radio"]').first().exists()).toBe(
true
);
});
test('it pass the correct props to child', () => {
const closureOptionsRadioComponent = wrapper.find(ClosureOptionsRadio);
expect(closureOptionsRadioComponent.props().disabled).toEqual(false);
expect(closureOptionsRadioComponent.props().closureTypeSelected).toEqual('close-by-user');
expect(closureOptionsRadioComponent.props().onChangeClosureType).toEqual(onChangeClosureType);
});
test('the closure type is changed successfully', () => {
wrapper.find('input[id="close-by-pushing"]').simulate('change');
expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing');
});
});

View file

@ -1,54 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
import { ClosureType } from '../../containers/configure/types';
import { ClosureOptionsRadio } from './closure_options_radio';
import * as i18n from './translations';
export interface ClosureOptionsProps {
closureTypeSelected: ClosureType;
disabled: boolean;
onChangeClosureType: (newClosureType: ClosureType) => void;
}
const ClosureOptionsComponent: React.FC<ClosureOptionsProps> = ({
closureTypeSelected,
disabled,
onChangeClosureType,
}) => {
return (
<EuiDescribedFormGroup
fullWidth
title={<h3>{i18n.CASE_CLOSURE_OPTIONS_TITLE}</h3>}
description={
<>
<p>{i18n.CASE_CLOSURE_OPTIONS_DESC}</p>
<p>{i18n.CASE_COLSURE_OPTIONS_SUB_CASES}</p>
</>
}
data-test-subj="case-closure-options-form-group"
>
<EuiFormRow
fullWidth
label={i18n.CASE_CLOSURE_OPTIONS_LABEL}
data-test-subj="case-closure-options-form-row"
>
<ClosureOptionsRadio
closureTypeSelected={closureTypeSelected}
disabled={disabled}
onChangeClosureType={onChangeClosureType}
data-test-subj="case-closure-options-radio"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};
export const ClosureOptions = React.memo(ClosureOptionsComponent);

View file

@ -1,77 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { ReactWrapper, mount } from 'enzyme';
import { ClosureOptionsRadio, ClosureOptionsRadioComponentProps } from './closure_options_radio';
import { TestProviders } from '../../common/mock';
describe('ClosureOptionsRadio', () => {
let wrapper: ReactWrapper;
const onChangeClosureType = jest.fn();
const props: ClosureOptionsRadioComponentProps = {
disabled: false,
closureTypeSelected: 'close-by-user',
onChangeClosureType,
};
beforeAll(() => {
wrapper = mount(<ClosureOptionsRadio {...props} />, { wrappingComponent: TestProviders });
});
test('it renders', () => {
expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').first().exists()).toBe(
true
);
});
test('it shows the correct number of radio buttons', () => {
expect(wrapper.find('input[name="closure_options"]')).toHaveLength(2);
});
test('it renders close by user radio button', () => {
expect(wrapper.find('input[id="close-by-user"]').exists()).toBeTruthy();
});
test('it renders close by pushing radio button', () => {
expect(wrapper.find('input[id="close-by-pushing"]').exists()).toBeTruthy();
});
test('it disables the close by user radio button', () => {
const newWrapper = mount(<ClosureOptionsRadio {...props} disabled={true} />, {
wrappingComponent: TestProviders,
});
expect(newWrapper.find('input[id="close-by-user"]').prop('disabled')).toEqual(true);
});
test('it disables correctly the close by pushing radio button', () => {
const newWrapper = mount(<ClosureOptionsRadio {...props} disabled={true} />, {
wrappingComponent: TestProviders,
});
expect(newWrapper.find('input[id="close-by-pushing"]').prop('disabled')).toEqual(true);
});
test('it selects the correct radio button', () => {
const newWrapper = mount(
<ClosureOptionsRadio {...props} closureTypeSelected={'close-by-pushing'} />,
{
wrappingComponent: TestProviders,
}
);
expect(newWrapper.find('input[id="close-by-pushing"]').prop('checked')).toEqual(true);
});
test('it calls the onChangeClosureType function', () => {
wrapper.find('input[id="close-by-pushing"]').simulate('change');
wrapper.update();
expect(onChangeClosureType).toHaveBeenCalled();
expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing');
});
});

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ReactNode, useCallback } from 'react';
import { EuiRadioGroup } from '@elastic/eui';
import { ClosureType } from '../../containers/configure/types';
import * as i18n from './translations';
interface ClosureRadios {
id: ClosureType;
label: ReactNode;
}
const radios: ClosureRadios[] = [
{
id: 'close-by-user',
label: i18n.CASE_CLOSURE_OPTIONS_MANUAL,
},
{
id: 'close-by-pushing',
label: i18n.CASE_CLOSURE_OPTIONS_NEW_INCIDENT,
},
];
export interface ClosureOptionsRadioComponentProps {
closureTypeSelected: ClosureType;
disabled: boolean;
onChangeClosureType: (newClosureType: ClosureType) => void;
}
const ClosureOptionsRadioComponent: React.FC<ClosureOptionsRadioComponentProps> = ({
closureTypeSelected,
disabled,
onChangeClosureType,
}) => {
const onChangeLocal = useCallback(
(id: string) => {
onChangeClosureType(id as ClosureType);
},
[onChangeClosureType]
);
return (
<EuiRadioGroup
disabled={disabled}
options={radios}
idSelected={closureTypeSelected}
onChange={onChangeLocal}
name="closure_options"
data-test-subj="closure-options-radio-group"
/>
);
};
export const ClosureOptionsRadio = React.memo(ClosureOptionsRadioComponent);

View file

@ -1,115 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { Connectors, Props } from './connectors';
import { TestProviders } from '../../common/mock';
import { ConnectorsDropdown } from './connectors_dropdown';
import { connectors } from './__mock__';
import { ConnectorTypes } from '../../../common';
describe('Connectors', () => {
let wrapper: ReactWrapper;
const onChangeConnector = jest.fn();
const handleShowEditFlyout = jest.fn();
const props: Props = {
connectors,
disabled: false,
handleShowEditFlyout,
isLoading: false,
mappings: [],
onChangeConnector,
selectedConnector: { id: 'none', type: ConnectorTypes.none },
updateConnectorDisabled: false,
};
beforeAll(() => {
wrapper = mount(<Connectors {...props} />, { wrappingComponent: TestProviders });
});
test('it shows the connectors from group', () => {
expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').first().exists()).toBe(
true
);
});
test('it shows the connectors form row', () => {
expect(wrapper.find('[data-test-subj="case-connectors-form-row"]').first().exists()).toBe(true);
});
test('it shows the connectors dropdown', () => {
expect(wrapper.find('[data-test-subj="case-connectors-dropdown"]').first().exists()).toBe(true);
});
test('it pass the correct props to child', () => {
const connectorsDropdownProps = wrapper.find(ConnectorsDropdown).props();
expect(connectorsDropdownProps).toMatchObject({
disabled: false,
isLoading: false,
connectors,
selectedConnector: 'none',
onChange: props.onChangeConnector,
});
});
test('the connector is changed successfully', () => {
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
expect(onChangeConnector).toHaveBeenCalled();
expect(onChangeConnector).toHaveBeenCalledWith('resilient-2');
});
test('the connector is changed successfully to none', () => {
onChangeConnector.mockClear();
const newWrapper = mount(
<Connectors
{...props}
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.serviceNowITSM }}
/>,
{
wrappingComponent: TestProviders,
}
);
newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click');
expect(onChangeConnector).toHaveBeenCalled();
expect(onChangeConnector).toHaveBeenCalledWith('none');
});
test('it shows the add connector button', () => {
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.update();
expect(
wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').exists()
).toBeTruthy();
});
test('the text of the update button is shown correctly', () => {
const newWrapper = mount(
<Connectors
{...props}
selectedConnector={{ id: 'servicenow-1', type: ConnectorTypes.serviceNowITSM }}
/>,
{
wrappingComponent: TestProviders,
}
);
expect(
newWrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.text()
).toBe('Update My Connector');
});
});

View file

@ -1,119 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import {
EuiDescribedFormGroup,
EuiFormRow,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import styled from 'styled-components';
import { ConnectorsDropdown } from './connectors_dropdown';
import * as i18n from './translations';
import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types';
import { Mapping } from './mapping';
import { ConnectorTypes } from '../../../common';
const EuiFormRowExtended = styled(EuiFormRow)`
.euiFormRow__labelWrapper {
.euiFormRow__label {
width: 100%;
}
}
`;
export interface Props {
connectors: ActionConnector[];
disabled: boolean;
handleShowEditFlyout: () => void;
isLoading: boolean;
mappings: CaseConnectorMapping[];
onChangeConnector: (id: string) => void;
selectedConnector: { id: string; type: string };
updateConnectorDisabled: boolean;
}
const ConnectorsComponent: React.FC<Props> = ({
connectors,
disabled,
handleShowEditFlyout,
isLoading,
mappings,
onChangeConnector,
selectedConnector,
updateConnectorDisabled,
}) => {
const connectorsName = useMemo(
() => connectors.find((c) => c.id === selectedConnector.id)?.name ?? 'none',
[connectors, selectedConnector.id]
);
const dropDownLabel = useMemo(
() => (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL}</EuiFlexItem>
<EuiFlexItem grow={false}>
{connectorsName !== 'none' && (
<EuiLink
disabled={updateConnectorDisabled}
onClick={handleShowEditFlyout}
data-test-subj="case-configure-update-selected-connector-button"
>
{i18n.UPDATE_SELECTED_CONNECTOR(connectorsName)}
</EuiLink>
)}
</EuiFlexItem>
</EuiFlexGroup>
),
[connectorsName, handleShowEditFlyout, updateConnectorDisabled]
);
return (
<>
<EuiDescribedFormGroup
fullWidth
title={<h3>{i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}</h3>}
description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC}
data-test-subj="case-connectors-form-group"
>
<EuiFormRowExtended
fullWidth
label={dropDownLabel}
data-test-subj="case-connectors-form-row"
>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<ConnectorsDropdown
connectors={connectors}
disabled={disabled}
selectedConnector={selectedConnector.id}
isLoading={isLoading}
onChange={onChangeConnector}
data-test-subj="case-connectors-dropdown"
appendAddConnectorButton={true}
/>
</EuiFlexItem>
{selectedConnector.type !== ConnectorTypes.none ? (
<EuiFlexItem grow={false}>
<Mapping
connectorActionTypeId={selectedConnector.type}
isLoading={isLoading}
mappings={mappings}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
</EuiFormRowExtended>
</EuiDescribedFormGroup>
</>
);
};
export const Connectors = React.memo(ConnectorsComponent);

View file

@ -1,203 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { EuiSuperSelect } from '@elastic/eui';
import { ConnectorsDropdown, Props } from './connectors_dropdown';
import { TestProviders } from '../../common/mock';
import { connectors } from './__mock__';
describe('ConnectorsDropdown', () => {
let wrapper: ReactWrapper;
const props: Props = {
disabled: false,
connectors,
isLoading: false,
onChange: jest.fn(),
selectedConnector: 'none',
};
beforeAll(() => {
wrapper = mount(<ConnectorsDropdown {...props} />, { wrappingComponent: TestProviders });
});
test('it renders', () => {
expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().exists()).toBe(true);
});
test('it formats the connectors correctly', () => {
const selectProps = wrapper.find(EuiSuperSelect).props();
expect(selectProps.options).toMatchInlineSnapshot(`
Array [
Object {
"data-test-subj": "dropdown-connector-no-connector",
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="none"
>
<EuiFlexItem
grow={false}
>
<Styled(EuiIcon)
size="m"
type="minusInCircle"
/>
</EuiFlexItem>
<EuiFlexItem>
<span
data-test-subj="dropdown-connector-no-connector"
>
No connector selected
</span>
</EuiFlexItem>
</EuiFlexGroup>,
"value": "none",
},
Object {
"data-test-subj": "dropdown-connector-servicenow-1",
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="none"
>
<EuiFlexItem
grow={false}
>
<Styled(EuiIcon)
size="m"
type="test-file-stub"
/>
</EuiFlexItem>
<EuiFlexItem>
<span>
My Connector
</span>
</EuiFlexItem>
</EuiFlexGroup>,
"value": "servicenow-1",
},
Object {
"data-test-subj": "dropdown-connector-resilient-2",
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="none"
>
<EuiFlexItem
grow={false}
>
<Styled(EuiIcon)
size="m"
type="test-file-stub"
/>
</EuiFlexItem>
<EuiFlexItem>
<span>
My Connector 2
</span>
</EuiFlexItem>
</EuiFlexGroup>,
"value": "resilient-2",
},
Object {
"data-test-subj": "dropdown-connector-jira-1",
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="none"
>
<EuiFlexItem
grow={false}
>
<Styled(EuiIcon)
size="m"
type="test-file-stub"
/>
</EuiFlexItem>
<EuiFlexItem>
<span>
Jira
</span>
</EuiFlexItem>
</EuiFlexGroup>,
"value": "jira-1",
},
Object {
"data-test-subj": "dropdown-connector-servicenow-sir",
"inputDisplay": <EuiFlexGroup
alignItems="center"
gutterSize="none"
>
<EuiFlexItem
grow={false}
>
<Styled(EuiIcon)
size="m"
type="test-file-stub"
/>
</EuiFlexItem>
<EuiFlexItem>
<span>
My Connector SIR
</span>
</EuiFlexItem>
</EuiFlexGroup>,
"value": "servicenow-sir",
},
]
`);
});
test('it disables the dropdown', () => {
const newWrapper = mount(<ConnectorsDropdown {...props} disabled={true} />, {
wrappingComponent: TestProviders,
});
expect(
newWrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')
).toEqual(true);
});
test('it loading correctly', () => {
const newWrapper = mount(<ConnectorsDropdown {...props} isLoading={true} />, {
wrappingComponent: TestProviders,
});
expect(
newWrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading')
).toEqual(true);
});
test('it selects the correct connector', () => {
const newWrapper = mount(<ConnectorsDropdown {...props} selectedConnector={'servicenow-1'} />, {
wrappingComponent: TestProviders,
});
expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector');
});
test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => {
const newWrapper = mount(
<ConnectorsDropdown
{...props}
selectedConnector={'servicenow-1'}
hideConnectorServiceNowSir={true}
/>,
{
wrappingComponent: TestProviders,
}
);
const selectProps = newWrapper.find(EuiSuperSelect).props();
const options = selectProps.options as Array<{ 'data-test-subj': string }>;
expect(
options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-1')
).toBeTruthy();
expect(
options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-sir')
).toBeFalsy();
});
});

View file

@ -1,121 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui';
import styled from 'styled-components';
import { ConnectorTypes } from '../../../common';
import { ActionConnector } from '../../containers/configure/types';
import { connectorsConfiguration } from '../connectors';
import * as i18n from './translations';
export interface Props {
connectors: ActionConnector[];
disabled: boolean;
isLoading: boolean;
onChange: (id: string) => void;
selectedConnector: string;
appendAddConnectorButton?: boolean;
hideConnectorServiceNowSir?: boolean;
}
const ICON_SIZE = 'm';
const EuiIconExtended = styled(EuiIcon)`
margin-right: 13px;
margin-bottom: 0 !important;
`;
const noConnectorOption = {
value: 'none',
inputDisplay: (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIconExtended type="minusInCircle" size={ICON_SIZE} />
</EuiFlexItem>
<EuiFlexItem>
<span data-test-subj={`dropdown-connector-no-connector`}>{i18n.NO_CONNECTOR}</span>
</EuiFlexItem>
</EuiFlexGroup>
),
'data-test-subj': 'dropdown-connector-no-connector',
};
const addNewConnector = {
value: 'add-connector',
inputDisplay: (
<span className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushLeft">
{i18n.ADD_NEW_CONNECTOR}
</span>
),
'data-test-subj': 'dropdown-connector-add-connector',
};
const ConnectorsDropdownComponent: React.FC<Props> = ({
connectors,
disabled,
isLoading,
onChange,
selectedConnector,
appendAddConnectorButton = false,
hideConnectorServiceNowSir = false,
}) => {
const connectorsAsOptions = useMemo(() => {
const connectorsFormatted = connectors.reduce(
(acc, connector) => {
if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) {
return acc;
}
return [
...acc,
{
value: connector.id,
inputDisplay: (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIconExtended
type={connectorsConfiguration[connector.actionTypeId]?.logo ?? ''}
size={ICON_SIZE}
/>
</EuiFlexItem>
<EuiFlexItem>
<span>{connector.name}</span>
</EuiFlexItem>
</EuiFlexGroup>
),
'data-test-subj': `dropdown-connector-${connector.id}`,
},
];
},
[noConnectorOption]
);
if (appendAddConnectorButton) {
return [...connectorsFormatted, addNewConnector];
}
return connectorsFormatted;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectors]);
return (
<EuiSuperSelect
aria-label={i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL}
data-test-subj="dropdown-connectors"
disabled={disabled}
fullWidth
isLoading={isLoading}
onChange={onChange}
options={connectorsAsOptions}
valueOfSelected={selectedConnector}
/>
);
};
export const ConnectorsDropdown = React.memo(ConnectorsDropdownComponent);

View file

@ -1,55 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { FieldMapping, FieldMappingProps } from './field_mapping';
import { mappings } from './__mock__';
import { TestProviders } from '../../common/mock';
import { FieldMappingRowStatic } from './field_mapping_row_static';
describe('FieldMappingRow', () => {
let wrapper: ReactWrapper;
const props: FieldMappingProps = {
isLoading: false,
mappings,
connectorActionTypeId: '.servicenow',
};
beforeAll(() => {
wrapper = mount(<FieldMapping {...props} />, { wrappingComponent: TestProviders });
});
test('it renders', () => {
expect(
wrapper.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]').first().exists()
).toBe(true);
expect(wrapper.find(FieldMappingRowStatic).length).toEqual(3);
});
test('it does not render without mappings', () => {
const newWrapper = mount(<FieldMapping {...props} mappings={[]} />, {
wrappingComponent: TestProviders,
});
expect(
newWrapper
.find('[data-test-subj="case-configure-field-mappings-row-wrapper"]')
.first()
.exists()
).toBe(false);
});
test('it pass the corrects props to mapping row', () => {
const rows = wrapper.find(FieldMappingRowStatic);
rows.forEach((row, index) => {
expect(row.prop('casesField')).toEqual(mappings[index].source);
expect(row.prop('selectedActionType')).toEqual(mappings[index].actionType);
expect(row.prop('selectedThirdParty')).toEqual(mappings[index].target);
});
});
});

View file

@ -1,73 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import styled from 'styled-components';
import { FieldMappingRowStatic } from './field_mapping_row_static';
import * as i18n from './translations';
import { CaseConnectorMapping } from '../../containers/configure/types';
import { connectorsConfiguration } from '../connectors';
const FieldRowWrapper = styled.div`
margin: 10px 0;
font-size: 14px;
`;
export interface FieldMappingProps {
connectorActionTypeId: string;
isLoading: boolean;
mappings: CaseConnectorMapping[];
}
const FieldMappingComponent: React.FC<FieldMappingProps> = ({
connectorActionTypeId,
isLoading,
mappings,
}) => {
const selectedConnector = useMemo(
() => connectorsConfiguration[connectorActionTypeId] ?? { fields: {} },
[connectorActionTypeId]
);
return mappings.length ? (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>
{' '}
<EuiFlexGroup>
<EuiFlexItem>
<span className="euiFormLabel">{i18n.FIELD_MAPPING_FIRST_COL}</span>
</EuiFlexItem>
<EuiFlexItem>
<span className="euiFormLabel">
{i18n.FIELD_MAPPING_SECOND_COL(selectedConnector.name)}
</span>
</EuiFlexItem>
<EuiFlexItem>
<span className="euiFormLabel">{i18n.FIELD_MAPPING_THIRD_COL}</span>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<FieldRowWrapper data-test-subj="case-configure-field-mappings-row-wrapper">
{mappings.map((item) => (
<FieldMappingRowStatic
key={`${item.source}`}
casesField={item.source}
isLoading={isLoading}
selectedActionType={item.actionType}
selectedThirdParty={item.target ?? 'not_mapped'}
/>
))}
</FieldRowWrapper>
</EuiFlexItem>
</EuiFlexGroup>
) : null;
};
export const FieldMapping = React.memo(FieldMappingComponent);

View file

@ -1,60 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiCode, EuiFlexItem, EuiFlexGroup, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
import { capitalize } from 'lodash/fp';
import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types';
export interface RowProps {
isLoading: boolean;
casesField: CaseField;
selectedActionType: ActionType;
selectedThirdParty: ThirdPartyField;
}
const FieldMappingRowComponent: React.FC<RowProps> = ({
isLoading,
casesField,
selectedActionType,
selectedThirdParty,
}) => {
const selectedActionTypeCapitalized = useMemo(() => capitalize(selectedActionType), [
selectedActionType,
]);
return (
<EuiFlexGroup data-test-subj="static-mappings" alignItems="center">
<EuiFlexItem>
<EuiFlexGroup component="span" justifyContent="spaceBetween">
<EuiFlexItem component="span" grow={false}>
<EuiCode data-test-subj="field-mapping-source">{casesField}</EuiCode>
</EuiFlexItem>
<EuiFlexItem component="span" grow={false}>
<EuiIcon type="sortRight" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup component="span" justifyContent="spaceBetween">
<EuiFlexItem component="span" grow={false}>
{isLoading ? (
<EuiLoadingSpinner size="m" />
) : (
<EuiCode data-test-subj="field-mapping-target">{selectedThirdParty}</EuiCode>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
{isLoading ? <EuiLoadingSpinner size="m" /> : selectedActionTypeCapitalized}
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const FieldMappingRowStatic = React.memo(FieldMappingRowComponent);

View file

@ -1,591 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { ReactWrapper, mount } from 'enzyme';
import { ConfigureCases } from '.';
import { TestProviders } from '../../common/mock';
import { Connectors } from './connectors';
import { ClosureOptions } from './closure_options';
import {
ActionConnector,
ConnectorAddFlyout,
ConnectorEditFlyout,
TriggersAndActionsUIPublicPluginStart,
} from '../../../../triggers_actions_ui/public';
import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock';
import { useKibana } from '../../common/lib/kibana';
import { useConnectors } from '../../containers/configure/use_connectors';
import { useCaseConfigure } from '../../containers/configure/use_configure';
import { useActionTypes } from '../../containers/configure/use_action_types';
import {
connectors,
searchURL,
useCaseConfigureResponse,
useConnectorsResponse,
useActionTypesResponse,
} from './__mock__';
import { ConnectorTypes } from '../../../common';
jest.mock('../../common/lib/kibana');
jest.mock('../../containers/configure/use_connectors');
jest.mock('../../containers/configure/use_configure');
jest.mock('../../containers/configure/use_action_types');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const useConnectorsMock = useConnectors as jest.Mock;
const useCaseConfigureMock = useCaseConfigure as jest.Mock;
const useGetUrlSearchMock = jest.fn();
const useActionTypesMock = useActionTypes as jest.Mock;
describe('ConfigureCases', () => {
beforeEach(() => {
useKibanaMock().services.triggersActionsUi = ({
actionTypeRegistry: actionTypeRegistryMock.create(),
getAddConnectorFlyout: jest.fn().mockImplementation(() => (
<ConnectorAddFlyout
onClose={() => {}}
actionTypeRegistry={actionTypeRegistryMock.create()}
actionTypes={[
{
id: '.servicenow',
name: 'servicenow',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
},
{
id: '.jira',
name: 'jira',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
},
{
id: '.resilient',
name: 'resilient',
enabled: true,
enabledInConfig: true,
enabledInLicense: true,
minimumLicenseRequired: 'gold',
},
]}
/>
)),
getEditConnectorFlyout: jest
.fn()
.mockImplementation(() => (
<ConnectorEditFlyout
onClose={() => {}}
actionTypeRegistry={actionTypeRegistryMock.create()}
initialConnector={connectors[1] as ActionConnector}
/>
)),
} as unknown) as TriggersAndActionsUIPublicPluginStart;
useActionTypesMock.mockImplementation(() => useActionTypesResponse);
});
describe('rendering', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] }));
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it renders the Connectors', () => {
expect(wrapper.find('[data-test-subj="dropdown-connectors"]').exists()).toBeTruthy();
});
test('it renders the ClosureType', () => {
expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').exists()).toBeTruthy();
});
test('it does NOT render the ConnectorAddFlyout', () => {
// Components from triggersActionsUi do not have a data-test-subj
expect(wrapper.find(ConnectorAddFlyout).exists()).toBeFalsy();
});
test('it does NOT render the ConnectorEditFlyout', () => {
// Components from triggersActionsUi do not have a data-test-subj
expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy();
});
test('it does NOT render the EuiCallOut', () => {
expect(
wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()
).toBeFalsy();
});
});
describe('Unhappy path', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
closureType: 'close-by-user',
connector: {
id: 'not-id',
name: 'unchanged',
type: ConnectorTypes.none,
fields: null,
},
currentConfiguration: {
connector: {
id: 'not-id',
name: 'unchanged',
type: ConnectorTypes.none,
fields: null,
},
closureType: 'close-by-user',
},
}));
useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] }));
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it shows the warning callout when configuration is invalid', () => {
expect(
wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()
).toBeTruthy();
});
test('it hides the update connector button when the connectorId is invalid', () => {
expect(
wrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.exists()
).toBeFalsy();
});
});
describe('Happy path', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
mappings: [],
closureType: 'close-by-user',
connector: {
id: 'servicenow-1',
name: 'unchanged',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
currentConfiguration: {
connector: {
id: 'servicenow-1',
name: 'unchanged',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
closureType: 'close-by-user',
},
}));
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it renders with correct props', () => {
// Connector
expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors);
expect(wrapper.find(Connectors).prop('disabled')).toBe(false);
expect(wrapper.find(Connectors).prop('isLoading')).toBe(false);
expect(wrapper.find(Connectors).prop('selectedConnector').id).toBe('servicenow-1');
// ClosureOptions
expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false);
expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user');
// Flyouts
expect(wrapper.find(ConnectorAddFlyout).exists()).toBe(false);
expect(wrapper.find(ConnectorEditFlyout).exists()).toBe(false);
});
test('it disables correctly when the user cannot crud', () => {
const newWrapper = mount(<ConfigureCases userCanCrud={false} />, {
wrappingComponent: TestProviders,
});
expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe(
true
);
expect(
newWrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.prop('disabled')
).toBe(true);
// Two closure options
expect(
newWrapper
.find('[data-test-subj="closure-options-radio-group"] input')
.first()
.prop('disabled')
).toBe(true);
expect(
newWrapper
.find('[data-test-subj="closure-options-radio-group"] input')
.at(1)
.prop('disabled')
).toBe(true);
});
});
describe('loading connectors', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
mapping: null,
closureType: 'close-by-user',
connector: {
id: 'resilient-2',
name: 'unchanged',
type: ConnectorTypes.resilient,
fields: null,
},
currentConfiguration: {
connector: {
id: 'servicenow-1',
name: 'unchanged',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
closureType: 'close-by-user',
},
}));
useConnectorsMock.mockImplementation(() => ({
...useConnectorsResponse,
loading: true,
}));
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it disables correctly Connector when loading connectors', () => {
expect(
wrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')
).toBeTruthy();
});
test('it pass the correct value to isLoading attribute on Connector', () => {
expect(wrapper.find(Connectors).prop('isLoading')).toBe(true);
});
test('it disables correctly ClosureOptions when loading connectors', () => {
expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true);
});
test('it hides the update connector button when loading the connectors', () => {
expect(
wrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.prop('disabled')
).toBe(true);
});
test('it shows isLoading when loading action types', () => {
useConnectorsMock.mockImplementation(() => ({
...useConnectorsResponse,
loading: false,
}));
useActionTypesMock.mockImplementation(() => ({ ...useActionTypesResponse, loading: true }));
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
expect(wrapper.find(Connectors).prop('isLoading')).toBe(true);
});
});
describe('saving configuration', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
connector: {
id: 'servicenow-1',
name: 'SN',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
persistLoading: true,
}));
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it disables correctly Connector when saving configuration', () => {
expect(wrapper.find(Connectors).prop('disabled')).toBe(true);
});
test('it disables correctly ClosureOptions when saving configuration', () => {
expect(
wrapper
.find('[data-test-subj="closure-options-radio-group"] input')
.first()
.prop('disabled')
).toBe(true);
expect(
wrapper.find('[data-test-subj="closure-options-radio-group"] input').at(1).prop('disabled')
).toBe(true);
});
test('it disables the update connector button when saving the configuration', () => {
expect(
wrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.prop('disabled')
).toBe(true);
});
});
describe('loading configuration', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
loading: true,
}));
useConnectorsMock.mockImplementation(() => ({
...useConnectorsResponse,
}));
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it hides the update connector button when loading the configuration', () => {
expect(
wrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.exists()
).toBeFalsy();
});
});
describe('connectors', () => {
let wrapper: ReactWrapper;
let persistCaseConfigure: jest.Mock;
beforeEach(() => {
persistCaseConfigure = jest.fn();
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
mapping: null,
closureType: 'close-by-user',
connector: {
id: 'resilient-2',
name: 'My connector',
type: ConnectorTypes.resilient,
fields: null,
},
currentConfiguration: {
connector: {
id: 'My connector',
name: 'My connector',
type: ConnectorTypes.jira,
fields: null,
},
closureType: 'close-by-user',
},
persistCaseConfigure,
}));
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it submits the configuration correctly when changing connector', () => {
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
wrapper.update();
expect(persistCaseConfigure).toHaveBeenCalled();
expect(persistCaseConfigure).toHaveBeenCalledWith({
connector: {
id: 'resilient-2',
name: 'My Connector 2',
type: ConnectorTypes.resilient,
fields: null,
},
closureType: 'close-by-user',
});
});
test('the text of the update button is changed successfully', () => {
useCaseConfigureMock
.mockImplementationOnce(() => ({
...useCaseConfigureResponse,
connector: {
id: 'servicenow-1',
name: 'My connector',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
}))
.mockImplementation(() => ({
...useCaseConfigureResponse,
connector: {
id: 'resilient-2',
name: 'My connector 2',
type: ConnectorTypes.resilient,
fields: null,
},
}));
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click');
wrapper.update();
expect(
wrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.text()
).toBe('Update My Connector 2');
});
});
});
describe('closure options', () => {
let wrapper: ReactWrapper;
let persistCaseConfigure: jest.Mock;
beforeEach(() => {
persistCaseConfigure = jest.fn();
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
mapping: null,
closureType: 'close-by-user',
connector: {
id: 'servicenow-1',
name: 'My connector',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
currentConfiguration: {
connector: {
id: 'My connector',
name: 'My connector',
type: ConnectorTypes.jira,
fields: null,
},
closureType: 'close-by-user',
},
persistCaseConfigure,
}));
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
});
test('it submits the configuration correctly when changing closure type', () => {
wrapper.find('input[id="close-by-pushing"]').simulate('change');
wrapper.update();
expect(persistCaseConfigure).toHaveBeenCalled();
expect(persistCaseConfigure).toHaveBeenCalledWith({
connector: {
id: 'servicenow-1',
name: 'My connector',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
closureType: 'close-by-pushing',
});
});
});
describe('user interactions', () => {
beforeEach(() => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
mapping: null,
closureType: 'close-by-user',
connector: {
id: 'resilient-2',
name: 'unchanged',
type: ConnectorTypes.resilient,
fields: null,
},
currentConfiguration: {
connector: {
id: 'resilient-2',
name: 'unchanged',
type: ConnectorTypes.serviceNowITSM,
fields: null,
},
closureType: 'close-by-user',
},
}));
useConnectorsMock.mockImplementation(() => useConnectorsResponse);
useGetUrlSearchMock.mockImplementation(() => searchURL);
});
test('it show the add flyout when pressing the add connector button', () => {
const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.update();
wrapper.find('button[data-test-subj="dropdown-connector-add-connector"]').simulate('click');
wrapper.update();
expect(wrapper.find(ConnectorAddFlyout).exists()).toBe(true);
expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([
expect.objectContaining({
id: '.servicenow',
}),
expect.objectContaining({
id: '.jira',
}),
expect.objectContaining({
id: '.resilient',
}),
]);
});
test('it show the edit flyout when pressing the update connector button', () => {
const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders });
wrapper
.find('button[data-test-subj="case-configure-update-selected-connector-button"]')
.simulate('click');
wrapper.update();
expect(wrapper.find(ConnectorEditFlyout).exists()).toBe(true);
expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[1]);
expect(
wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists()
).toBeFalsy();
});
});

View file

@ -1,224 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled, { css } from 'styled-components';
import { EuiCallOut } from '@elastic/eui';
import { SUPPORTED_CONNECTORS } from '../../../common';
import { useKibana } from '../../common/lib/kibana';
import { useConnectors } from '../../containers/configure/use_connectors';
import { useActionTypes } from '../../containers/configure/use_action_types';
import { useCaseConfigure } from '../../containers/configure/use_configure';
import { ClosureType } from '../../containers/configure/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public/types';
import { SectionWrapper } from '../wrappers';
import { Connectors } from './connectors';
import { ClosureOptions } from './closure_options';
import {
getConnectorById,
getNoneConnector,
normalizeActionConnector,
normalizeCaseConnector,
} from './utils';
import * as i18n from './translations';
const FormWrapper = styled.div`
${({ theme }) => css`
& > * {
margin-top 40px;
}
& > :first-child {
margin-top: 0;
}
padding-top: ${theme.eui.paddingSizes.xl};
padding-bottom: ${theme.eui.paddingSizes.xl};
.euiFlyout {
z-index: ${theme.eui.euiZNavigation + 1};
}
`}
`;
interface ConfigureCasesComponentProps {
userCanCrud: boolean;
}
const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userCanCrud }) => {
const { triggersActionsUi } = useKibana().services;
const [connectorIsValid, setConnectorIsValid] = useState(true);
const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
const [editedConnectorItem, setEditedConnectorItem] = useState<ActionConnectorTableItem | null>(
null
);
const {
connector,
closureType,
loading: loadingCaseConfigure,
mappings,
persistLoading,
persistCaseConfigure,
refetchCaseConfigure,
setConnector,
setClosureType,
} = useCaseConfigure();
const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors();
const { loading: isLoadingActionTypes, actionTypes, refetchActionTypes } = useActionTypes();
const supportedActionTypes = useMemo(
() => actionTypes.filter((actionType) => SUPPORTED_CONNECTORS.includes(actionType.id)),
[actionTypes]
);
const onConnectorUpdate = useCallback(async () => {
refetchConnectors();
refetchActionTypes();
refetchCaseConfigure();
}, [refetchActionTypes, refetchCaseConfigure, refetchConnectors]);
const isLoadingAny =
isLoadingConnectors || persistLoading || loadingCaseConfigure || isLoadingActionTypes;
const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none';
const onClickUpdateConnector = useCallback(() => {
setEditFlyoutVisibility(true);
}, []);
const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [
setAddFlyoutVisibility,
]);
const onCloseEditFlyout = useCallback(() => setEditFlyoutVisibility(false), []);
const onChangeConnector = useCallback(
(id: string) => {
if (id === 'add-connector') {
setAddFlyoutVisibility(true);
return;
}
const actionConnector = getConnectorById(id, connectors);
const caseConnector =
actionConnector != null ? normalizeActionConnector(actionConnector) : getNoneConnector();
setConnector(caseConnector);
persistCaseConfigure({
connector: caseConnector,
closureType,
});
},
[connectors, closureType, persistCaseConfigure, setConnector]
);
const onChangeClosureType = useCallback(
(type: ClosureType) => {
setClosureType(type);
persistCaseConfigure({
connector,
closureType: type,
});
},
[connector, persistCaseConfigure, setClosureType]
);
useEffect(() => {
if (
!isLoadingConnectors &&
connector.id !== 'none' &&
!connectors.some((c) => c.id === connector.id)
) {
setConnectorIsValid(false);
} else if (
!isLoadingConnectors &&
(connector.id === 'none' || connectors.some((c) => c.id === connector.id))
) {
setConnectorIsValid(true);
}
}, [connectors, connector, isLoadingConnectors]);
useEffect(() => {
if (!isLoadingConnectors && connector.id !== 'none') {
setEditedConnectorItem(
normalizeCaseConnector(connectors, connector) as ActionConnectorTableItem
);
}
}, [connectors, connector, isLoadingConnectors]);
const ConnectorAddFlyout = useMemo(
() =>
triggersActionsUi.getAddConnectorFlyout({
consumer: 'case',
onClose: onCloseAddFlyout,
actionTypes: supportedActionTypes,
reloadConnectors: onConnectorUpdate,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[supportedActionTypes]
);
const ConnectorEditFlyout = useMemo(
() =>
editedConnectorItem && editFlyoutVisible
? triggersActionsUi.getEditConnectorFlyout({
initialConnector: editedConnectorItem,
consumer: 'case',
onClose: onCloseEditFlyout,
reloadConnectors: onConnectorUpdate,
})
: null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[connector.id, editFlyoutVisible]
);
return (
<FormWrapper>
{!connectorIsValid && (
<SectionWrapper style={{ marginTop: 0 }}>
<EuiCallOut
title={i18n.WARNING_NO_CONNECTOR_TITLE}
color="warning"
iconType="help"
data-test-subj="configure-cases-warning-callout"
>
{i18n.WARNING_NO_CONNECTOR_MESSAGE}
</EuiCallOut>
</SectionWrapper>
)}
<SectionWrapper>
<ClosureOptions
closureTypeSelected={closureType}
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
onChangeClosureType={onChangeClosureType}
/>
</SectionWrapper>
<SectionWrapper>
<Connectors
connectors={connectors ?? []}
disabled={persistLoading || isLoadingConnectors || !userCanCrud}
handleShowEditFlyout={onClickUpdateConnector}
isLoading={isLoadingAny}
mappings={mappings}
onChangeConnector={onChangeConnector}
selectedConnector={connector}
updateConnectorDisabled={updateConnectorDisabled || !userCanCrud}
/>
</SectionWrapper>
{addFlyoutVisible && ConnectorAddFlyout}
{ConnectorEditFlyout}
</FormWrapper>
);
};
export const ConfigureCases = React.memo(ConfigureCasesComponent);

View file

@ -1,47 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { TestProviders } from '../../common/mock';
import { Mapping, MappingProps } from './mapping';
import { mappings } from './__mock__';
describe('Mapping', () => {
const props: MappingProps = {
connectorActionTypeId: '.servicenow',
isLoading: false,
mappings,
};
beforeEach(() => {
jest.clearAllMocks();
});
test('it shows mapping form group', () => {
const wrapper = mount(<Mapping {...props} />, { wrappingComponent: TestProviders });
expect(wrapper.find('[data-test-subj="static-mappings"]').first().exists()).toBe(true);
});
test('correctly maps fields', () => {
const wrapper = mount(<Mapping {...props} />, { wrappingComponent: TestProviders });
expect(wrapper.find('[data-test-subj="field-mapping-source"] code').first().text()).toBe(
'title'
);
expect(wrapper.find('[data-test-subj="field-mapping-target"] code').first().text()).toBe(
'short_description'
);
});
test('displays connection warning when isLoading: false and mappings: []', () => {
const wrapper = mount(<Mapping {...{ ...props, mappings: [] }} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe(
'Field mappings require an established connection to ServiceNow ITSM. Please check your connection credentials.'
);
});
});

View file

@ -1,62 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor } from '@elastic/eui';
import { TextColor } from '@elastic/eui/src/components/text/text_color';
import * as i18n from './translations';
import { FieldMapping } from './field_mapping';
import { CaseConnectorMapping } from '../../containers/configure/types';
import { connectorsConfiguration } from '../connectors';
export interface MappingProps {
connectorActionTypeId: string;
isLoading: boolean;
mappings: CaseConnectorMapping[];
}
const MappingComponent: React.FC<MappingProps> = ({
connectorActionTypeId,
isLoading,
mappings,
}) => {
const selectedConnector = useMemo(() => connectorsConfiguration[connectorActionTypeId], [
connectorActionTypeId,
]);
const fieldMappingDesc: { desc: string; color: TextColor } = useMemo(
() =>
mappings.length > 0 || isLoading
? { desc: i18n.FIELD_MAPPING_DESC(selectedConnector.name), color: 'subdued' }
: { desc: i18n.FIELD_MAPPING_DESC_ERR(selectedConnector.name), color: 'danger' },
[isLoading, mappings.length, selectedConnector.name]
);
return (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiText size="xs">
<h4>{i18n.FIELD_MAPPING_TITLE(selectedConnector.name)}</h4>
<EuiTextColor data-test-subj="field-mapping-desc" color={fieldMappingDesc.color}>
{fieldMappingDesc.desc}
</EuiTextColor>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FieldMapping
connectorActionTypeId={connectorActionTypeId}
data-test-subj="case-mappings-field"
isLoading={isLoading}
mappings={mappings}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const Mapping = React.memo(MappingComponent);

View file

@ -1,227 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export * from '../../common/translations';
export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate(
'xpack.cases.configureCases.incidentManagementSystemTitle',
{
defaultMessage: 'Connect to external incident management system',
}
);
export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate(
'xpack.cases.configureCases.incidentManagementSystemDesc',
{
defaultMessage:
'You may optionally connect cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.',
}
);
export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate(
'xpack.cases.configureCases.incidentManagementSystemLabel',
{
defaultMessage: 'Incident management system',
}
);
export const ADD_NEW_CONNECTOR = i18n.translate('xpack.cases.configureCases.addNewConnector', {
defaultMessage: 'Add new connector',
});
export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsTitle',
{
defaultMessage: 'Case Closures',
}
);
export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsDesc',
{
defaultMessage:
'Define how you wish cases to be closed. Automated case closures require an established connection to an external incident management system.',
}
);
export const CASE_COLSURE_OPTIONS_SUB_CASES = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsSubCases',
{
defaultMessage: 'Automated closures of sub-cases is not currently supported.',
}
);
export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsLabel',
{
defaultMessage: 'Case closure options',
}
);
export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsManual',
{
defaultMessage: 'Manually close cases',
}
);
export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsNewIncident',
{
defaultMessage: 'Automatically close cases when pushing new incident to external system',
}
);
export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate(
'xpack.cases.configureCases.caseClosureOptionsClosedIncident',
{
defaultMessage: 'Automatically close cases when incident is closed in external system',
}
);
export const FIELD_MAPPING_TITLE = (thirdPartyName: string): string => {
return i18n.translate('xpack.cases.configureCases.fieldMappingTitle', {
values: { thirdPartyName },
defaultMessage: '{ thirdPartyName } field mappings',
});
};
export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => {
return i18n.translate('xpack.cases.configureCases.fieldMappingDesc', {
values: { thirdPartyName },
defaultMessage:
'Map Case fields to { thirdPartyName } fields when pushing data to { thirdPartyName }. Field mappings require an established connection to { thirdPartyName }.',
});
};
export const FIELD_MAPPING_DESC_ERR = (thirdPartyName: string): string => {
return i18n.translate('xpack.cases.configureCases.fieldMappingDescErr', {
values: { thirdPartyName },
defaultMessage:
'Field mappings require an established connection to { thirdPartyName }. Please check your connection credentials.',
});
};
export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => {
return i18n.translate('xpack.cases.configureCases.editFieldMappingTitle', {
values: { thirdPartyName },
defaultMessage: 'Edit { thirdPartyName } field mappings',
});
};
export const FIELD_MAPPING_FIRST_COL = i18n.translate(
'xpack.cases.configureCases.fieldMappingFirstCol',
{
defaultMessage: 'Kibana case field',
}
);
export const FIELD_MAPPING_SECOND_COL = (thirdPartyName: string): string => {
return i18n.translate('xpack.cases.configureCases.fieldMappingSecondCol', {
values: { thirdPartyName },
defaultMessage: '{ thirdPartyName } field',
});
};
export const FIELD_MAPPING_THIRD_COL = i18n.translate(
'xpack.cases.configureCases.fieldMappingThirdCol',
{
defaultMessage: 'On edit and update',
}
);
export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate(
'xpack.cases.configureCases.fieldMappingEditNothing',
{
defaultMessage: 'Nothing',
}
);
export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate(
'xpack.cases.configureCases.fieldMappingEditOverwrite',
{
defaultMessage: 'Overwrite',
}
);
export const FIELD_MAPPING_EDIT_APPEND = i18n.translate(
'xpack.cases.configureCases.fieldMappingEditAppend',
{
defaultMessage: 'Append',
}
);
export const CANCEL = i18n.translate('xpack.cases.configureCases.cancelButton', {
defaultMessage: 'Cancel',
});
export const SAVE = i18n.translate('xpack.cases.configureCases.saveButton', {
defaultMessage: 'Save',
});
export const SAVE_CLOSE = i18n.translate('xpack.cases.configureCases.saveAndCloseButton', {
defaultMessage: 'Save & close',
});
export const WARNING_NO_CONNECTOR_TITLE = i18n.translate(
'xpack.cases.configureCases.warningTitle',
{
defaultMessage: 'Warning',
}
);
export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate(
'xpack.cases.configureCases.warningMessage',
{
defaultMessage:
'The selected connector has been deleted. Either select a different connector or create a new one.',
}
);
export const MAPPING_FIELD_NOT_MAPPED = i18n.translate(
'xpack.cases.configureCases.mappingFieldNotMapped',
{
defaultMessage: 'Not mapped',
}
);
export const COMMENT = i18n.translate('xpack.cases.configureCases.commentMapping', {
defaultMessage: 'Comments',
});
export const NO_FIELDS_ERROR = (connectorName: string): string => {
return i18n.translate('xpack.cases.configureCases.noFieldsError', {
values: { connectorName },
defaultMessage:
'No { connectorName } fields found. Please check your { connectorName } connector settings or your { connectorName } instance settings to resolve.',
});
};
export const BLANK_MAPPINGS = (connectorName: string): string => {
return i18n.translate('xpack.cases.configureCases.blankMappings', {
values: { connectorName },
defaultMessage: 'At least one field needs to be mapped to { connectorName }',
});
};
export const REQUIRED_MAPPINGS = (connectorName: string, fields: string): string => {
return i18n.translate('xpack.cases.configureCases.requiredMappings', {
values: { connectorName, fields },
defaultMessage:
'At least one Case field needs to be mapped to the following required { connectorName } fields: { fields }',
});
};
export const UPDATE_FIELD_MAPPINGS = i18n.translate('xpack.cases.configureCases.updateConnector', {
defaultMessage: 'Update field mappings',
});
export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => {
return i18n.translate('xpack.cases.configureCases.updateSelectedConnector', {
values: { connectorName },
defaultMessage: 'Update { connectorName }',
});
};

View file

@ -1,64 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mappings } from './__mock__';
import { setActionTypeToMapping, setThirdPartyToMapping } from './utils';
import { CaseConnectorMapping } from '../../containers/configure/types';
describe('FieldMappingRow', () => {
test('it should change the action type', () => {
const newMapping = setActionTypeToMapping('title', 'nothing', mappings);
expect(newMapping[0].actionType).toBe('nothing');
});
test('it should not change other fields', () => {
const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mappings);
expect(newTitle).not.toEqual(mappings[0]);
expect(description).toEqual(mappings[1]);
expect(comments).toEqual(mappings[2]);
});
test('it should return a new array when changing action type', () => {
const newMapping = setActionTypeToMapping('title', 'nothing', mappings);
expect(newMapping).not.toBe(mappings);
});
test('it should change the third party', () => {
const newMapping = setThirdPartyToMapping('title', 'description', mappings);
expect(newMapping[0].target).toBe('description');
});
test('it should not change other fields when there is not a conflict', () => {
const tempMapping: CaseConnectorMapping[] = [
{
source: 'title',
target: 'short_description',
actionType: 'overwrite',
},
{
source: 'comments',
target: 'comments',
actionType: 'append',
},
];
const [newTitle, comments] = setThirdPartyToMapping('title', 'description', tempMapping);
expect(newTitle).not.toEqual(mappings[0]);
expect(comments).toEqual(tempMapping[1]);
});
test('it should return a new array when changing third party', () => {
const newMapping = setThirdPartyToMapping('title', 'description', mappings);
expect(newMapping).not.toBe(mappings);
});
test('it should change the target of the conflicting third party field to not_mapped', () => {
const newMapping = setThirdPartyToMapping('title', 'description', mappings);
expect(newMapping[1].target).toBe('not_mapped');
});
});

View file

@ -1,80 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ConnectorTypeFields, ConnectorTypes } from '../../../common';
import {
CaseField,
ActionType,
ThirdPartyField,
ActionConnector,
CaseConnector,
CaseConnectorMapping,
} from '../../containers/configure/types';
export const setActionTypeToMapping = (
caseField: CaseField,
newActionType: ActionType,
mapping: CaseConnectorMapping[]
): CaseConnectorMapping[] => {
const findItemIndex = mapping.findIndex((item) => item.source === caseField);
if (findItemIndex >= 0) {
return [
...mapping.slice(0, findItemIndex),
{ ...mapping[findItemIndex], actionType: newActionType },
...mapping.slice(findItemIndex + 1),
];
}
return [...mapping];
};
export const setThirdPartyToMapping = (
caseField: CaseField,
newThirdPartyField: ThirdPartyField,
mapping: CaseConnectorMapping[]
): CaseConnectorMapping[] =>
mapping.map((item) => {
if (item.source !== caseField && item.target === newThirdPartyField) {
return { ...item, target: 'not_mapped' };
} else if (item.source === caseField) {
return { ...item, target: newThirdPartyField };
}
return item;
});
export const getNoneConnector = (): CaseConnector => ({
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
});
export const getConnectorById = (
id: string,
connectors: ActionConnector[]
): ActionConnector | null => connectors.find((c) => c.id === id) ?? null;
export const normalizeActionConnector = (
actionConnector: ActionConnector,
fields: CaseConnector['fields'] = null
): CaseConnector => {
const caseConnectorFieldsType = {
type: actionConnector.actionTypeId,
fields,
} as ConnectorTypeFields;
return {
id: actionConnector.id,
name: actionConnector.name,
...caseConnectorFieldsType,
};
};
export const normalizeCaseConnector = (
connectors: ActionConnector[],
caseConnector: CaseConnector
): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null;

View file

@ -1,67 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { UseField, Form, useForm, FormHook } from '../../common/shared_imports';
import { ConnectorSelector } from './form';
import { connectorsMock } from '../../containers/mock';
import { getFormMock } from '../__mock__/form';
jest.mock('../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form');
const useFormMock = useForm as jest.Mock;
describe('ConnectorSelector', () => {
const formHookMock = getFormMock({ connectorId: connectorsMock[0].id });
beforeEach(() => {
jest.resetAllMocks();
useFormMock.mockImplementation(() => ({ form: formHookMock }));
});
it('it should render', async () => {
const wrapper = mount(
<Form form={(formHookMock as unknown) as FormHook}>
<UseField
path="connectorId"
component={ConnectorSelector}
componentProps={{
connectors: connectorsMock,
dataTestSubj: 'caseConnectors',
disabled: false,
idAria: 'caseConnectors',
isLoading: false,
}}
/>
</Form>
);
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
});
it('it should not render when is not in edit mode', async () => {
const wrapper = mount(
<Form form={(formHookMock as unknown) as FormHook}>
<UseField
path="connectorId"
component={ConnectorSelector}
componentProps={{
connectors: connectorsMock,
dataTestSubj: 'caseConnectors',
disabled: false,
idAria: 'caseConnectors',
isLoading: false,
isEdit: false,
}}
/>
</Form>
);
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeFalsy();
});
});

View file

@ -1,70 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback } from 'react';
import { isEmpty } from 'lodash/fp';
import { EuiFormRow } from '@elastic/eui';
import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports';
import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown';
import { ActionConnector } from '../../../common';
interface ConnectorSelectorProps {
connectors: ActionConnector[];
dataTestSubj: string;
disabled: boolean;
field: FieldHook<string>;
idAria: string;
isEdit: boolean;
isLoading: boolean;
handleChange?: (newValue: string) => void;
hideConnectorServiceNowSir?: boolean;
}
export const ConnectorSelector = ({
connectors,
dataTestSubj,
disabled = false,
field,
idAria,
isEdit = true,
isLoading = false,
handleChange,
hideConnectorServiceNowSir = false,
}: ConnectorSelectorProps) => {
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
const onChange = useCallback(
(val: string) => {
if (handleChange) {
handleChange(val);
}
field.setValue(val);
},
[handleChange, field]
);
return isEdit ? (
<EuiFormRow
data-test-subj={dataTestSubj}
describedByIds={idAria ? [idAria] : undefined}
error={errorMessage}
fullWidth
helpText={field.helpText}
isInvalid={isInvalid}
label={field.label}
labelAppend={field.labelAppend}
>
<ConnectorsDropdown
connectors={connectors}
disabled={disabled}
hideConnectorServiceNowSir={hideConnectorServiceNowSir}
isLoading={isLoading}
onChange={onChange}
selectedConnector={isEmpty(field.value) ? 'none' : field.value}
/>
</EuiFormRow>
) : null;
};

View file

@ -1,71 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui';
import styled from 'styled-components';
import { connectorsConfiguration } from '.';
import { ConnectorTypes } from '../../../common';
interface ConnectorCardProps {
connectorType: ConnectorTypes;
title: string;
listItems: Array<{ title: string; description: React.ReactNode }>;
isLoading: boolean;
}
const StyledText = styled.span`
span {
display: block;
}
`;
const ConnectorCardDisplay: React.FC<ConnectorCardProps> = ({
connectorType,
title,
listItems,
isLoading,
}) => {
const description = useMemo(
() => (
<StyledText>
{listItems.length > 0 &&
listItems.map((item, i) => (
<span data-test-subj="card-list-item" key={`${item.title}-${i}`}>
<strong>{`${item.title}: `}</strong>
{item.description}
</span>
))}
</StyledText>
),
[listItems]
);
const icon = useMemo(
() => <EuiIcon size="xl" type={connectorsConfiguration[`${connectorType}`]?.logo ?? ''} />,
[connectorType]
);
return (
<>
{isLoading && <EuiLoadingSpinner data-test-subj="connector-card-loading" />}
{!isLoading && (
<EuiCard
data-test-subj={`connector-card`}
description={description}
display="plain"
icon={icon}
layout="horizontal"
paddingSize="none"
title={title}
titleSize="xs"
/>
)}
</>
);
};
export const ConnectorCard = memo(ConnectorCardDisplay);

View file

@ -1,106 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable @kbn/eslint/no-restricted-paths */
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { ActionParamsProps } from '../../../../../triggers_actions_ui/public/types';
import { CommentType } from '../../../../common';
import { CaseActionParams } from './types';
import { ExistingCase } from './existing_case';
import * as i18n from './translations';
const Container = styled.div`
${({ theme }) => `
padding: ${theme.eui?.euiSizeS ?? '8px'} ${theme.eui?.euiSizeL ?? '24px'} ${
theme.eui?.euiSizeL ?? '24px'
} ${theme.eui?.euiSizeL ?? '24px'};
`}
`;
const defaultAlertComment = {
type: CommentType.generatedAlert,
alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{signal.rule.id}}", "ruleName": "{{signal.rule.name}}"}__SEPARATOR__{{/context.alerts}}]`,
};
const CaseParamsFields: React.FunctionComponent<ActionParamsProps<CaseActionParams>> = ({
actionParams,
editAction,
index,
errors,
messageVariables,
actionConnector,
}) => {
const { caseId = null, comment = defaultAlertComment } = actionParams.subActionParams ?? {};
const [selectedCase, setSelectedCase] = useState<string | null>(null);
const editSubActionProperty = useCallback(
(key: string, value: unknown) => {
const newProps = { ...actionParams.subActionParams, [key]: value };
editAction('subActionParams', newProps, index);
},
// edit action causes re-renders
// eslint-disable-next-line react-hooks/exhaustive-deps
[actionParams.subActionParams, index]
);
const onCaseChanged = useCallback(
(id: string) => {
setSelectedCase(id);
editSubActionProperty('caseId', id);
},
[editSubActionProperty]
);
useEffect(() => {
if (!actionParams.subAction) {
editAction('subAction', 'addComment', index);
}
if (!actionParams.subActionParams?.caseId) {
editSubActionProperty('caseId', caseId);
}
if (!actionParams.subActionParams?.comment) {
editSubActionProperty('comment', comment);
}
if (caseId != null) {
setSelectedCase((prevCaseId) => (prevCaseId !== caseId ? caseId : prevCaseId));
}
// editAction creates an infinity loop.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
actionConnector,
index,
actionParams.subActionParams?.caseId,
actionParams.subActionParams?.comment,
caseId,
comment,
actionParams.subAction,
]);
return (
<Container>
<ExistingCase onCaseChanged={onCaseChanged} selectedCase={selectedCase} />
<EuiSpacer size="m" />
<EuiCallOut size="s" title={i18n.CASE_CONNECTOR_CALL_OUT_TITLE} iconType="iInCircle">
<p>{i18n.CASE_CONNECTOR_CALL_OUT_MSG}</p>
</EuiCallOut>
</Container>
);
};
// eslint-disable-next-line import/no-default-export
export { CaseParamsFields as default };

View file

@ -1,73 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui';
import React, { memo, useMemo, useCallback } from 'react';
import { Case } from '../../../containers/types';
import * as i18n from './translations';
interface CaseDropdownProps {
isLoading: boolean;
cases: Case[];
selectedCase?: string;
onCaseChanged: (id: string) => void;
}
export const ADD_CASE_BUTTON_ID = 'add-case';
const addNewCase = {
value: ADD_CASE_BUTTON_ID,
inputDisplay: (
<span className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushLeft">
{i18n.CASE_CONNECTOR_ADD_NEW_CASE}
</span>
),
'data-test-subj': 'dropdown-connector-add-connector',
};
const CasesDropdownComponent: React.FC<CaseDropdownProps> = ({
isLoading,
cases,
selectedCase,
onCaseChanged,
}) => {
const caseOptions: Array<EuiSuperSelectOption<string>> = useMemo(
() =>
cases.reduce<Array<EuiSuperSelectOption<string>>>(
(acc, theCase) => [
...acc,
{
value: theCase.id,
inputDisplay: <span>{theCase.title}</span>,
'data-test-subj': `case-connector-cases-dropdown-${theCase.id}`,
},
],
[]
),
[cases]
);
const options = useMemo(() => [...caseOptions, addNewCase], [caseOptions]);
const onChange = useCallback((id: string) => onCaseChanged(id), [onCaseChanged]);
return (
<EuiFormRow label={i18n.CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL} fullWidth={true}>
<EuiSuperSelect
options={options}
data-test-subj="case-connector-cases-dropdown"
disabled={isLoading}
fullWidth
isLoading={isLoading}
valueOfSelected={selectedCase}
onChange={onChange}
/>
</EuiFormRow>
);
};
export const CasesDropdown = memo(CasesDropdownComponent);

View file

@ -1,76 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo, useCallback } from 'react';
import { CaseType } from '../../../../common';
import {
useGetCases,
DEFAULT_QUERY_PARAMS,
DEFAULT_FILTER_OPTIONS,
} from '../../../containers/use_get_cases';
import { useCreateCaseModal } from '../../use_create_case_modal';
import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown';
interface ExistingCaseProps {
selectedCase: string | null;
onCaseChanged: (id: string) => void;
}
const ExistingCaseComponent: React.FC<ExistingCaseProps> = ({ onCaseChanged, selectedCase }) => {
const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, {
...DEFAULT_FILTER_OPTIONS,
onlyCollectionType: true,
});
const onCaseCreated = useCallback(
(newCase) => {
refetchCases();
onCaseChanged(newCase.id);
},
[onCaseChanged, refetchCases]
);
const { modal, openModal } = useCreateCaseModal({
onCaseCreated,
caseType: CaseType.collection,
// FUTURE DEVELOPER
// We are making the assumption that this component is only used in rules creation
// that's why we want to hide ServiceNow SIR
hideConnectorServiceNowSir: true,
});
const onChange = useCallback(
(id: string) => {
if (id === ADD_CASE_BUTTON_ID) {
openModal();
return;
}
onCaseChanged(id);
},
[onCaseChanged, openModal]
);
const isCasesLoading = useMemo(
() => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'),
[isLoadingCases]
);
return (
<>
<CasesDropdown
isLoading={isCasesLoading}
cases={cases.cases}
selectedCase={selectedCase ?? undefined}
onCaseChanged={onChange}
/>
{modal}
</>
);
};
export const ExistingCase = memo(ExistingCaseComponent);

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ActionTypeModel } from '../../../../../triggers_actions_ui/public/types';
import { CaseActionParams } from './types';
import * as i18n from './translations';
interface ValidationResult {
errors: {
caseId: string[];
};
}
const validateParams = (actionParams: CaseActionParams) => {
const validationResult: ValidationResult = { errors: { caseId: [] } };
if (actionParams.subActionParams && !actionParams.subActionParams.caseId) {
validationResult.errors.caseId.push(i18n.CASE_CONNECTOR_CASE_REQUIRED);
}
return validationResult;
};
export function getActionType(): ActionTypeModel {
return {
id: '.case',
iconClass: 'securityAnalyticsApp',
selectMessage: i18n.CASE_CONNECTOR_DESC,
actionTypeTitle: i18n.CASE_CONNECTOR_TITLE,
validateConnector: () => ({ config: { errors: {} }, secrets: { errors: {} } }),
validateParams,
actionConnectorFields: null,
actionParamsFields: lazy(() => import('./alert_fields')),
};
}

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export * from '../../../common/translations';
export const CASE_CONNECTOR_DESC = i18n.translate(
'xpack.cases.components.connectors.cases.selectMessageText',
{
defaultMessage: 'Create or update a case.',
}
);
export const CASE_CONNECTOR_TITLE = i18n.translate(
'xpack.cases.components.connectors.cases.actionTypeTitle',
{
defaultMessage: 'Cases',
}
);
export const CASE_CONNECTOR_COMMENT_LABEL = i18n.translate(
'xpack.cases.components.connectors.cases.commentLabel',
{
defaultMessage: 'Comment',
}
);
export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate(
'xpack.cases.components.connectors.cases.commentRequired',
{
defaultMessage: 'Comment is required.',
}
);
export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate(
'xpack.cases.components.connectors.cases.casesDropdownRowLabel',
{
defaultMessage: 'Case allowing sub-cases',
}
);
export const CASE_CONNECTOR_CASES_DROPDOWN_PLACEHOLDER = i18n.translate(
'xpack.cases.components.connectors.cases.casesDropdownPlaceholder',
{
defaultMessage: 'Select case',
}
);
export const CASE_CONNECTOR_CASES_OPTION_NEW_CASE = i18n.translate(
'xpack.cases.components.connectors.cases.optionAddNewCase',
{
defaultMessage: 'Add to a new case',
}
);
export const CASE_CONNECTOR_CASES_OPTION_EXISTING_CASE = i18n.translate(
'xpack.cases.components.connectors.cases.optionAddToExistingCase',
{
defaultMessage: 'Add to existing case',
}
);
export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate(
'xpack.cases.components.connectors.cases.caseRequired',
{
defaultMessage: 'You must select a case.',
}
);
export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate(
'xpack.cases.components.connectors.cases.callOutTitle',
{
defaultMessage: 'Generated alerts will be attached to sub-cases',
}
);
export const CASE_CONNECTOR_CALL_OUT_MSG = i18n.translate(
'xpack.cases.components.connectors.cases.callOutMsg',
{
defaultMessage:
'A case can contain multiple sub-cases to allow grouping of generated alerts. Sub-cases will give more granular control over the status of these generated alerts and prevents having too many alerts attached to one case.',
}
);
export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate(
'xpack.cases.components.connectors.cases.addNewCaseOption',
{
defaultMessage: 'Add new case',
}
);
export const CREATE_CASE = i18n.translate(
'xpack.cases.components.connectors.cases.createCaseLabel',
{
defaultMessage: 'Create case',
}
);
export const CONNECTED_CASE = i18n.translate(
'xpack.cases.components.connectors.cases.connectedCaseLabel',
{
defaultMessage: 'Connected case',
}
);

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface CaseActionParams {
subAction: string;
subActionParams: {
caseId: string;
comment: {
alertId: string;
index: string;
type: 'alert';
};
};
}

View file

@ -1,39 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
getResilientActionType,
getServiceNowITSMActionType,
getServiceNowSIRActionType,
getJiraActionType,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../triggers_actions_ui/public/common';
import { ConnectorConfiguration } from './types';
const resilient = getResilientActionType();
const serviceNowITSM = getServiceNowITSMActionType();
const serviceNowSIR = getServiceNowSIRActionType();
const jira = getJiraActionType();
export const connectorsConfiguration: Record<string, ConnectorConfiguration> = {
'.servicenow': {
name: serviceNowITSM.actionTypeTitle ?? '',
logo: serviceNowITSM.iconClass,
},
'.servicenow-sir': {
name: serviceNowSIR.actionTypeTitle ?? '',
logo: serviceNowSIR.iconClass,
},
'.jira': {
name: jira.actionTypeTitle ?? '',
logo: jira.iconClass,
},
'.resilient': {
name: resilient.actionTypeTitle ?? '',
logo: resilient.iconClass,
},
};

View file

@ -1,49 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { CaseConnector, CaseConnectorsRegistry } from './types';
export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => {
const connectors: Map<string, CaseConnector<any>> = new Map();
const registry: CaseConnectorsRegistry = {
has: (id: string) => connectors.has(id),
register: <UIProps>(connector: CaseConnector<UIProps>) => {
if (connectors.has(connector.id)) {
throw new Error(
i18n.translate('xpack.cases.connecors.register.duplicateCaseConnectorErrorMessage', {
defaultMessage: 'Object type "{id}" is already registered.',
values: {
id: connector.id,
},
})
);
}
connectors.set(connector.id, connector);
},
get: <UIProps>(id: string): CaseConnector<UIProps> => {
if (!connectors.has(id)) {
throw new Error(
i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', {
defaultMessage: 'Object type "{id}" is not registered.',
values: {
id,
},
})
);
}
return connectors.get(id)!;
},
list: () => {
return Array.from(connectors).map(([id, connector]) => connector);
},
};
return registry;
};

View file

@ -1,54 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, Suspense } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { CaseActionConnector, ConnectorFieldsProps } from './types';
import { getCaseConnectors } from '.';
import { ConnectorTypeFields } from '../../../common';
interface Props extends Omit<ConnectorFieldsProps<ConnectorTypeFields['fields']>, 'connector'> {
connector: CaseActionConnector | null;
}
const ConnectorFieldsFormComponent: React.FC<Props> = ({ connector, isEdit, onChange, fields }) => {
const { caseConnectorsRegistry } = getCaseConnectors();
if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') {
return null;
}
const { fieldsComponent: FieldsComponent } = caseConnectorsRegistry.get(connector.actionTypeId);
return (
<>
{FieldsComponent != null ? (
<Suspense
fallback={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
}
>
<div data-test-subj={'connector-fields'}>
<FieldsComponent
isEdit={isEdit}
fields={fields}
connector={connector}
onChange={onChange}
/>
</div>
</Suspense>
) : null}
</>
);
};
export const ConnectorFieldsForm = memo(ConnectorFieldsFormComponent);

View file

@ -1,57 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CaseConnectorsRegistry } from './types';
import { createCaseConnectorsRegistry } from './connectors_registry';
import { getCaseConnector as getJiraCaseConnector } from './jira';
import { getCaseConnector as getResilientCaseConnector } from './resilient';
import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow';
import {
JiraFieldsType,
ServiceNowITSMFieldsType,
ServiceNowSIRFieldsType,
ResilientFieldsType,
} from '../../../common';
export { getActionType as getCaseConnectorUI } from './case';
export * from './config';
export * from './types';
interface GetCaseConnectorsReturn {
caseConnectorsRegistry: CaseConnectorsRegistry;
}
class CaseConnectors {
private caseConnectorsRegistry: CaseConnectorsRegistry;
constructor() {
this.caseConnectorsRegistry = createCaseConnectorsRegistry();
this.init();
}
private init() {
this.caseConnectorsRegistry.register<JiraFieldsType>(getJiraCaseConnector());
this.caseConnectorsRegistry.register<ResilientFieldsType>(getResilientCaseConnector());
this.caseConnectorsRegistry.register<ServiceNowITSMFieldsType>(
getServiceNowITSMCaseConnector()
);
this.caseConnectorsRegistry.register<ServiceNowSIRFieldsType>(getServiceNowSIRCaseConnector());
}
registry(): CaseConnectorsRegistry {
return this.caseConnectorsRegistry;
}
}
const caseConnectors = new CaseConnectors();
export const getCaseConnectors = (): GetCaseConnectorsReturn => {
return {
caseConnectorsRegistry: caseConnectors.registry(),
};
};

View file

@ -1,45 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { GetIssueTypesProps, GetFieldsByIssueTypeProps, GetIssueTypeProps } from '../api';
import { IssueTypes, Fields, Issues, Issue } from '../types';
import { issues } from '../../mock';
const issueTypes = [
{
id: '10006',
name: 'Task',
},
{
id: '10007',
name: 'Bug',
},
];
const fieldsByIssueType = {
summary: { allowedValues: [], defaultValue: {} },
priority: {
allowedValues: [
{
name: 'Medium',
id: '3',
},
],
defaultValue: { name: 'Medium', id: '3' },
},
};
export const getIssue = async (props: GetIssueTypeProps): Promise<{ data: Issue }> =>
Promise.resolve({ data: issues[0] });
export const getIssues = async (props: GetIssueTypesProps): Promise<{ data: Issues }> =>
Promise.resolve({ data: issues });
export const getIssueTypes = async (props: GetIssueTypesProps): Promise<{ data: IssueTypes }> =>
Promise.resolve({ data: issueTypes });
export const getFieldsByIssueType = async (
props: GetFieldsByIssueTypeProps
): Promise<{ data: Fields }> => Promise.resolve({ data: fieldsByIssueType });

View file

@ -1,160 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { httpServiceMock } from '../../../../../../../src/core/public/mocks';
import { getIssueTypes, getFieldsByIssueType, getIssues, getIssue } from './api';
const issueTypesResponse = {
data: {
projects: [
{
issuetypes: [
{
id: '10006',
name: 'Task',
},
{
id: '10007',
name: 'Bug',
},
],
},
],
},
};
const fieldsResponse = {
data: {
projects: [
{
issuetypes: [
{
id: '10006',
name: 'Task',
fields: {
summary: { fieldId: 'summary' },
priority: {
fieldId: 'priority',
allowedValues: [
{
name: 'Highest',
id: '1',
},
{
name: 'High',
id: '2',
},
{
name: 'Medium',
id: '3',
},
{
name: 'Low',
id: '4',
},
{
name: 'Lowest',
id: '5',
},
],
defaultValue: {
name: 'Medium',
id: '3',
},
},
},
},
],
},
],
},
};
const issueResponse = {
id: '10267',
key: 'RJ-107',
fields: { summary: 'Test title' },
};
const issuesResponse = [issueResponse];
describe('Jira API', () => {
const http = httpServiceMock.createStartContract();
beforeEach(() => jest.resetAllMocks());
describe('getIssueTypes', () => {
test('should call get issue types API', async () => {
const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issueTypesResponse);
const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'test' });
expect(res).toEqual(issueTypesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}',
signal: abortCtrl.signal,
});
});
});
describe('getFieldsByIssueType', () => {
test('should call get fields API', async () => {
const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(fieldsResponse);
const res = await getFieldsByIssueType({
http,
signal: abortCtrl.signal,
connectorId: 'test',
id: '10006',
});
expect(res).toEqual(fieldsResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}',
signal: abortCtrl.signal,
});
});
});
describe('getIssues', () => {
test('should call get fields API', async () => {
const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issuesResponse);
const res = await getIssues({
http,
signal: abortCtrl.signal,
connectorId: 'test',
title: 'test issue',
});
expect(res).toEqual(issuesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}',
signal: abortCtrl.signal,
});
});
});
describe('getIssue', () => {
test('should call get fields API', async () => {
const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issuesResponse);
const res = await getIssue({
http,
signal: abortCtrl.signal,
connectorId: 'test',
id: 'RJ-107',
});
expect(res).toEqual(issuesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}',
signal: abortCtrl.signal,
});
});
});
});

View file

@ -1,93 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../actions/common';
import { IssueTypes, Fields, Issues, Issue } from './types';
export const BASE_ACTION_API_PATH = '/api/actions';
export interface GetIssueTypesProps {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
}
export async function getIssueTypes({ http, signal, connectorId }: GetIssueTypesProps) {
return http.post<ActionTypeExecutorResult<IssueTypes>>(
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
{
body: JSON.stringify({
params: { subAction: 'issueTypes', subActionParams: {} },
}),
signal,
}
);
}
export interface GetFieldsByIssueTypeProps {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
id: string;
}
export async function getFieldsByIssueType({
http,
signal,
connectorId,
id,
}: GetFieldsByIssueTypeProps): Promise<ActionTypeExecutorResult<Fields>> {
return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
body: JSON.stringify({
params: { subAction: 'fieldsByIssueType', subActionParams: { id } },
}),
signal,
});
}
export interface GetIssuesTypeProps {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
title: string;
}
export async function getIssues({
http,
signal,
connectorId,
title,
}: GetIssuesTypeProps): Promise<ActionTypeExecutorResult<Issues>> {
return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
body: JSON.stringify({
params: { subAction: 'issues', subActionParams: { title } },
}),
signal,
});
}
export interface GetIssueTypeProps {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
id: string;
}
export async function getIssue({
http,
signal,
connectorId,
id,
}: GetIssueTypeProps): Promise<ActionTypeExecutorResult<Issue>> {
return http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, {
body: JSON.stringify({
params: { subAction: 'issue', subActionParams: { id } },
}),
signal,
});
}

View file

@ -1,262 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { omit } from 'lodash/fp';
import { connector, issues } from '../mock';
import { useGetIssueTypes } from './use_get_issue_types';
import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
import Fields from './case_fields';
import { waitFor } from '@testing-library/dom';
import { useGetSingleIssue } from './use_get_single_issue';
import { useGetIssues } from './use_get_issues';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
jest.mock('./use_get_issue_types');
jest.mock('./use_get_fields_by_issue_type');
jest.mock('./use_get_single_issue');
jest.mock('./use_get_issues');
jest.mock('../../../common/lib/kibana');
const useGetIssueTypesMock = useGetIssueTypes as jest.Mock;
const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock;
const useGetSingleIssueMock = useGetSingleIssue as jest.Mock;
const useGetIssuesMock = useGetIssues as jest.Mock;
describe('Jira Fields', () => {
const useGetIssueTypesResponse = {
isLoading: false,
issueTypes: [
{
id: '10006',
name: 'Task',
},
{
id: '10007',
name: 'Bug',
},
],
};
const useGetFieldsByIssueTypeResponse = {
isLoading: false,
fields: {
summary: { allowedValues: [], defaultValue: {} },
labels: { allowedValues: [], defaultValue: {} },
description: { allowedValues: [], defaultValue: {} },
priority: {
allowedValues: [
{
name: 'Medium',
id: '3',
},
{
name: 'Low',
id: '2',
},
],
defaultValue: { name: 'Medium', id: '3' },
},
},
};
const useGetSingleIssueResponse = {
isLoading: false,
issue: { title: 'Parent Task', key: 'parentId' },
};
const fields = {
issueType: '10006',
priority: 'High',
parent: null,
};
const useGetIssuesResponse = {
isLoading: false,
issues,
};
const onChange = jest.fn();
beforeEach(() => {
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
useGetSingleIssueMock.mockReturnValue(useGetSingleIssueResponse);
jest.clearAllMocks();
});
test('all params fields are rendered - isEdit: true', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('value')).toStrictEqual(
'10006'
);
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual(
'High'
);
expect(wrapper.find('[data-test-subj="search-parent-issues"]').first().exists()).toBeFalsy();
});
test('all params fields are rendered - isEdit: false', () => {
const wrapper = mount(
<Fields
isEdit={false}
fields={{ ...fields, parent: 'Parent Task' }}
onChange={onChange}
connector={connector}
/>
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual(
'Issue type: Task'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual(
'Parent issue: Parent Task'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual(
'Priority: High'
);
});
test('it sets parent correctly', async () => {
useGetFieldsByIssueTypeMock.mockReturnValue({
...useGetFieldsByIssueTypeResponse,
fields: {
...useGetFieldsByIssueTypeResponse.fields,
parent: {},
},
});
useGetIssuesMock.mockReturnValue(useGetIssuesResponse);
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
await waitFor(() =>
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ label: 'parentId', value: 'parentId' }])
);
wrapper.update();
expect(onChange).toHaveBeenCalledWith({
issueType: '10006',
parent: 'parentId',
priority: 'High',
});
});
test('it searches parent correctly', async () => {
useGetFieldsByIssueTypeMock.mockReturnValue({
...useGetFieldsByIssueTypeResponse,
fields: {
...useGetFieldsByIssueTypeResponse.fields,
parent: {},
},
});
useGetSingleIssueMock.mockReturnValue({ useGetSingleIssueResponse, issue: null });
useGetIssuesMock.mockReturnValue(useGetIssuesResponse);
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
await waitFor(() =>
((wrapper.find(EuiComboBox).props() as unknown) as {
onSearchChange: (a: string) => void;
}).onSearchChange('womanId')
);
wrapper.update();
expect(useGetIssuesMock.mock.calls[2][0].query).toEqual('womanId');
});
test('it disabled the fields when loading issue types', () => {
useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true });
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(
wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled')
).toBeTruthy();
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy();
});
test('it disabled the fields when loading fields', () => {
useGetFieldsByIssueTypeMock.mockReturnValue({
...useGetFieldsByIssueTypeResponse,
isLoading: true,
});
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(
wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled')
).toBeTruthy();
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy();
});
test('it hides the priority if not supported', () => {
const response = omit('fields.priority', useGetFieldsByIssueTypeResponse);
useGetFieldsByIssueTypeMock.mockReturnValue(response);
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(wrapper.find('[data-test-subj="prioritySelect"]').first().exists()).toBeFalsy();
});
test('it sets issue type correctly', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
wrapper
.find('select[data-test-subj="issueTypeSelect"]')
.first()
.simulate('change', {
target: { value: '10007' },
});
expect(onChange).toHaveBeenCalledWith({ issueType: '10007', parent: null, priority: null });
});
test('it sets issue type when it comes as null', () => {
const wrapper = mount(
<Fields fields={{ ...fields, issueType: null }} onChange={onChange} connector={connector} />
);
expect(wrapper.find('select[data-test-subj="issueTypeSelect"]').first().props().value).toEqual(
'10006'
);
});
test('it sets issue type when it comes as unknown value', () => {
const wrapper = mount(
<Fields
fields={{ ...fields, issueType: '99999' }}
onChange={onChange}
connector={connector}
/>
);
expect(wrapper.find('select[data-test-subj="issueTypeSelect"]').first().props().value).toEqual(
'10006'
);
});
test('it sets priority correctly', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
wrapper
.find('select[data-test-subj="prioritySelect"]')
.first()
.simulate('change', {
target: { value: '2' },
});
expect(onChange).toHaveBeenCalledWith({ issueType: '10006', parent: null, priority: '2' });
});
test('it resets priority when changing issue type', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
wrapper
.find('select[data-test-subj="issueTypeSelect"]')
.first()
.simulate('change', {
target: { value: '10007' },
});
expect(onChange).toBeCalledWith({ issueType: '10007', parent: null, priority: null });
});
});

View file

@ -1,214 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useMemo, useEffect, useRef } from 'react';
import { map } from 'lodash/fp';
import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import * as i18n from './translations';
import { ConnectorTypes, JiraFieldsType } from '../../../../common';
import { useKibana } from '../../../common/lib/kibana';
import { ConnectorFieldsProps } from '../types';
import { useGetIssueTypes } from './use_get_issue_types';
import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type';
import { SearchIssues } from './search_issues';
import { ConnectorCard } from '../card';
const JiraFieldsComponent: React.FunctionComponent<ConnectorFieldsProps<JiraFieldsType>> = ({
connector,
fields,
isEdit = true,
onChange,
}) => {
const init = useRef(true);
const { issueType = null, priority = null, parent = null } = fields ?? {};
const { http, notifications } = useKibana().services;
const handleIssueType = useCallback(
(issueTypeSelectOptions: Array<{ value: string; text: string }>) => {
if (issueType == null && issueTypeSelectOptions.length > 0) {
// if there is no issue type set in the edit view, set it to default
if (isEdit) {
onChange({
issueType: issueTypeSelectOptions[0].value,
parent,
priority,
});
}
}
},
[isEdit, issueType, onChange, parent, priority]
);
const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({
connector,
http,
toastNotifications: notifications.toasts,
handleIssueType,
});
const issueTypesSelectOptions = useMemo(
() =>
issueTypes.map((type) => ({
text: type.name ?? '',
value: type.id ?? '',
})),
[issueTypes]
);
const currentIssueType = useMemo(() => {
if (!issueType && issueTypesSelectOptions.length > 0) {
return issueTypesSelectOptions[0].value;
} else if (
issueTypesSelectOptions.length > 0 &&
!issueTypesSelectOptions.some(({ value }) => value === issueType)
) {
return issueTypesSelectOptions[0].value;
}
return issueType;
}, [issueType, issueTypesSelectOptions]);
const { isLoading: isLoadingFields, fields: fieldsByIssueType } = useGetFieldsByIssueType({
connector,
http,
issueType: currentIssueType,
toastNotifications: notifications.toasts,
});
const hasPriority = useMemo(() => fieldsByIssueType.priority != null, [fieldsByIssueType]);
const hasParent = useMemo(() => fieldsByIssueType.parent != null, [fieldsByIssueType]);
const prioritiesSelectOptions = useMemo(() => {
const priorities = fieldsByIssueType.priority?.allowedValues ?? [];
return map(
(p) => ({
text: p.name,
value: p.name,
}),
priorities
);
}, [fieldsByIssueType]);
const listItems = useMemo(
() => [
...(issueType != null && issueType.length > 0
? [
{
title: i18n.ISSUE_TYPE,
description: issueTypes.find((issue) => issue.id === issueType)?.name ?? '',
},
]
: []),
...(parent != null && parent.length > 0
? [
{
title: i18n.PARENT_ISSUE,
description: parent,
},
]
: []),
...(priority != null && priority.length > 0
? [
{
title: i18n.PRIORITY,
description: priority,
},
]
: []),
],
[issueType, issueTypes, parent, priority]
);
const onFieldChange = useCallback(
(key, value) => {
if (key === 'issueType') {
return onChange({ ...fields, issueType: value, priority: null, parent: null });
}
return onChange({
...fields,
issueType: currentIssueType,
parent,
priority,
[key]: value,
});
},
[currentIssueType, fields, onChange, parent, priority]
);
// Set field at initialization
useEffect(() => {
if (init.current) {
init.current = false;
onChange({ issueType, priority, parent });
}
}, [issueType, onChange, parent, priority]);
return isEdit ? (
<div data-test-subj={'connector-fields-jira'}>
<EuiFormRow fullWidth label={i18n.ISSUE_TYPE}>
<EuiSelect
data-test-subj="issueTypeSelect"
disabled={isLoadingIssueTypes || isLoadingFields}
fullWidth
isLoading={isLoadingIssueTypes}
onChange={(e) => onFieldChange('issueType', e.target.value)}
options={issueTypesSelectOptions}
value={currentIssueType ?? ''}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<>
{hasParent && (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.PARENT_ISSUE}>
<SearchIssues
actionConnector={connector}
onChange={(parentIssueKey) => onFieldChange('parent', parentIssueKey)}
selectedValue={parent}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
</>
)}
{hasPriority && (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.PRIORITY}>
<EuiSelect
data-test-subj="prioritySelect"
disabled={isLoadingIssueTypes || isLoadingFields}
fullWidth
hasNoInitialSelection
isLoading={isLoadingFields}
onChange={(e) => onFieldChange('priority', e.target.value)}
options={prioritiesSelectOptions}
value={priority ?? ''}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
</div>
) : (
<ConnectorCard
connectorType={ConnectorTypes.jira}
isLoading={isLoadingIssueTypes || isLoadingFields}
listItems={listItems}
title={connector.name}
/>
);
};
// eslint-disable-next-line import/no-default-export
export { JiraFieldsComponent as default };

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
import { CaseConnector } from '../types';
import { JiraFieldsType } from '../../../../common';
import * as i18n from './translations';
export * from './types';
export const getCaseConnector = (): CaseConnector<JiraFieldsType> => {
return {
id: '.jira',
fieldsComponent: lazy(() => import('./case_fields')),
};
};
export const fieldLabels = {
issueType: i18n.ISSUE_TYPE,
priority: i18n.PRIORITY,
parent: i18n.PARENT_ISSUE,
};

View file

@ -1,95 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo, useEffect, useCallback, useState, memo } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
import { ActionConnector } from '../../../containers/types';
import { useGetIssues } from './use_get_issues';
import { useGetSingleIssue } from './use_get_single_issue';
import * as i18n from './translations';
interface Props {
selectedValue: string | null;
actionConnector?: ActionConnector;
onChange: (parentIssueKey: string) => void;
}
const SearchIssuesComponent: React.FC<Props> = ({ selectedValue, actionConnector, onChange }) => {
const [query, setQuery] = useState<string | null>(null);
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
[]
);
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
const { http, notifications } = useKibana().services;
const { isLoading: isLoadingIssues, issues } = useGetIssues({
http,
toastNotifications: notifications.toasts,
actionConnector,
query,
});
const { isLoading: isLoadingSingleIssue, issue: singleIssue } = useGetSingleIssue({
http,
toastNotifications: notifications.toasts,
actionConnector,
id: selectedValue,
});
useEffect(() => setOptions(issues.map((issue) => ({ label: issue.title, value: issue.key }))), [
issues,
]);
useEffect(() => {
if (isLoadingSingleIssue || singleIssue == null) {
return;
}
const singleIssueAsOptions = [{ label: singleIssue.title, value: singleIssue.key }];
setOptions(singleIssueAsOptions);
setSelectedOptions(singleIssueAsOptions);
}, [singleIssue, isLoadingSingleIssue]);
const onSearchChange = useCallback((searchVal: string) => {
setQuery(searchVal);
}, []);
const onChangeComboBox = useCallback(
(changedOptions) => {
setSelectedOptions(changedOptions);
onChange(changedOptions[0].value);
},
[onChange]
);
const inputPlaceholder = useMemo(
(): string =>
isLoadingIssues || isLoadingSingleIssue
? i18n.SEARCH_ISSUES_LOADING
: i18n.SEARCH_ISSUES_PLACEHOLDER,
[isLoadingIssues, isLoadingSingleIssue]
);
return (
<EuiComboBox
singleSelection
fullWidth
placeholder={inputPlaceholder}
data-test-subj={'search-parent-issues'}
aria-label={i18n.SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL}
options={options}
isLoading={isLoadingIssues || isLoadingSingleIssue}
onSearchChange={onSearchChange}
selectedOptions={selectedOptions}
onChange={onChangeComboBox}
/>
);
};
export const SearchIssues = memo(SearchIssuesComponent);

View file

@ -1,68 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const ISSUE_TYPES_API_ERROR = i18n.translate(
'xpack.cases.connectors.jira.unableToGetIssueTypesMessage',
{
defaultMessage: 'Unable to get issue types',
}
);
export const FIELDS_API_ERROR = i18n.translate(
'xpack.cases.connectors.jira.unableToGetFieldsMessage',
{
defaultMessage: 'Unable to get connectors',
}
);
export const ISSUES_API_ERROR = i18n.translate(
'xpack.cases.connectors.jira.unableToGetIssuesMessage',
{
defaultMessage: 'Unable to get issues',
}
);
export const GET_ISSUE_API_ERROR = (id: string) =>
i18n.translate('xpack.cases.connectors.jira.unableToGetIssueMessage', {
defaultMessage: 'Unable to get issue with id {id}',
values: { id },
});
export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate(
'xpack.cases.connectors.jira.searchIssuesComboBoxAriaLabel',
{
defaultMessage: 'Type to search',
}
);
export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate(
'xpack.cases.connectors.jira.searchIssuesComboBoxPlaceholder',
{
defaultMessage: 'Type to search',
}
);
export const SEARCH_ISSUES_LOADING = i18n.translate(
'xpack.cases.connectors.jira.searchIssuesLoading',
{
defaultMessage: 'Loading...',
}
);
export const PRIORITY = i18n.translate('xpack.cases.connectors.jira.prioritySelectFieldLabel', {
defaultMessage: 'Priority',
});
export const ISSUE_TYPE = i18n.translate('xpack.cases.connectors.jira.issueTypesSelectFieldLabel', {
defaultMessage: 'Issue type',
});
export const PARENT_ISSUE = i18n.translate('xpack.cases.connectors.jira.parentIssueSearchLabel', {
defaultMessage: 'Parent issue',
});

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type IssueTypes = Array<{ id: string; name: string }>;
export interface Fields {
[key: string]: {
allowedValues: Array<{ name: string; id: string }> | [];
defaultValue: { name: string; id: string } | {};
};
}
export interface Issue {
id: string;
key: string;
title: string;
}
export type Issues = Issue[];

View file

@ -1,105 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { connector } from '../mock';
import { useGetFieldsByIssueType, UseGetFieldsByIssueType } from './use_get_fields_by_issue_type';
import * as api from './api';
jest.mock('../../../common/lib/kibana');
jest.mock('./api');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('useGetFieldsByIssueType', () => {
const { http, notifications } = useKibanaMock().services;
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
useGetFieldsByIssueType({ http, toastNotifications: notifications.toasts, issueType: null })
);
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: true, fields: {} });
});
});
test('does not fetch when issueType is not provided', async () => {
const spyOnGetFieldsByIssueType = jest.spyOn(api, 'getFieldsByIssueType');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
useGetFieldsByIssueType({
http,
toastNotifications: notifications.toasts,
connector,
issueType: null,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(spyOnGetFieldsByIssueType).not.toHaveBeenCalled();
expect(result.current).toEqual({ isLoading: false, fields: {} });
});
});
test('fetch fields', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
useGetFieldsByIssueType({
http,
toastNotifications: notifications.toasts,
connector,
issueType: 'Task',
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
fields: {
summary: { allowedValues: [], defaultValue: {} },
priority: {
allowedValues: [
{
name: 'Medium',
id: '3',
},
],
defaultValue: { name: 'Medium', id: '3' },
},
},
});
});
});
test('unhappy path', async () => {
const spyOnGetCaseConfigure = jest.spyOn(api, 'getFieldsByIssueType');
spyOnGetCaseConfigure.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetFieldsByIssueType>(() =>
useGetFieldsByIssueType({
http,
toastNotifications: notifications.toasts,
connector,
issueType: null,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, fields: {} });
});
});
});

View file

@ -1,96 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getFieldsByIssueType } from './api';
import { Fields } from './types';
import * as i18n from './translations';
interface Props {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
issueType: string | null;
connector?: ActionConnector;
}
export interface UseGetFieldsByIssueType {
fields: Fields;
isLoading: boolean;
}
export const useGetFieldsByIssueType = ({
http,
toastNotifications,
connector,
issueType,
}: Props): UseGetFieldsByIssueType => {
const [isLoading, setIsLoading] = useState(true);
const [fields, setFields] = useState<Fields>({});
const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
if (!connector || !issueType) {
setIsLoading(false);
return;
}
try {
abortCtrl.current = new AbortController();
setIsLoading(true);
const res = await getFieldsByIssueType({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
id: issueType,
});
if (!didCancel.current) {
setIsLoading(false);
setFields(res.data ?? {});
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.FIELDS_API_ERROR,
text: `${res.serviceMessage ?? res.message}`,
});
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.FIELDS_API_ERROR,
text: error.message,
});
}
}
}
};
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, connector, issueType, toastNotifications]);
return {
isLoading,
fields,
};
};

View file

@ -1,107 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { connector } from '../mock';
import { useGetIssueTypes, UseGetIssueTypes } from './use_get_issue_types';
import * as api from './api';
jest.mock('../../../common/lib/kibana');
jest.mock('./api');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('useGetIssueTypes', () => {
const { http, notifications } = useKibanaMock().services;
const handleIssueType = jest.fn();
beforeEach(() => jest.clearAllMocks());
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
useGetIssueTypes({ http, toastNotifications: notifications.toasts, handleIssueType })
);
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: true, issueTypes: [] });
});
});
test('fetch issue types', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
useGetIssueTypes({
http,
toastNotifications: notifications.toasts,
connector,
handleIssueType,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
issueTypes: [
{
id: '10006',
name: 'Task',
},
{
id: '10007',
name: 'Bug',
},
],
});
});
});
test('handleIssueType is called', async () => {
await act(async () => {
const { waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
useGetIssueTypes({
http,
toastNotifications: notifications.toasts,
connector,
handleIssueType,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(handleIssueType).toHaveBeenCalledWith([
{ text: 'Task', value: '10006' },
{ text: 'Bug', value: '10007' },
]);
});
});
test('unhappy path', async () => {
const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssueTypes');
spyOnGetCaseConfigure.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIssueTypes>(() =>
useGetIssueTypes({
http,
toastNotifications: notifications.toasts,
connector,
handleIssueType,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, issueTypes: [] });
});
});
});

View file

@ -1,102 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getIssueTypes } from './api';
import { IssueTypes } from './types';
import * as i18n from './translations';
interface Props {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
connector?: ActionConnector;
handleIssueType: (options: Array<{ value: string; text: string }>) => void;
}
export interface UseGetIssueTypes {
issueTypes: IssueTypes;
isLoading: boolean;
}
export const useGetIssueTypes = ({
http,
connector,
toastNotifications,
handleIssueType,
}: Props): UseGetIssueTypes => {
const [isLoading, setIsLoading] = useState(true);
const [issueTypes, setIssueTypes] = useState<IssueTypes>([]);
const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
try {
abortCtrl.current = new AbortController();
setIsLoading(true);
const res = await getIssueTypes({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
});
if (!didCancel.current) {
setIsLoading(false);
const asOptions = (res.data ?? []).map((type) => ({
text: type.name ?? '',
value: type.id ?? '',
}));
setIssueTypes(res.data ?? []);
handleIssueType(asOptions);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.ISSUE_TYPES_API_ERROR,
text: `${res.serviceMessage ?? res.message}`,
});
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.ISSUE_TYPES_API_ERROR,
text: error.message,
});
}
}
}
};
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
// handleIssueType unmounts the component at init causing the request to be aborted
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [http, connector, toastNotifications]);
return {
issueTypes,
isLoading,
};
};

View file

@ -1,80 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { connector as actionConnector, issues } from '../mock';
import { useGetIssues, UseGetIssues } from './use_get_issues';
import * as api from './api';
jest.mock('../../../common/lib/kibana');
jest.mock('./api');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('useGetIssues', () => {
const { http, notifications } = useKibanaMock().services;
beforeEach(() => jest.clearAllMocks());
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIssues>(() =>
useGetIssues({
http,
toastNotifications: notifications.toasts,
actionConnector,
query: null,
})
);
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, issues: [] });
});
});
test('fetch issues', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIssues>(() =>
useGetIssues({
http,
toastNotifications: notifications.toasts,
actionConnector,
query: 'Task',
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
issues,
});
});
});
test('unhappy path', async () => {
const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssues');
spyOnGetCaseConfigure.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIssues>(() =>
useGetIssues({
http,
toastNotifications: notifications.toasts,
actionConnector,
query: 'oh no',
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, issues: [] });
});
});
});

View file

@ -1,97 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isEmpty, debounce } from 'lodash/fp';
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getIssues } from './api';
import { Issues } from './types';
import * as i18n from './translations';
interface Props {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
actionConnector?: ActionConnector;
query: string | null;
}
export interface UseGetIssues {
issues: Issues;
isLoading: boolean;
}
export const useGetIssues = ({
http,
actionConnector,
toastNotifications,
query,
}: Props): UseGetIssues => {
const [isLoading, setIsLoading] = useState(false);
const [issues, setIssues] = useState<Issues>([]);
const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
const fetchData = debounce(500, async () => {
if (!actionConnector || isEmpty(query)) {
setIsLoading(false);
return;
}
try {
abortCtrl.current = new AbortController();
setIsLoading(true);
const res = await getIssues({
http,
signal: abortCtrl.current.signal,
connectorId: actionConnector.id,
title: query ?? '',
});
if (!didCancel.current) {
setIsLoading(false);
setIssues(res.data ?? []);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.ISSUES_API_ERROR,
text: `${res.serviceMessage ?? res.message}`,
});
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.ISSUES_API_ERROR,
text: error.message,
});
}
}
}
});
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, actionConnector, toastNotifications, query]);
return {
issues,
isLoading,
};
};

View file

@ -1,80 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { connector as actionConnector, issues } from '../mock';
import { useGetSingleIssue, UseGetSingleIssue } from './use_get_single_issue';
import * as api from './api';
jest.mock('../../../common/lib/kibana');
jest.mock('./api');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('useGetSingleIssue', () => {
const { http, notifications } = useKibanaMock().services;
beforeEach(() => jest.clearAllMocks());
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetSingleIssue>(() =>
useGetSingleIssue({
http,
toastNotifications: notifications.toasts,
actionConnector,
id: null,
})
);
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, issue: null });
});
});
test('fetch issues', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetSingleIssue>(() =>
useGetSingleIssue({
http,
toastNotifications: notifications.toasts,
actionConnector,
id: '123',
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
issue: issues[0],
});
});
});
test('unhappy path', async () => {
const spyOnGetCaseConfigure = jest.spyOn(api, 'getIssue');
spyOnGetCaseConfigure.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetSingleIssue>(() =>
useGetSingleIssue({
http,
toastNotifications: notifications.toasts,
actionConnector,
id: '123',
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, issue: null });
});
});
});

View file

@ -1,95 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getIssue } from './api';
import { Issue } from './types';
import * as i18n from './translations';
interface Props {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
id: string | null;
actionConnector?: ActionConnector;
}
export interface UseGetSingleIssue {
issue: Issue | null;
isLoading: boolean;
}
export const useGetSingleIssue = ({
http,
toastNotifications,
actionConnector,
id,
}: Props): UseGetSingleIssue => {
const [isLoading, setIsLoading] = useState(false);
const [issue, setIssue] = useState<Issue | null>(null);
const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
if (!actionConnector || !id) {
setIsLoading(false);
return;
}
abortCtrl.current = new AbortController();
setIsLoading(true);
try {
const res = await getIssue({
http,
signal: abortCtrl.current.signal,
connectorId: actionConnector.id,
id,
});
if (!didCancel.current) {
setIsLoading(false);
setIssue(res.data ?? null);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.GET_ISSUE_API_ERROR(id),
text: `${res.serviceMessage ?? res.message}`,
});
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.GET_ISSUE_API_ERROR(id),
text: error.message,
});
}
}
}
};
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, actionConnector, id, toastNotifications]);
return {
isLoading,
issue,
};
};

View file

@ -1,121 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const connector = {
id: '123',
name: 'My connector',
actionTypeId: '.jira',
config: {},
isPreconfigured: false,
};
export const issues = [
{ id: 'personId', title: 'Person Task', key: 'personKey' },
{ id: 'womanId', title: 'Woman Task', key: 'womanKey' },
{ id: 'manId', title: 'Man Task', key: 'manKey' },
{ id: 'cameraId', title: 'Camera Task', key: 'cameraKey' },
{ id: 'tvId', title: 'TV Task', key: 'tvKey' },
];
export const choices = [
{
dependent_value: '',
label: 'Priviledge Escalation',
value: 'Priviledge Escalation',
element: 'category',
},
{
dependent_value: '',
label: 'Criminal activity/investigation',
value: 'Criminal activity/investigation',
element: 'category',
},
{
dependent_value: '',
label: 'Denial of Service',
value: 'Denial of Service',
element: 'category',
},
{
dependent_value: 'Denial of Service',
label: 'Inbound or outbound',
value: '12',
element: 'subcategory',
},
{
dependent_value: 'Denial of Service',
label: 'Single or distributed (DoS or DDoS)',
value: '26',
element: 'subcategory',
},
{
dependent_value: 'Denial of Service',
label: 'Inbound DDos',
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) => [
{
dependent_value: '',
label: '1 - Critical',
value: '1',
element,
},
{
dependent_value: '',
label: '2 - High',
value: '2',
element,
},
{
dependent_value: '',
label: '3 - Moderate',
value: '3',
element,
},
{
dependent_value: '',
label: '4 - Low',
value: '4',
element,
},
])
.flat(),
];
export const severity = [
{
id: 4,
name: 'Low',
},
{
id: 5,
name: 'Medium',
},
{
id: 6,
name: 'High',
},
];
export const incidentTypes = [
{ id: 17, name: 'Communication error (fax; email)' },
{ id: 1001, name: 'Custom type' },
];

View file

@ -1,16 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { incidentTypes, severity } from '../../mock';
import { Props } from '../api';
import { ResilientIncidentTypes, ResilientSeverity } from '../types';
export const getIncidentTypes = async (props: Props): Promise<{ data: ResilientIncidentTypes }> =>
Promise.resolve({ data: incidentTypes });
export const getSeverity = async (props: Props): Promise<{ data: ResilientSeverity }> =>
Promise.resolve({ data: severity });

View file

@ -1,42 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../actions/common';
import { ResilientIncidentTypes, ResilientSeverity } from './types';
export const BASE_ACTION_API_PATH = '/api/actions';
export interface Props {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
}
export async function getIncidentTypes({ http, signal, connectorId }: Props) {
return http.post<ActionTypeExecutorResult<ResilientIncidentTypes>>(
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
{
body: JSON.stringify({
params: { subAction: 'incidentTypes', subActionParams: {} },
}),
signal,
}
);
}
export async function getSeverity({ http, signal, connectorId }: Props) {
return http.post<ActionTypeExecutorResult<ResilientSeverity>>(
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
{
body: JSON.stringify({
params: { subAction: 'severity', subActionParams: {} },
}),
signal,
}
);
}

View file

@ -1,134 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import { connector } from '../mock';
import { useGetIncidentTypes } from './use_get_incident_types';
import { useGetSeverity } from './use_get_severity';
import Fields from './case_fields';
jest.mock('../../../common/lib/kibana');
jest.mock('./use_get_incident_types');
jest.mock('./use_get_severity');
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
const useGetSeverityMock = useGetSeverity as jest.Mock;
describe('ResilientParamsFields renders', () => {
const useGetIncidentTypesResponse = {
isLoading: false,
incidentTypes: [
{
id: 19,
name: 'Malware',
},
{
id: 21,
name: 'Denial of Service',
},
],
};
const useGetSeverityResponse = {
isLoading: false,
severity: [
{
id: 4,
name: 'Low',
},
{
id: 5,
name: 'Medium',
},
{
id: 6,
name: 'High',
},
],
};
const fields = {
severityCode: '6',
incidentTypes: ['19'],
};
const onChange = jest.fn();
beforeEach(() => {
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
jest.clearAllMocks();
});
test('all params fields are rendered', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('options')).toEqual(
[
{ label: 'Malware', value: '19' },
{ label: 'Denial of Service', value: '21' },
]
);
expect(
wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('selectedOptions')
).toEqual([{ label: 'Malware', value: '19' }]);
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual(
'6'
);
});
test('it disabled the fields when loading incident types', () => {
useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true });
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(
wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled')
).toBeTruthy();
});
test('it disabled the fields when loading severity', () => {
useGetSeverityMock.mockReturnValue({
...useGetSeverityResponse,
isLoading: true,
});
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy();
});
test('it sets issue type correctly', async () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
await waitFor(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ value: '19', label: 'Denial of Service' }]);
});
expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '6' });
});
test('it sets severity correctly', async () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
wrapper
.find('select[data-test-subj="severitySelect"]')
.first()
.simulate('change', {
target: { value: '4' },
});
expect(onChange).toHaveBeenCalledWith({ incidentTypes: ['19'], severityCode: '4' });
});
});

View file

@ -1,189 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFormRow,
EuiSelect,
EuiSelectOption,
EuiSpacer,
} from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
import { ConnectorFieldsProps } from '../types';
import { useGetIncidentTypes } from './use_get_incident_types';
import { useGetSeverity } from './use_get_severity';
import * as i18n from './translations';
import { ConnectorTypes, ResilientFieldsType } from '../../../../common';
import { ConnectorCard } from '../card';
const ResilientFieldsComponent: React.FunctionComponent<
ConnectorFieldsProps<ResilientFieldsType>
> = ({ isEdit = true, fields, connector, onChange }) => {
const init = useRef(true);
const { incidentTypes = null, severityCode = null } = fields ?? {};
const { http, notifications } = useKibana().services;
const {
isLoading: isLoadingIncidentTypes,
incidentTypes: allIncidentTypes,
} = useGetIncidentTypes({
http,
toastNotifications: notifications.toasts,
connector,
});
const { isLoading: isLoadingSeverity, severity } = useGetSeverity({
http,
toastNotifications: notifications.toasts,
connector,
});
const severitySelectOptions: EuiSelectOption[] = useMemo(
() =>
severity.map((s) => ({
value: s.id.toString(),
text: s.name,
})),
[severity]
);
const incidentTypesComboBoxOptions: Array<EuiComboBoxOptionOption<string>> = useMemo(
() =>
allIncidentTypes
? allIncidentTypes.map((type: { id: number; name: string }) => ({
label: type.name,
value: type.id.toString(),
}))
: [],
[allIncidentTypes]
);
const listItems = useMemo(
() => [
...(incidentTypes != null && incidentTypes.length > 0
? [
{
title: i18n.INCIDENT_TYPES_LABEL,
description: allIncidentTypes
.filter((type) => incidentTypes.includes(type.id.toString()))
.map((type) => type.name)
.join(', '),
},
]
: []),
...(severityCode != null && severityCode.length > 0
? [
{
title: i18n.SEVERITY_LABEL,
description:
severity.find((severityObj) => severityObj.id.toString() === severityCode)?.name ??
'',
},
]
: []),
],
[incidentTypes, severityCode, allIncidentTypes, severity]
);
const onFieldChange = useCallback(
(key, value) => {
onChange({
...fields,
incidentTypes,
severityCode,
[key]: value,
});
},
[incidentTypes, severityCode, onChange, fields]
);
const selectedIncidentTypesComboBoxOptionsMemo = useMemo(() => {
const allIncidentTypesAsObject = allIncidentTypes.reduce(
(acc, type) => ({ ...acc, [type.id.toString()]: type.name }),
{} as Record<string, string>
);
return incidentTypes
? incidentTypes
.map((type) => ({
label: allIncidentTypesAsObject[type.toString()],
value: type.toString(),
}))
.filter((type) => type.label != null)
: [];
}, [allIncidentTypes, incidentTypes]);
const onIncidentChange = useCallback(
(selectedOptions: Array<{ label: string; value?: string }>) => {
onFieldChange(
'incidentTypes',
selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label)
);
},
[onFieldChange]
);
const onIncidentBlur = useCallback(() => {
if (!incidentTypes) {
onFieldChange('incidentTypes', []);
}
}, [incidentTypes, onFieldChange]);
// Set field at initialization
useEffect(() => {
if (init.current) {
init.current = false;
onChange({ incidentTypes, severityCode });
}
}, [incidentTypes, onChange, severityCode]);
return isEdit ? (
<span data-test-subj={'connector-fields-resilient'}>
<EuiFormRow fullWidth label={i18n.INCIDENT_TYPES_LABEL}>
<EuiComboBox
data-test-subj="incidentTypeComboBox"
fullWidth
isClearable={true}
isDisabled={isLoadingIncidentTypes}
isLoading={isLoadingIncidentTypes}
onBlur={onIncidentBlur}
onChange={onIncidentChange}
options={incidentTypesComboBoxOptions}
placeholder={i18n.INCIDENT_TYPES_PLACEHOLDER}
selectedOptions={selectedIncidentTypesComboBoxOptionsMemo}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow fullWidth label={i18n.SEVERITY_LABEL}>
<EuiSelect
data-test-subj="severitySelect"
disabled={isLoadingSeverity}
fullWidth
hasNoInitialSelection
isLoading={isLoadingSeverity}
onChange={(e) => onFieldChange('severityCode', e.target.value)}
options={severitySelectOptions}
value={severityCode ?? undefined}
/>
</EuiFormRow>
<EuiSpacer size="m" />
</span>
) : (
<ConnectorCard
connectorType={ConnectorTypes.resilient}
isLoading={isLoadingIncidentTypes || isLoadingSeverity}
listItems={listItems}
title={connector.name}
/>
);
};
// eslint-disable-next-line import/no-default-export
export { ResilientFieldsComponent as default };

View file

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
import { CaseConnector } from '../types';
import { ResilientFieldsType } from '../../../../common';
import * as i18n from './translations';
export * from './types';
export const getCaseConnector = (): CaseConnector<ResilientFieldsType> => {
return {
id: '.resilient',
fieldsComponent: lazy(() => import('./case_fields')),
};
};
export const fieldLabels = {
incidentTypes: i18n.INCIDENT_TYPES_LABEL,
severityCode: i18n.SEVERITY_LABEL,
};

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const INCIDENT_TYPES_API_ERROR = i18n.translate(
'xpack.cases.connectors.resilient.unableToGetIncidentTypesMessage',
{
defaultMessage: 'Unable to get incident types',
}
);
export const SEVERITY_API_ERROR = i18n.translate(
'xpack.cases.connectors.resilient.unableToGetSeverityMessage',
{
defaultMessage: 'Unable to get severity',
}
);
export const INCIDENT_TYPES_PLACEHOLDER = i18n.translate(
'xpack.cases.connectors.resilient.incidentTypesPlaceholder',
{
defaultMessage: 'Choose types',
}
);
export const INCIDENT_TYPES_LABEL = i18n.translate(
'xpack.cases.connectors.resilient.incidentTypesLabel',
{
defaultMessage: 'Incident Types',
}
);
export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.resilient.severityLabel', {
defaultMessage: 'Severity',
});

View file

@ -1,9 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type ResilientIncidentTypes = Array<{ id: number; name: string }>;
export type ResilientSeverity = ResilientIncidentTypes;

View file

@ -1,71 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { connector } from '../mock';
import { useGetIncidentTypes, UseGetIncidentTypes } from './use_get_incident_types';
import * as api from './api';
jest.mock('../../../common/lib/kibana');
jest.mock('./api');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('useGetIncidentTypes', () => {
const { http, notifications } = useKibanaMock().services;
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIncidentTypes>(() =>
useGetIncidentTypes({ http, toastNotifications: notifications.toasts })
);
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: true, incidentTypes: [] });
});
});
test('fetch incident types', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIncidentTypes>(() =>
useGetIncidentTypes({
http,
toastNotifications: notifications.toasts,
connector,
})
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
incidentTypes: [
{ id: 17, name: 'Communication error (fax; email)' },
{ id: 1001, name: 'Custom type' },
],
});
});
});
test('unhappy path', async () => {
const spyOnGetCaseConfigure = jest.spyOn(api, 'getIncidentTypes');
spyOnGetCaseConfigure.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetIncidentTypes>(() =>
useGetIncidentTypes({ http, toastNotifications: notifications.toasts, connector })
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, incidentTypes: [] });
});
});
});

View file

@ -1,94 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getIncidentTypes } from './api';
import * as i18n from './translations';
type IncidentTypes = Array<{ id: number; name: string }>;
interface Props {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
connector?: ActionConnector;
}
export interface UseGetIncidentTypes {
incidentTypes: IncidentTypes;
isLoading: boolean;
}
export const useGetIncidentTypes = ({
http,
toastNotifications,
connector,
}: Props): UseGetIncidentTypes => {
const [isLoading, setIsLoading] = useState(true);
const [incidentTypes, setIncidentTypes] = useState<IncidentTypes>([]);
const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
try {
abortCtrl.current = new AbortController();
setIsLoading(true);
const res = await getIncidentTypes({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
});
if (!didCancel.current) {
setIsLoading(false);
setIncidentTypes(res.data ?? []);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.INCIDENT_TYPES_API_ERROR,
text: `${res.serviceMessage ?? res.message}`,
});
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.INCIDENT_TYPES_API_ERROR,
text: error.message,
});
}
}
}
};
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, connector, toastNotifications]);
return {
incidentTypes,
isLoading,
};
};

View file

@ -1,77 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { connector } from '../mock';
import { useGetSeverity, UseGetSeverity } from './use_get_severity';
import * as api from './api';
jest.mock('../../../common/lib/kibana');
jest.mock('./api');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('useGetSeverity', () => {
const { http, notifications } = useKibanaMock().services;
test('init', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetSeverity>(() =>
useGetSeverity({ http, toastNotifications: notifications.toasts })
);
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: true, severity: [] });
});
});
test('fetch severity', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetSeverity>(() =>
useGetSeverity({ http, toastNotifications: notifications.toasts, connector })
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
severity: [
{
id: 4,
name: 'Low',
},
{
id: 5,
name: 'Medium',
},
{
id: 6,
name: 'High',
},
],
});
});
});
test('unhappy path', async () => {
const spyOnGetCaseConfigure = jest.spyOn(api, 'getSeverity');
spyOnGetCaseConfigure.mockImplementation(() => {
throw new Error('Something went wrong');
});
await act(async () => {
const { result, waitForNextUpdate } = renderHook<string, UseGetSeverity>(() =>
useGetSeverity({ http, toastNotifications: notifications.toasts, connector })
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual({ isLoading: false, severity: [] });
});
});
});

View file

@ -1,91 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getSeverity } from './api';
import * as i18n from './translations';
type Severity = Array<{ id: number; name: string }>;
interface Props {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
connector?: ActionConnector;
}
export interface UseGetSeverity {
severity: Severity;
isLoading: boolean;
}
export const useGetSeverity = ({ http, toastNotifications, connector }: Props): UseGetSeverity => {
const [isLoading, setIsLoading] = useState(true);
const [severity, setSeverity] = useState<Severity>([]);
const abortCtrl = useRef(new AbortController());
const didCancel = useRef(false);
useEffect(() => {
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
try {
abortCtrl.current = new AbortController();
setIsLoading(true);
const res = await getSeverity({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
});
if (!didCancel.current) {
setIsLoading(false);
setSeverity(res.data ?? []);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.SEVERITY_API_ERROR,
text: `${res.serviceMessage ?? res.message}`,
});
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.SEVERITY_API_ERROR,
text: error.message,
});
}
}
}
};
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
}, [http, connector, toastNotifications]);
return {
severity,
isLoading,
};
};

View file

@ -1,19 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { choices } from '../../mock';
import { GetChoicesProps } from '../api';
import { Choice } from '../types';
export const choicesResponse = {
status: 'ok',
data: choices,
};
export const getChoices = async (
props: GetChoicesProps
): Promise<{ status: string; data: Choice[] }> => Promise.resolve(choicesResponse);

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { httpServiceMock } from '../../../../../../../src/core/public/mocks';
import { getChoices } from './api';
import { choices } from '../mock';
const choicesResponse = {
status: 'ok',
data: choices,
};
describe('ServiceNow API', () => {
const http = httpServiceMock.createStartContract();
beforeEach(() => jest.resetAllMocks());
describe('getChoices', () => {
test('should call get choices API', async () => {
const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(choicesResponse);
const res = await getChoices({
http,
signal: abortCtrl.signal,
connectorId: 'test',
fields: ['priority'],
});
expect(res).toEqual(choicesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', {
body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}',
signal: abortCtrl.signal,
});
});
});
});

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpSetup } from 'kibana/public';
import { ActionTypeExecutorResult } from '../../../../../actions/common';
import { Choice } from './types';
export const BASE_ACTION_API_PATH = '/api/actions';
export interface GetChoicesProps {
http: HttpSetup;
signal: AbortSignal;
connectorId: string;
fields: string[];
}
export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) {
return http.post<ActionTypeExecutorResult<Choice[]>>(
`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`,
{
body: JSON.stringify({
params: { subAction: 'getChoices', subActionParams: { fields } },
}),
signal,
}
);
}

View file

@ -1,12 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 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 }));

View file

@ -1,32 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { lazy } from 'react';
import { CaseConnector } from '../types';
import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../common';
import * as i18n from './translations';
export const getServiceNowITSMCaseConnector = (): CaseConnector<ServiceNowITSMFieldsType> => {
return {
id: '.servicenow',
fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')),
};
};
export const getServiceNowSIRCaseConnector = (): CaseConnector<ServiceNowSIRFieldsType> => {
return {
id: '.servicenow-sir',
fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')),
};
};
export const serviceNowITSMFieldLabels = {
impact: i18n.IMPACT,
severity: i18n.SEVERITY,
urgency: i18n.URGENCY,
};

View file

@ -1,164 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { waitFor, act } from '@testing-library/react';
import { EuiSelect } from '@elastic/eui';
import { mount } from 'enzyme';
import { connector, choices as mockChoices } from '../mock';
import { Choice } from './types';
import Fields from './servicenow_itsm_case_fields';
let onChoicesSuccess = (c: Choice[]) => {};
jest.mock('../../../common/lib/kibana');
jest.mock('./use_get_choices', () => ({
useGetChoices: (args: { onSuccess: () => void }) => {
onChoicesSuccess = args.onSuccess;
return { isLoading: false, choices: mockChoices };
},
}));
describe('ServiceNowITSM Fields', () => {
const fields = {
severity: '1',
urgency: '2',
impact: '3',
category: 'software',
subcategory: 'os',
};
const onChange = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('all params fields are rendered - isEdit: true', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
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', () => {
const wrapper = mount(
<Fields isEdit={false} fields={fields} onChange={onChange} connector={connector} />
);
act(() => {
onChoicesSuccess(mockChoices);
});
expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual(
'Urgency: 2 - High'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual(
'Severity: 1 - Critical'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual(
'Impact: 3 - Moderate'
);
});
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(() => {
onChoicesSuccess(mockChoices);
});
wrapper.update();
const testers = ['severity', 'urgency', 'impact'];
testers.forEach((subj) =>
expect(wrapper.find(`[data-test-subj="${subj}Select"]`).first().prop('options')).toEqual([
{ value: '1', text: '1 - Critical' },
{ value: '2', text: '2 - High' },
{ value: '3', text: '3 - Moderate' },
{ value: '4', text: '4 - Low' },
])
);
});
describe('onChange calls', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(onChange).toHaveBeenCalledWith(fields);
const testers = ['severity', 'urgency', 'impact', 'subcategory'];
testers.forEach((subj) =>
test(`${subj.toUpperCase()}`, async () => {
await waitFor(() => {
const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!;
select.prop('onChange')!({
target: {
value: '9',
},
} as React.ChangeEvent<HTMLSelectElement>);
});
wrapper.update();
expect(onChange).toHaveBeenCalledWith({
...fields,
[subj]: '9',
});
})
);
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',
});
});
});
});

View file

@ -1,235 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import * as i18n from './translations';
import { ConnectorFieldsProps } from '../types';
import { ConnectorTypes, ServiceNowITSMFieldsType } from '../../../../common';
import { useKibana } from '../../../common/lib/kibana';
import { ConnectorCard } from '../card';
import { useGetChoices } from './use_get_choices';
import { Fields, Choice } from './types';
import { choicesToEuiOptions } from './helpers';
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, category = null, subcategory = null } =
fields ?? {};
const { http, notifications } = useKibana().services;
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(
() => [
...(urgency != null && urgency.length > 0
? [
{
title: i18n.URGENCY,
description: urgencyOptions.find((option) => `${option.value}` === urgency)?.text,
},
]
: []),
...(severity != null && severity.length > 0
? [
{
title: i18n.SEVERITY,
description: severityOptions.find((option) => `${option.value}` === severity)?.text,
},
]
: []),
...(impact != null && impact.length > 0
? [
{
title: i18n.IMPACT,
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,
},
]
: []),
],
[
category,
categoryOptions,
impact,
impactOptions,
severity,
severityOptions,
subcategory,
subcategoryOptions,
urgency,
urgencyOptions,
]
);
const onChoicesSuccess = (values: Choice[]) => {
setChoices(
values.reduce(
(acc, value) => ({
...acc,
[value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value],
}),
defaultFields
)
);
};
const { isLoading: isLoadingChoices } = useGetChoices({
http,
toastNotifications: notifications.toasts,
connector,
fields: useGetChoicesFields,
onSuccess: onChoicesSuccess,
});
const onChangeCb = useCallback(
(
key: keyof ServiceNowITSMFieldsType,
value: ServiceNowITSMFieldsType[keyof ServiceNowITSMFieldsType]
) => {
onChange({ ...fields, [key]: value });
},
[fields, onChange]
);
// Set field at initialization
useEffect(() => {
if (init.current) {
init.current = false;
onChange({ urgency, severity, impact, category, subcategory });
}
}, [category, impact, onChange, severity, subcategory, urgency]);
return isEdit ? (
<div data-test-subj={'connector-fields-sn-itsm'}>
<EuiFormRow fullWidth label={i18n.URGENCY}>
<EuiSelect
fullWidth
data-test-subj="urgencySelect"
options={urgencyOptions}
value={urgency ?? undefined}
isLoading={isLoadingChoices}
disabled={isLoadingChoices}
hasNoInitialSelection
onChange={(e) => onChangeCb('urgency', e.target.value)}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.SEVERITY}>
<EuiSelect
fullWidth
data-test-subj="severitySelect"
options={severityOptions}
value={severity ?? undefined}
isLoading={isLoadingChoices}
disabled={isLoadingChoices}
hasNoInitialSelection
onChange={(e) => onChangeCb('severity', e.target.value)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.IMPACT}>
<EuiSelect
fullWidth
data-test-subj="impactSelect"
options={impactOptions}
value={impact ?? undefined}
isLoading={isLoadingChoices}
disabled={isLoadingChoices}
hasNoInitialSelection
onChange={(e) => onChangeCb('impact', e.target.value)}
/>
</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
connectorType={ConnectorTypes.serviceNowITSM}
title={connector.name}
listItems={listItems}
isLoading={false}
/>
);
};
// eslint-disable-next-line import/no-default-export
export { ServiceNowITSMFieldsComponent as default };

View file

@ -1,221 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { waitFor, act } from '@testing-library/react';
import { EuiSelect } from '@elastic/eui';
import { connector, choices as mockChoices } from '../mock';
import { Choice } from './types';
import Fields from './servicenow_sir_case_fields';
let onChoicesSuccess = (c: Choice[]) => {};
jest.mock('../../../common/lib/kibana');
jest.mock('./use_get_choices', () => ({
useGetChoices: (args: { onSuccess: () => void }) => {
onChoicesSuccess = args.onSuccess;
return { isLoading: false, mockChoices };
},
}));
describe('ServiceNowSIR Fields', () => {
const fields = {
destIp: true,
sourceIp: true,
malwareHash: true,
malwareUrl: true,
priority: '1',
category: 'Denial of Service',
subcategory: '26',
};
const onChange = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('all params fields are rendered - isEdit: true', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
expect(wrapper.find('[data-test-subj="destIpCheckbox"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="sourceIpCheckbox"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="malwareUrlCheckbox"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="malwareHashCheckbox"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy();
});
test('all params fields are rendered - isEdit: false', () => {
const wrapper = mount(
<Fields isEdit={false} fields={fields} onChange={onChange} connector={connector} />
);
act(() => {
onChoicesSuccess(mockChoices);
});
wrapper.update();
expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual(
'Destination IP: Yes'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual(
'Source IP: Yes'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual(
'Malware URL: Yes'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(3).text()).toEqual(
'Malware Hash: Yes'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(4).text()).toEqual(
'Priority: 1 - Critical'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(5).text()).toEqual(
'Category: Denial of Service'
);
expect(wrapper.find('[data-test-subj="card-list-item"]').at(6).text()).toEqual(
'Subcategory: Single or distributed (DoS or DDoS)'
);
});
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' },
{
text: 'Software',
value: '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: 'Inbound or outbound',
value: '12',
},
{
text: 'Single or distributed (DoS or DDoS)',
value: '26',
},
{
text: 'Inbound DDos',
value: 'inbound_ddos',
},
]);
});
test('it transforms the priorities 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="prioritySelect"]').first().prop('options')).toEqual([
{
text: '1 - Critical',
value: '1',
},
{
text: '2 - High',
value: '2',
},
{
text: '3 - Moderate',
value: '3',
},
{
text: '4 - Low',
value: '4',
},
]);
});
describe('onChange calls', () => {
const wrapper = mount(<Fields fields={fields} onChange={onChange} connector={connector} />);
act(() => {
onChoicesSuccess(mockChoices);
});
wrapper.update();
expect(onChange).toHaveBeenCalledWith(fields);
const checkbox = ['destIp', 'sourceIp', 'malwareHash', 'malwareUrl'];
checkbox.forEach((subj) =>
test(`${subj.toUpperCase()}`, async () => {
await waitFor(() => {
wrapper
.find(`[data-test-subj="${subj}Checkbox"] input`)
.first()
.simulate('change', { target: { checked: false } });
expect(onChange).toHaveBeenCalledWith({
...fields,
[subj]: false,
});
});
})
);
const testers = ['priority', 'subcategory'];
testers.forEach((subj) =>
test(`${subj.toUpperCase()}`, async () => {
await waitFor(() => {
const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!;
select.prop('onChange')!({
target: {
value: '9',
},
} as React.ChangeEvent<HTMLSelectElement>);
});
wrapper.update();
expect(onChange).toHaveBeenCalledWith({
...fields,
[subj]: '9',
});
})
);
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',
});
});
});
});
});

View file

@ -1,282 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui';
import { ConnectorTypes, ServiceNowSIRFieldsType } from '../../../../common';
import { useKibana } from '../../../common/lib/kibana';
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';
const useGetChoicesFields = ['category', 'subcategory', 'priority'];
const defaultFields: Fields = {
category: [],
subcategory: [],
priority: [],
};
const ServiceNowSIRFieldsComponent: React.FunctionComponent<
ConnectorFieldsProps<ServiceNowSIRFieldsType>
> = ({ isEdit = true, fields, connector, onChange }) => {
const init = useRef(true);
const {
category = null,
destIp = true,
malwareHash = true,
malwareUrl = true,
priority = null,
sourceIp = true,
subcategory = null,
} = fields ?? {};
const { http, notifications } = useKibana().services;
const [choices, setChoices] = useState<Fields>(defaultFields);
const onChangeCb = useCallback(
(
key: keyof ServiceNowSIRFieldsType,
value: ServiceNowSIRFieldsType[keyof ServiceNowSIRFieldsType]
) => {
onChange({ ...fields, [key]: value });
},
[fields, onChange]
);
const onChoicesSuccess = (values: Choice[]) => {
setChoices(
values.reduce(
(acc, value) => ({
...acc,
[value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value],
}),
defaultFields
)
);
};
const { isLoading: isLoadingChoices } = useGetChoices({
http,
toastNotifications: notifications.toasts,
connector,
fields: useGetChoicesFields,
onSuccess: onChoicesSuccess,
});
const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]);
const priorityOptions = useMemo(() => choicesToEuiOptions(choices.priority), [choices.priority]);
const subcategoryOptions = useMemo(
() =>
choicesToEuiOptions(
choices.subcategory.filter((choice) => choice.dependent_value === category)
),
[choices.subcategory, category]
);
const listItems = useMemo(
() => [
...(destIp != null && destIp
? [
{
title: i18n.DEST_IP,
description: i18n.ALERT_FIELD_ENABLED_TEXT,
},
]
: []),
...(sourceIp != null && sourceIp
? [
{
title: i18n.SOURCE_IP,
description: i18n.ALERT_FIELD_ENABLED_TEXT,
},
]
: []),
...(malwareUrl != null && malwareUrl
? [
{
title: i18n.MALWARE_URL,
description: i18n.ALERT_FIELD_ENABLED_TEXT,
},
]
: []),
...(malwareHash != null && malwareHash
? [
{
title: i18n.MALWARE_HASH,
description: i18n.ALERT_FIELD_ENABLED_TEXT,
},
]
: []),
...(priority != null && priority.length > 0
? [
{
title: i18n.PRIORITY,
description: priorityOptions.find((option) => `${option.value}` === priority)?.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,
},
]
: []),
],
[
category,
categoryOptions,
destIp,
malwareHash,
malwareUrl,
priority,
priorityOptions,
sourceIp,
subcategory,
subcategoryOptions,
]
);
// Set field at initialization
useEffect(() => {
if (init.current) {
init.current = false;
onChange({ category, destIp, malwareHash, malwareUrl, priority, sourceIp, subcategory });
}
}, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]);
return isEdit ? (
<div data-test-subj={'connector-fields-sn-sir'}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.ALERT_FIELDS_LABEL}>
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiCheckbox
id="destIpCheckbox"
data-test-subj="destIpCheckbox"
label={i18n.DEST_IP}
checked={destIp ?? false}
compressed
onChange={(e) => onChangeCb('destIp', e.target.checked)}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCheckbox
id="sourceIpCheckbox"
data-test-subj="sourceIpCheckbox"
label={i18n.SOURCE_IP}
checked={sourceIp ?? false}
compressed
onChange={(e) => onChangeCb('sourceIp', e.target.checked)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiCheckbox
id="malwareUrlCheckbox"
data-test-subj="malwareUrlCheckbox"
label={i18n.MALWARE_URL}
checked={malwareUrl ?? false}
compressed
onChange={(e) => onChangeCb('malwareUrl', e.target.checked)}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCheckbox
id="malwareHashCheckbox"
data-test-subj="malwareHashCheckbox"
label={i18n.MALWARE_HASH}
checked={malwareHash ?? false}
compressed
onChange={(e) => onChangeCb('malwareHash', e.target.checked)}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow fullWidth label={i18n.PRIORITY}>
<EuiSelect
fullWidth
data-test-subj="prioritySelect"
hasNoInitialSelection
isLoading={isLoadingChoices}
disabled={isLoadingChoices}
options={priorityOptions}
value={priority ?? undefined}
onChange={(e) => onChangeCb('priority', e.target.value)}
/>
</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
connectorType={ConnectorTypes.serviceNowITSM}
title={connector.name}
listItems={listItems}
isLoading={false}
/>
);
};
// eslint-disable-next-line import/no-default-export
export { ServiceNowSIRFieldsComponent as default };

View file

@ -1,75 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const URGENCY = i18n.translate('xpack.cases.connectors.serviceNow.urgencySelectFieldLabel', {
defaultMessage: 'Urgency',
});
export const SEVERITY = i18n.translate(
'xpack.cases.connectors.serviceNow.severitySelectFieldLabel',
{
defaultMessage: 'Severity',
}
);
export const IMPACT = i18n.translate('xpack.cases.connectors.serviceNow.impactSelectFieldLabel', {
defaultMessage: 'Impact',
});
export const CHOICES_API_ERROR = i18n.translate(
'xpack.cases.connectors.serviceNow.unableToGetChoicesMessage',
{
defaultMessage: 'Unable to get choices',
}
);
export const MALWARE_URL = i18n.translate('xpack.cases.connectors.serviceNow.malwareURLTitle', {
defaultMessage: 'Malware URL',
});
export const MALWARE_HASH = i18n.translate('xpack.cases.connectors.serviceNow.malwareHashTitle', {
defaultMessage: 'Malware Hash',
});
export const CATEGORY = i18n.translate('xpack.cases.connectors.serviceNow.categoryTitle', {
defaultMessage: 'Category',
});
export const SUBCATEGORY = i18n.translate('xpack.cases.connectors.serviceNow.subcategoryTitle', {
defaultMessage: 'Subcategory',
});
export const SOURCE_IP = i18n.translate('xpack.cases.connectors.serviceNow.sourceIPTitle', {
defaultMessage: 'Source IP',
});
export const DEST_IP = i18n.translate('xpack.cases.connectors.serviceNow.destinationIPTitle', {
defaultMessage: 'Destination IP',
});
export const PRIORITY = i18n.translate(
'xpack.cases.connectors.serviceNow.prioritySelectFieldTitle',
{
defaultMessage: 'Priority',
}
);
export const ALERT_FIELDS_LABEL = i18n.translate(
'xpack.cases.connectors.serviceNow.alertFieldsTitle',
{
defaultMessage: 'Select Observables to push',
}
);
export const ALERT_FIELD_ENABLED_TEXT = i18n.translate(
'xpack.cases.connectors.serviceNow.alertFieldEnabledText',
{
defaultMessage: 'Yes',
}
);

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export interface Choice {
value: string;
label: string;
dependent_value: string;
element: string;
}
export type Fields = Record<string, Choice[]>;

View file

@ -1,144 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { useKibana } from '../../../common/lib/kibana';
import { ActionConnector } from '../../../containers/types';
import { choices } from '../mock';
import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices';
import * as api from './api';
jest.mock('./api');
jest.mock('../../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const onSuccess = jest.fn();
const fields = ['priority'];
const connector = {
secrets: {
username: 'user',
password: 'pass',
},
id: 'test',
actionTypeId: '.servicenow',
name: 'ServiceNow',
isPreconfigured: false,
config: {
apiUrl: 'https://dev94428.service-now.com/',
},
} as ActionConnector;
describe('useGetChoices', () => {
const { services } = useKibanaMock();
beforeEach(() => {
jest.clearAllMocks();
});
it('init', async () => {
const { result, waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
useGetChoices({
http: services.http,
connector,
toastNotifications: services.notifications.toasts,
fields,
onSuccess,
})
);
await waitForNextUpdate();
expect(result.current).toEqual({
isLoading: false,
choices,
});
});
it('returns an empty array when connector is not presented', async () => {
const { result } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
useGetChoices({
http: services.http,
connector: undefined,
toastNotifications: services.notifications.toasts,
fields,
onSuccess,
})
);
expect(result.current).toEqual({
isLoading: false,
choices: [],
});
});
it('it calls onSuccess', async () => {
const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
useGetChoices({
http: services.http,
connector,
toastNotifications: services.notifications.toasts,
fields,
onSuccess,
})
);
await waitForNextUpdate();
expect(onSuccess).toHaveBeenCalledWith(choices);
});
it('it displays an error when service fails', async () => {
const spyOnGetChoices = jest.spyOn(api, 'getChoices');
spyOnGetChoices.mockResolvedValue(
Promise.resolve({
actionId: 'test',
status: 'error',
serviceMessage: 'An error occurred',
})
);
const { waitForNextUpdate } = renderHook<UseGetChoicesProps, UseGetChoices>(() =>
useGetChoices({
http: services.http,
connector,
toastNotifications: services.notifications.toasts,
fields,
onSuccess,
})
);
await waitForNextUpdate();
expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({
text: 'An error occurred',
title: 'Unable to get choices',
});
});
it('it displays an error when http throws an error', async () => {
const spyOnGetChoices = jest.spyOn(api, 'getChoices');
spyOnGetChoices.mockImplementation(() => {
throw new Error('An error occurred');
});
renderHook<UseGetChoicesProps, UseGetChoices>(() =>
useGetChoices({
http: services.http,
connector,
toastNotifications: services.notifications.toasts,
fields,
onSuccess,
})
);
expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({
text: 'An error occurred',
title: 'Unable to get choices',
});
});
});

View file

@ -1,101 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useRef } from 'react';
import { HttpSetup, ToastsApi } from 'kibana/public';
import { ActionConnector } from '../../../containers/types';
import { getChoices } from './api';
import { Choice } from './types';
import * as i18n from './translations';
export interface UseGetChoicesProps {
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
connector?: ActionConnector;
fields: string[];
onSuccess?: (choices: Choice[]) => void;
}
export interface UseGetChoices {
choices: Choice[];
isLoading: boolean;
}
export const useGetChoices = ({
http,
connector,
toastNotifications,
fields,
onSuccess,
}: UseGetChoicesProps): UseGetChoices => {
const [isLoading, setIsLoading] = useState(false);
const [choices, setChoices] = useState<Choice[]>([]);
const didCancel = useRef(false);
const abortCtrl = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
if (!connector) {
setIsLoading(false);
return;
}
try {
abortCtrl.current = new AbortController();
setIsLoading(true);
const res = await getChoices({
http,
signal: abortCtrl.current.signal,
connectorId: connector.id,
fields,
});
if (!didCancel.current) {
setIsLoading(false);
setChoices(res.data ?? []);
if (res.status && res.status === 'error') {
toastNotifications.addDanger({
title: i18n.CHOICES_API_ERROR,
text: `${res.serviceMessage ?? res.message}`,
});
} else if (onSuccess) {
onSuccess(res.data ?? []);
}
}
} catch (error) {
if (!didCancel.current) {
setIsLoading(false);
if (error.name !== 'AbortError') {
toastNotifications.addDanger({
title: i18n.CHOICES_API_ERROR,
text: error.message,
});
}
}
}
};
didCancel.current = false;
abortCtrl.current.abort();
fetchData();
return () => {
didCancel.current = true;
abortCtrl.current.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [http, connector, toastNotifications, fields]);
return {
choices,
isLoading,
};
};

View file

@ -1,53 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
ActionType as ThirdPartySupportedActions,
CaseField,
ActionConnector,
ConnectorTypeFields,
} from '../../../common';
export { ThirdPartyField as AllThirdPartyFields } from '../../../common';
export type CaseActionConnector = ActionConnector;
export interface ThirdPartyField {
label: string;
validSourceFields: CaseField[];
defaultSourceField: CaseField;
defaultActionType: ThirdPartySupportedActions;
}
export interface ConnectorConfiguration {
name: string;
logo: string;
}
export interface CaseConnector<UIProps = unknown> {
id: string;
fieldsComponent: React.LazyExoticComponent<
React.ComponentType<ConnectorFieldsProps<UIProps>>
> | null;
}
export interface CaseConnectorsRegistry {
has: (id: string) => boolean;
register: <UIProps extends ConnectorTypeFields['fields']>(
connector: CaseConnector<UIProps>
) => void;
get: <UIProps extends ConnectorTypeFields['fields']>(id: string) => CaseConnector<UIProps>;
list: () => CaseConnector[];
}
export interface ConnectorFieldsProps<TFields> {
isEdit?: boolean;
connector: CaseActionConnector;
fields: TFields;
onChange: (fields: TFields) => void;
}

View file

@ -1,187 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { act, waitFor } from '@testing-library/react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { useForm, Form, FormHook } from '../../common/shared_imports';
import { connectorsMock } from '../../containers/mock';
import { Connector } from './connector';
import { useConnectors } from '../../containers/configure/use_connectors';
import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types';
import { useGetSeverity } from '../connectors/resilient/use_get_severity';
import { useGetChoices } from '../connectors/servicenow/use_get_choices';
import { incidentTypes, severity, choices } from '../connectors/mock';
import { schema, FormProps } from './schema';
jest.mock('../../common/lib/kibana', () => {
return {
useKibana: () => ({
services: {
notifications: {},
http: {},
},
}),
};
});
jest.mock('../../containers/configure/use_connectors');
jest.mock('../connectors/resilient/use_get_incident_types');
jest.mock('../connectors/resilient/use_get_severity');
jest.mock('../connectors/servicenow/use_get_choices');
const useConnectorsMock = useConnectors as jest.Mock;
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
const useGetSeverityMock = useGetSeverity as jest.Mock;
const useGetChoicesMock = useGetChoices as jest.Mock;
const useGetIncidentTypesResponse = {
isLoading: false,
incidentTypes,
};
const useGetSeverityResponse = {
isLoading: false,
severity,
};
const useGetChoicesResponse = {
isLoading: false,
choices,
};
describe('Connector', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<FormProps>({
defaultValue: { connectorId: connectorsMock[0].id, fields: null },
schema: {
connectorId: schema.connectorId,
fields: schema.fields,
},
});
globalForm = form;
return <Form form={form}>{children}</Form>;
};
beforeEach(() => {
jest.resetAllMocks();
useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock });
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
useGetChoicesMock.mockReturnValue(useGetChoicesResponse);
});
it('it renders', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<Connector isLoading={false} />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy();
await waitFor(() => {
expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe(
'My Connector'
);
});
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy();
});
});
it('it is loading when fetching connectors', async () => {
useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock });
const wrapper = mount(
<MockHookWrapperComponent>
<Connector isLoading={false} />
</MockHookWrapperComponent>
);
expect(
wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading')
).toEqual(true);
});
it('it is disabled when fetching connectors', async () => {
useConnectorsMock.mockReturnValue({ loading: true, connectors: connectorsMock });
const wrapper = mount(
<MockHookWrapperComponent>
<Connector isLoading={false} />
</MockHookWrapperComponent>
);
expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual(
true
);
});
it('it is disabled and loading when passing loading as true', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<Connector isLoading={true} />
</MockHookWrapperComponent>
);
expect(
wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('isLoading')
).toEqual(true);
expect(wrapper.find('[data-test-subj="dropdown-connectors"]').first().prop('disabled')).toEqual(
true
);
});
it(`it should change connector`, async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<Connector isLoading={false} />
</MockHookWrapperComponent>
);
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click');
wrapper.update();
});
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy();
});
act(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ value: '19', label: 'Denial of Service' }]);
});
act(() => {
wrapper
.find('select[data-test-subj="severitySelect"]')
.first()
.simulate('change', {
target: { value: '4' },
});
});
await waitFor(() => {
expect(globalForm.getFormData()).toEqual({
connectorId: 'resilient-2',
fields: { incidentTypes: ['19'], severityCode: '4' },
});
});
});
});

View file

@ -1,103 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ConnectorTypes } from '../../../common';
import { UseField, useFormData, FieldHook, useFormContext } from '../../common/shared_imports';
import { useConnectors } from '../../containers/configure/use_connectors';
import { ConnectorSelector } from '../connector_selector/form';
import { ConnectorFieldsForm } from '../connectors/fields_form';
import { ActionConnector } from '../../containers/types';
import { getConnectorById } from '../configure_cases/utils';
import { FormProps } from './schema';
interface Props {
isLoading: boolean;
hideConnectorServiceNowSir?: boolean;
}
interface ConnectorsFieldProps {
connectors: ActionConnector[];
field: FieldHook<FormProps['fields']>;
isEdit: boolean;
hideConnectorServiceNowSir?: boolean;
}
const ConnectorFields = ({
connectors,
isEdit,
field,
hideConnectorServiceNowSir = false,
}: ConnectorsFieldProps) => {
const [{ connectorId }] = useFormData({ watch: ['connectorId'] });
const { setValue } = field;
let connector = getConnectorById(connectorId, connectors) ?? null;
if (
connector &&
hideConnectorServiceNowSir &&
connector.actionTypeId === ConnectorTypes.serviceNowSIR
) {
connector = null;
}
return (
<ConnectorFieldsForm
connector={connector}
fields={field.value}
isEdit={isEdit}
onChange={setValue}
/>
);
};
const ConnectorComponent: React.FC<Props> = ({ hideConnectorServiceNowSir = false, isLoading }) => {
const { getFields } = useFormContext();
const { loading: isLoadingConnectors, connectors } = useConnectors();
const handleConnectorChange = useCallback(
(newConnector) => {
const { fields } = getFields();
fields.setValue(null);
},
[getFields]
);
return (
<EuiFlexGroup>
<EuiFlexItem>
<UseField
path="connectorId"
component={ConnectorSelector}
componentProps={{
connectors,
handleChange: handleConnectorChange,
hideConnectorServiceNowSir,
dataTestSubj: 'caseConnectors',
disabled: isLoading || isLoadingConnectors,
idAria: 'caseConnectors',
isLoading: isLoading || isLoadingConnectors,
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<UseField
path="fields"
component={ConnectorFields}
componentProps={{
connectors,
hideConnectorServiceNowSir,
isEdit: true,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
ConnectorComponent.displayName = 'ConnectorComponent';
export const Connector = memo(ConnectorComponent);

View file

@ -1,62 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { act } from '@testing-library/react';
import { useForm, Form, FormHook } from '../../common/shared_imports';
import { Description } from './description';
import { schema, FormProps } from './schema';
describe('Description', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<FormProps>({
defaultValue: { description: 'My description' },
schema: {
description: schema.description,
},
});
globalForm = form;
return <Form form={form}>{children}</Form>;
};
beforeEach(() => {
jest.resetAllMocks();
});
it('it renders', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<Description isLoading={false} />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy();
});
it('it changes the description', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<Description isLoading={false} />
</MockHookWrapperComponent>
);
await act(async () => {
wrapper
.find(`[data-test-subj="caseDescription"] textarea`)
.first()
.simulate('change', { target: { value: 'My new description' } });
});
expect(globalForm.getFormData()).toEqual({ description: 'My new description' });
});
});

View file

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo } from 'react';
import { MarkdownEditorForm } from '../markdown_editor';
import { UseField } from '../../common/shared_imports';
interface Props {
isLoading: boolean;
}
export const fieldName = 'description';
const DescriptionComponent: React.FC<Props> = ({ isLoading }) => (
<UseField
path={fieldName}
component={MarkdownEditorForm}
componentProps={{
dataTestSubj: 'caseDescription',
idAria: 'caseDescription',
isDisabled: isLoading,
}}
/>
);
DescriptionComponent.displayName = 'DescriptionComponent';
export const Description = memo(DescriptionComponent);

View file

@ -1,115 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ReactNode } from 'react';
import { mount } from 'enzyme';
import { CreateCaseFlyout } from './flyout';
import { TestProviders } from '../../common/mock';
jest.mock('../create/form_context', () => {
return {
FormContext: ({
children,
onSuccess,
}: {
children: ReactNode;
onSuccess: ({ id }: { id: string }) => Promise<void>;
}) => {
return (
<>
<button
type="button"
data-test-subj="form-context-on-success"
onClick={async () => {
await onSuccess({ id: 'case-id' });
}}
>
{'submit'}
</button>
{children}
</>
);
},
};
});
jest.mock('../create/form', () => {
return {
CreateCaseForm: () => {
return <>{'form'}</>;
},
};
});
jest.mock('../create/submit_button', () => {
return {
SubmitCaseButton: () => {
return <>{'Submit'}</>;
},
};
});
const onCloseFlyout = jest.fn();
const onSuccess = jest.fn();
const defaultProps = {
onCloseFlyout,
onSuccess,
};
describe('CreateCaseFlyout', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('renders', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseFlyout {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy();
});
it('Closing modal calls onCloseCaseModal', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseFlyout {...defaultProps} />
</TestProviders>
);
wrapper.find('.euiFlyout__closeButton').first().simulate('click');
expect(onCloseFlyout).toBeCalled();
});
it('pass the correct props to FormContext component', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseFlyout {...defaultProps} />
</TestProviders>
);
const props = wrapper.find('FormContext').props();
expect(props).toEqual(
expect.objectContaining({
onSuccess,
})
);
});
it('onSuccess called when creating a case', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseFlyout {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click');
expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' });
});
});

View file

@ -1,71 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo } from 'react';
import styled from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import { FormContext } from '../create/form_context';
import { CreateCaseForm } from '../create/form';
import { SubmitCaseButton } from '../create/submit_button';
import { Case } from '../../containers/types';
import * as i18n from '../../common/translations';
export interface CreateCaseModalProps {
onCloseFlyout: () => void;
onSuccess: (theCase: Case) => Promise<void>;
afterCaseCreated?: (theCase: Case) => Promise<void>;
}
const Container = styled.div`
${({ theme }) => `
margin-top: ${theme.eui.euiSize};
text-align: right;
`}
`;
const StyledFlyout = styled(EuiFlyout)`
${({ theme }) => `
z-index: ${theme.eui.euiZModal};
`}
`;
// Adding bottom padding because timeline's
// bottom bar gonna hide the submit button.
const FormWrapper = styled.div`
padding-bottom: 50px;
`;
const CreateCaseFlyoutComponent: React.FC<CreateCaseModalProps> = ({
onSuccess,
afterCaseCreated,
onCloseFlyout,
}) => {
return (
<StyledFlyout onClose={onCloseFlyout} data-test-subj="create-case-flyout">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>{i18n.CREATE_TITLE}</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<FormWrapper>
<FormContext onSuccess={onSuccess} afterCaseCreated={afterCaseCreated}>
<CreateCaseForm withSteps={false} />
<Container>
<SubmitCaseButton />
</Container>
</FormContext>
</FormWrapper>
</EuiFlyoutBody>
</StyledFlyout>
);
};
export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent);
CreateCaseFlyout.displayName = 'CreateCaseFlyout';

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { mount } from 'enzyme';
import { act, waitFor } from '@testing-library/react';
import { useForm, Form, FormHook } from '../../common/shared_imports';
import { useGetTags } from '../../containers/use_get_tags';
import { useConnectors } from '../../containers/configure/use_connectors';
import { connectorsMock } from '../../containers/mock';
import { schema, FormProps } from './schema';
import { CreateCaseForm } from './form';
jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/configure/use_connectors');
const useGetTagsMock = useGetTags as jest.Mock;
const useConnectorsMock = useConnectors as jest.Mock;
const initialCaseValue: FormProps = {
description: '',
tags: [],
title: '',
connectorId: 'none',
fields: null,
syncAlerts: true,
};
describe('CreateCaseForm', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<FormProps>({
defaultValue: initialCaseValue,
options: { stripEmptyFields: false },
schema,
});
globalForm = form;
return <Form form={form}>{children}</Form>;
};
beforeEach(() => {
jest.resetAllMocks();
useGetTagsMock.mockReturnValue({ tags: ['test'] });
useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock });
});
it('it renders with steps', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeTruthy();
});
it('it renders without steps', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm withSteps={false} />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy();
});
it('it renders all form fields', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
});
it('should render spinner when loading', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm />
</MockHookWrapperComponent>
);
await act(async () => {
globalForm.setFieldValue('title', 'title');
globalForm.setFieldValue('description', 'description');
globalForm.submit();
// For some weird reason this is needed to pass the test.
// It does not do anything useful
await wrapper.find(`[data-test-subj="caseTitle"]`);
await wrapper.update();
await waitFor(() => {
expect(
wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()
).toBeTruthy();
});
});
});
});

Some files were not shown because too many files have changed in this diff Show more