mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Revert "[Security Solution] [Cases] Move create page components and dependencies to Cases (#94444)" (#94975)
This reverts commit c497239d55
.
This commit is contained in:
parent
d1040f0105
commit
5f4939be76
362 changed files with 327 additions and 17518 deletions
|
@ -108,4 +108,3 @@ pageLoadAssetSize:
|
|||
fileUpload: 25664
|
||||
banners: 17946
|
||||
mapsEms: 26072
|
||||
cases: 102558
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
export * from './cases';
|
||||
export * from './connectors';
|
||||
export * from './helpers';
|
||||
export * from './runtime_types';
|
||||
export * from './saved_object';
|
||||
export * from './user';
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
|
@ -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';
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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?'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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';
|
|
@ -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, '');
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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;
|
|
@ -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(),
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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 }',
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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 };
|
|
@ -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);
|
|
@ -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);
|
|
@ -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')),
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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';
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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(),
|
||||
};
|
||||
};
|
|
@ -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 });
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
|
@ -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',
|
||||
});
|
|
@ -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[];
|
|
@ -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: {} });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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: [] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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: [] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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' },
|
||||
];
|
|
@ -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 });
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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' });
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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,
|
||||
};
|
|
@ -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',
|
||||
});
|
|
@ -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;
|
|
@ -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: [] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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: [] });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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);
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 }));
|
|
@ -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,
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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[]>;
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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' });
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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' });
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue