[ResponseOps][Stack Connectors] Opsgenie connector UI (#142411)

* Starting opsgenie backend

* Adding more integration tests

* Updating readme

* Starting ui

* Adding hash and alias

* Fixing tests

* Switch to platinum for now

* Adding server side translations

* Fixing merge issues

* Fixing file location error

* Working ui

* Default alias is working

* Almost working validation fails sometimes

* Adding end to end tests

* Adding more tests

* Adding note and description fields

* Removing todo

* Fixing test errors

* Addressing feedback

* Trying to fix test flakiness
This commit is contained in:
Jonathan Buttner 2022-10-18 15:54:37 -04:00 committed by GitHub
parent 66041ca2c2
commit e71a522567
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1586 additions and 103 deletions

View file

@ -13,3 +13,5 @@ export enum AdditionalEmailServices {
}
export const INTERNAL_BASE_STACK_CONNECTORS_API_PATH = '/internal/stack_connectors';
export { OpsgenieSubActions, OpsgenieConnectorTypeId } from './opsgenie';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export enum OpsgenieSubActions {
CreateAlert = 'createAlert',
CloseAlert = 'closeAlert',
}
export const OpsgenieConnectorTypeId = '.opsgenie';

View file

@ -16,6 +16,7 @@ import {
getSlackConnectorType,
getTeamsConnectorType,
getWebhookConnectorType,
getOpsgenieConnectorType,
getXmattersConnectorType,
} from './stack';
@ -56,5 +57,6 @@ export function registerConnectorTypes({
connectorTypeRegistry.register(getServiceNowSIRConnectorType());
connectorTypeRegistry.register(getJiraConnectorType());
connectorTypeRegistry.register(getResilientConnectorType());
connectorTypeRegistry.register(getOpsgenieConnectorType());
connectorTypeRegistry.register(getTeamsConnectorType());
}

View file

@ -13,4 +13,5 @@ export { getServiceNowITOMConnectorType } from './servicenow_itom';
export { getSlackConnectorType } from './slack';
export { getTeamsConnectorType } from './teams';
export { getWebhookConnectorType } from './webhook';
export { getOpsgenieConnectorType } from './opsgenie';
export { getXmattersConnectorType } from './xmatters';

View file

@ -0,0 +1,115 @@
/*
* 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 OpsgenieConnectorFields from './connector';
import { ConnectorFormTestProvider } from '../../lib/test_utils';
import { act, screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana');
const actionConnector = {
actionTypeId: '.opsgenie',
name: 'opsgenie',
config: {
apiUrl: 'https://test.com',
},
secrets: {
apiKey: 'secret',
},
isDeprecated: false,
};
describe('OpsgenieConnectorFields renders', () => {
const onSubmit = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the fields', async () => {
render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<OpsgenieConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
expect(screen.getByTestId('config.apiUrl-input')).toBeInTheDocument();
expect(screen.getByTestId('secrets.apiKey-input')).toBeInTheDocument();
});
describe('Validation', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const tests: Array<[string, string]> = [
['config.apiUrl-input', 'not-valid'],
['secrets.apiKey-input', ''],
];
it('connector validation succeeds when connector config is valid', async () => {
const { getByTestId } = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<OpsgenieConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await act(async () => {
userEvent.click(getByTestId('form-test-provide-submit'));
});
expect(onSubmit).toBeCalledWith({
data: {
actionTypeId: '.opsgenie',
name: 'opsgenie',
config: {
apiUrl: 'https://test.com',
},
secrets: {
apiKey: 'secret',
},
isDeprecated: false,
},
isValid: true,
});
});
it.each(tests)('validates correctly %p', async (field, value) => {
const res = render(
<ConnectorFormTestProvider connector={actionConnector} onSubmit={onSubmit}>
<OpsgenieConnectorFields
readOnly={false}
isEdit={false}
registerPreSubmitValidator={() => {}}
/>
</ConnectorFormTestProvider>
);
await act(async () => {
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
delay: 10,
});
});
await act(async () => {
userEvent.click(res.getByTestId('form-test-provide-submit'));
});
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
});
});
});

View file

@ -0,0 +1,37 @@
/*
* 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 type {
ActionConnectorFieldsProps,
ConfigFieldSchema,
SecretsFieldSchema,
} from '@kbn/triggers-actions-ui-plugin/public';
import { SimpleConnectorForm } from '@kbn/triggers-actions-ui-plugin/public';
import * as i18n from './translations';
const configFormSchema: ConfigFieldSchema[] = [
{ id: 'apiUrl', label: i18n.API_URL_LABEL, isUrlField: true },
];
const secretsFormSchema: SecretsFieldSchema[] = [
{ id: 'apiKey', label: i18n.API_KEY_LABEL, isPasswordField: true },
];
const OpsgenieConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
return (
<SimpleConnectorForm
isEdit={isEdit}
readOnly={readOnly}
configFormSchema={configFormSchema}
secretsFormSchema={secretsFormSchema}
/>
);
};
// eslint-disable-next-line import/no-default-export
export { OpsgenieConnectorFields as default };

View file

@ -0,0 +1,10 @@
/*
* 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 { AlertProvidedActionVariables } from '@kbn/triggers-actions-ui-plugin/public';
export const DEFAULT_ALIAS = `{{${AlertProvidedActionVariables.ruleId}}}:{{${AlertProvidedActionVariables.alertId}}}`;

View file

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

View file

@ -0,0 +1,15 @@
/*
* 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 { EuiIcon } from '@elastic/eui';
import React from 'react';
import { LogoProps } from '../../types';
const Logo = (props: LogoProps) => <EuiIcon type="casesApp" size="xl" />;
// eslint-disable-next-line import/no-default-export
export { Logo as default };

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
import { registerConnectorTypes } from '../..';
import type { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
import { registrationServicesMock } from '../../../mocks';
import { OpsgenieConnectorTypeId, OpsgenieSubActions } from '../../../../common';
let connectorTypeModel: ConnectorTypeModel;
beforeAll(() => {
const connectorTypeRegistry = new TypeRegistry<ConnectorTypeModel>();
registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock });
const getResult = connectorTypeRegistry.get(OpsgenieConnectorTypeId);
if (getResult !== null) {
connectorTypeModel = getResult;
}
});
describe('connectorTypeRegistry.get() works', () => {
it('sets the id field in the connector type static data to the correct opsgenie value', () => {
expect(connectorTypeModel.id).toEqual(OpsgenieConnectorTypeId);
});
});
describe('opsgenie action params validation', () => {
it('results in no errors when the action params are valid for creating an alert', async () => {
const actionParams = {
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
message: 'hello',
},
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'subActionParams.message': [],
'subActionParams.alias': [],
},
});
});
it('results in no errors when the action params are valid for closing an alert', async () => {
const actionParams = {
subAction: OpsgenieSubActions.CloseAlert,
subActionParams: {
alias: '123',
},
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'subActionParams.message': [],
'subActionParams.alias': [],
},
});
});
it('sets the message error when the message is missing for creating an alert', async () => {
const actionParams = {
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {},
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'subActionParams.message': ['Message is required.'],
'subActionParams.alias': [],
},
});
});
it('sets the alias error when the alias is missing for closing an alert', async () => {
const actionParams = {
subAction: OpsgenieSubActions.CloseAlert,
subActionParams: {},
};
expect(await connectorTypeModel.validateParams(actionParams)).toEqual({
errors: {
'subActionParams.message': [],
'subActionParams.alias': ['Alias is required.'],
},
});
});
});

View file

@ -0,0 +1,88 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
ActionTypeModel as ConnectorTypeModel,
GenericValidationResult,
} from '@kbn/triggers-actions-ui-plugin/public';
import { RecursivePartial } from '@elastic/eui';
import { OpsgenieSubActions } from '../../../../common';
import type {
OpsgenieActionConfig,
OpsgenieActionParams,
OpsgenieActionSecrets,
} from '../../../../server/connector_types/stack';
import { DEFAULT_ALIAS } from './constants';
const SELECT_MESSAGE = i18n.translate(
'xpack.stackConnectors.components.opsgenie.selectMessageText',
{
defaultMessage: 'Create or close an alert in Opsgenie.',
}
);
const TITLE = i18n.translate('xpack.stackConnectors.components.opsgenie.connectorTypeTitle', {
defaultMessage: 'Opsgenie',
});
export const getConnectorType = (): ConnectorTypeModel<
OpsgenieActionConfig,
OpsgenieActionSecrets,
OpsgenieActionParams
> => {
return {
id: '.opsgenie',
iconClass: lazy(() => import('./logo')),
selectMessage: SELECT_MESSAGE,
actionTypeTitle: TITLE,
validateParams: async (
actionParams: RecursivePartial<OpsgenieActionParams>
): Promise<GenericValidationResult<unknown>> => {
const translations = await import('./translations');
const errors = {
'subActionParams.message': new Array<string>(),
'subActionParams.alias': new Array<string>(),
};
const validationResult = {
errors,
};
if (
actionParams.subAction === OpsgenieSubActions.CreateAlert &&
!actionParams?.subActionParams?.message?.length
) {
errors['subActionParams.message'].push(translations.MESSAGE_IS_REQUIRED);
}
if (
actionParams.subAction === OpsgenieSubActions.CloseAlert &&
!actionParams?.subActionParams?.alias?.length
) {
errors['subActionParams.alias'].push(translations.ALIAS_IS_REQUIRED);
}
return validationResult;
},
actionConnectorFields: lazy(() => import('./connector')),
actionParamsFields: lazy(() => import('./params')),
defaultActionParams: {
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: {
alias: DEFAULT_ALIAS,
},
},
defaultRecoveredActionParams: {
subAction: OpsgenieSubActions.CloseAlert,
subActionParams: {
alias: DEFAULT_ALIAS,
},
},
};
};

View file

@ -0,0 +1,247 @@
/*
* 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 { act, screen, render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import OpsgenieParamFields from './params';
import { OpsgenieSubActions } from '../../../../common';
import { OpsgenieActionParams } from '../../../../server/connector_types/stack';
describe('OpsgenieParamFields', () => {
const editAction = jest.fn();
const createAlertActionParams: OpsgenieActionParams = {
subAction: OpsgenieSubActions.CreateAlert,
subActionParams: { message: 'hello', alias: '123' },
};
const closeAlertActionParams: OpsgenieActionParams = {
subAction: OpsgenieSubActions.CloseAlert,
subActionParams: { alias: '456' },
};
const connector = {
secrets: { apiKey: '123' },
config: { apiUrl: 'http://test.com' },
id: 'test',
actionTypeId: '.test',
name: 'Test',
isPreconfigured: false,
isDeprecated: false,
};
const defaultCreateAlertProps = {
actionParams: createAlertActionParams,
errors: {
'subActionParams.message': [],
'subActionParams.alias': [],
},
editAction,
index: 0,
messageVariables: [],
actionConnector: connector,
};
const defaultCloseAlertProps = {
actionParams: closeAlertActionParams,
errors: {
'subActionParams.message': [],
'subActionParams.alias': [],
},
editAction,
index: 0,
messageVariables: [],
actionConnector: connector,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the create alert component', async () => {
render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
expect(screen.getByText('Message')).toBeInTheDocument();
expect(screen.getByText('Alias')).toBeInTheDocument();
expect(screen.getByTestId('opsgenie-subActionSelect'));
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
expect(screen.getByDisplayValue('123')).toBeInTheDocument();
});
it('renders the close alert component', async () => {
render(<OpsgenieParamFields {...defaultCloseAlertProps} />);
expect(screen.queryByText('Message')).not.toBeInTheDocument();
expect(screen.getByText('Alias')).toBeInTheDocument();
expect(screen.getByTestId('opsgenie-subActionSelect'));
expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument();
expect(screen.queryByDisplayValue('123')).not.toBeInTheDocument();
expect(screen.getByDisplayValue('456')).toBeInTheDocument();
});
it('calls editAction when the message field is changed', async () => {
render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'a new message' } });
expect(editAction).toBeCalledTimes(1);
expect(editAction.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"subActionParams",
Object {
"alias": "123",
"message": "a new message",
},
0,
]
`);
});
it('calls editAction when the description field is changed', async () => {
render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
fireEvent.change(screen.getByTestId('descriptionTextArea'), {
target: { value: 'a new description' },
});
expect(editAction).toBeCalledTimes(1);
expect(editAction.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"subActionParams",
Object {
"alias": "123",
"description": "a new description",
"message": "hello",
},
0,
]
`);
});
it('calls editAction when the alias field is changed for closeAlert', async () => {
render(<OpsgenieParamFields {...defaultCloseAlertProps} />);
fireEvent.change(screen.getByDisplayValue('456'), { target: { value: 'a new alias' } });
expect(editAction).toBeCalledTimes(1);
expect(editAction.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"subActionParams",
Object {
"alias": "a new alias",
},
0,
]
`);
});
it('does not render the create or close alert components if the subAction is undefined', async () => {
render(<OpsgenieParamFields {...{ ...defaultCreateAlertProps, actionParams: {} }} />);
expect(screen.queryByTestId('opsgenie-alias-row')).not.toBeInTheDocument();
expect(screen.queryByText('Message')).not.toBeInTheDocument();
});
it('preserves the previous alias value when switching between the create and close alert event actions', async () => {
const { rerender } = render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
expect(screen.getByDisplayValue('123')).toBeInTheDocument();
fireEvent.change(screen.getByDisplayValue('123'), { target: { value: 'a new alias' } });
expect(editAction).toBeCalledTimes(1);
rerender(
<OpsgenieParamFields
{...{
...defaultCloseAlertProps,
actionParams: {
...defaultCloseAlertProps.actionParams,
subActionParams: {
alias: 'a new alias',
},
},
}}
/>
);
expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument();
expect(editAction).toBeCalledTimes(2);
expect(editAction.mock.calls[1]).toMatchInlineSnapshot(`
Array [
"subActionParams",
Object {
"alias": "a new alias",
},
0,
]
`);
});
it('only preserves the previous alias value when switching between the create and close alert event actions', async () => {
const { rerender } = render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
expect(screen.getByDisplayValue('123')).toBeInTheDocument();
fireEvent.change(screen.getByDisplayValue('123'), { target: { value: 'a new alias' } });
expect(editAction).toBeCalledTimes(1);
rerender(
<OpsgenieParamFields
{...{
...defaultCloseAlertProps,
actionParams: {
...defaultCloseAlertProps.actionParams,
subActionParams: {
message: 'hello',
alias: 'a new alias',
},
},
}}
/>
);
expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument();
expect(editAction).toBeCalledTimes(2);
expect(editAction.mock.calls[1]).toMatchInlineSnapshot(`
Array [
"subActionParams",
Object {
"alias": "a new alias",
},
0,
]
`);
});
it('calls editAction when changing the subAction', async () => {
render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
act(() =>
userEvent.selectOptions(
screen.getByTestId('opsgenie-subActionSelect'),
screen.getByText('Close Alert')
)
);
expect(editAction).toBeCalledTimes(1);
expect(editAction.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"subAction",
"closeAlert",
0,
]
`);
});
});

View file

@ -0,0 +1,219 @@
/*
* 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, useRef } from 'react';
import {
ActionParamsProps,
TextAreaWithMessageVariables,
TextFieldWithMessageVariables,
} from '@kbn/triggers-actions-ui-plugin/public';
import { EuiFormRow, EuiSelect, RecursivePartial } from '@elastic/eui';
import { OpsgenieSubActions } from '../../../../common';
import type {
OpsgenieActionParams,
OpsgenieCloseAlertParams,
OpsgenieCreateAlertParams,
} from '../../../../server/connector_types/stack';
import * as i18n from './translations';
type SubActionProps<SubActionParams> = Omit<
ActionParamsProps<OpsgenieActionParams>,
'actionParams' | 'editAction'
> & {
subActionParams?: RecursivePartial<SubActionParams>;
editSubAction: ActionParamsProps<OpsgenieActionParams>['editAction'];
};
const CreateAlertComponent: React.FC<SubActionProps<OpsgenieCreateAlertParams>> = ({
editSubAction,
errors,
index,
messageVariables,
subActionParams,
}) => {
const isMessageInvalid =
errors['subActionParams.message'] !== undefined &&
errors['subActionParams.message'].length > 0 &&
subActionParams?.message !== undefined;
return (
<>
<EuiFormRow
data-test-subj="opsgenie-message-row"
fullWidth
error={errors['subActionParams.message']}
label={i18n.MESSAGE_FIELD_LABEL}
isInvalid={isMessageInvalid}
>
<TextFieldWithMessageVariables
index={index}
editAction={editSubAction}
messageVariables={messageVariables}
paramsProperty={'message'}
inputTargetValue={subActionParams?.message}
errors={errors['subActionParams.message'] as string[]}
/>
</EuiFormRow>
<TextAreaWithMessageVariables
index={index}
editAction={editSubAction}
messageVariables={messageVariables}
paramsProperty={'description'}
inputTargetValue={subActionParams?.description}
label={i18n.DESCRIPTION_FIELD_LABEL}
/>
<EuiFormRow data-test-subj="opsgenie-alias-row" fullWidth label={i18n.ALIAS_FIELD_LABEL}>
<TextFieldWithMessageVariables
index={index}
editAction={editSubAction}
messageVariables={messageVariables}
paramsProperty={'alias'}
inputTargetValue={subActionParams?.alias}
/>
</EuiFormRow>
</>
);
};
CreateAlertComponent.displayName = 'CreateAlertComponent';
const CloseAlertComponent: React.FC<SubActionProps<OpsgenieCloseAlertParams>> = ({
editSubAction,
errors,
index,
messageVariables,
subActionParams,
}) => {
const isAliasInvalid =
errors['subActionParams.alias'] !== undefined &&
errors['subActionParams.alias'].length > 0 &&
subActionParams?.alias !== undefined;
return (
<>
<EuiFormRow
data-test-subj="opsgenie-alias-row"
fullWidth
error={errors['subActionParams.alias']}
isInvalid={isAliasInvalid}
label={i18n.ALIAS_FIELD_LABEL}
>
<TextFieldWithMessageVariables
index={index}
editAction={editSubAction}
messageVariables={messageVariables}
paramsProperty={'alias'}
inputTargetValue={subActionParams?.alias}
errors={errors['subActionParams.alias'] as string[]}
/>
</EuiFormRow>
<TextAreaWithMessageVariables
index={index}
editAction={editSubAction}
messageVariables={messageVariables}
paramsProperty={'note'}
inputTargetValue={subActionParams?.note}
label={i18n.NOTE_FIELD_LABEL}
/>
</>
);
};
CloseAlertComponent.displayName = 'CloseAlertComponent';
const actionOptions = [
{
value: OpsgenieSubActions.CreateAlert,
text: i18n.CREATE_ALERT_ACTION,
},
{
value: OpsgenieSubActions.CloseAlert,
text: i18n.CLOSE_ALERT_ACTION,
},
];
const OpsgenieParamFields: React.FC<ActionParamsProps<OpsgenieActionParams>> = ({
actionParams,
editAction,
errors,
index,
messageVariables,
}) => {
const { subAction, subActionParams } = actionParams;
const currentSubAction = useRef<string>(subAction ?? OpsgenieSubActions.CreateAlert);
const onActionChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
editAction('subAction', event.target.value, index);
},
[editAction, index]
);
const editSubAction = useCallback(
(key, value) => {
editAction('subActionParams', { ...subActionParams, [key]: value }, index);
},
[editAction, index, subActionParams]
);
useEffect(() => {
if (!subAction) {
editAction('subAction', OpsgenieSubActions.CreateAlert, index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [index, subAction]);
useEffect(() => {
if (subAction != null && currentSubAction.current !== subAction) {
currentSubAction.current = subAction;
const params = subActionParams?.alias ? { alias: subActionParams.alias } : undefined;
editAction('subActionParams', params, index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subAction, currentSubAction]);
return (
<>
<EuiFormRow fullWidth label={i18n.ACTION_LABEL}>
<EuiSelect
fullWidth
data-test-subj="opsgenie-subActionSelect"
options={actionOptions}
hasNoInitialSelection={subAction == null}
value={subAction}
onChange={onActionChange}
/>
</EuiFormRow>
{subAction != null && subAction === OpsgenieSubActions.CreateAlert && (
<CreateAlertComponent
subActionParams={subActionParams}
editSubAction={editSubAction}
errors={errors}
index={index}
messageVariables={messageVariables}
/>
)}
{subAction != null && subAction === OpsgenieSubActions.CloseAlert && (
<CloseAlertComponent
subActionParams={subActionParams}
editSubAction={editSubAction}
errors={errors}
index={index}
messageVariables={messageVariables}
/>
)}
</>
);
};
OpsgenieParamFields.displayName = 'OpsgenieParamFields';
// eslint-disable-next-line import/no-default-export
export { OpsgenieParamFields as default };

View file

@ -0,0 +1,85 @@
/*
* 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 API_URL_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.apiUrlTextFieldLabel',
{
defaultMessage: 'URL',
}
);
export const API_KEY_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.apiKeySecret',
{
defaultMessage: 'API Key',
}
);
export const ACTION_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.actionLabel',
{
defaultMessage: 'Action',
}
);
export const CREATE_ALERT_ACTION = i18n.translate(
'xpack.stackConnectors.components.opsgenie.createAlertAction',
{
defaultMessage: 'Create Alert',
}
);
export const CLOSE_ALERT_ACTION = i18n.translate(
'xpack.stackConnectors.components.opsgenie.closeAlertAction',
{
defaultMessage: 'Close Alert',
}
);
export const MESSAGE_FIELD_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.messageLabel',
{
defaultMessage: 'Message',
}
);
export const NOTE_FIELD_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.noteLabel',
{
defaultMessage: 'Note (optional)',
}
);
export const DESCRIPTION_FIELD_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.descriptionLabel',
{
defaultMessage: 'Description (optional)',
}
);
export const MESSAGE_IS_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.opsgenie.requiredMessageTextField',
{
defaultMessage: 'Message is required.',
}
);
export const ALIAS_FIELD_LABEL = i18n.translate(
'xpack.stackConnectors.components.opsgenie.aliasLabel',
{
defaultMessage: 'Alias',
}
);
export const ALIAS_IS_REQUIRED = i18n.translate(
'xpack.stackConnectors.components.opsgenie.requiredAliasTextField',
{
defaultMessage: 'Alias is required.',
}
);

View file

@ -0,0 +1,17 @@
/*
* 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 { UserConfiguredActionConnector } from '@kbn/triggers-actions-ui-plugin/public/types';
import type {
OpsgenieActionConfig,
OpsgenieActionSecrets,
} from '../../../../server/connector_types/stack';
export type OpsgenieActionConnector = UserConfiguredActionConnector<
OpsgenieActionConfig,
OpsgenieActionSecrets
>;

View file

@ -45,7 +45,6 @@ export {
SlackConnectorTypeId,
TeamsConnectorTypeId,
WebhookConnectorTypeId,
OpsgenieConnectorTypeId,
XmattersConnectorTypeId,
} from './stack';
export type {

View file

@ -49,7 +49,16 @@ export {
} from './webhook';
export type { ActionParamsType as WebhookActionParams } from './webhook';
export { getOpsgenieConnectorType, OpsgenieConnectorTypeId } from './opsgenie';
export { getOpsgenieConnectorType } from './opsgenie';
export type {
OpsgenieActionConfig,
OpsgenieActionSecrets,
OpsgenieActionParams,
OpsgenieCloseAlertSubActionParams,
OpsgenieCreateAlertSubActionParams,
OpsgenieCloseAlertParams,
OpsgenieCreateAlertParams,
} from './opsgenie';
export {
getConnectorType as getXmattersConnectorType,

View file

@ -12,7 +12,7 @@ import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.moc
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { MockedLogger } from '@kbn/logging-mocks';
import { OpsgenieConnectorTypeId } from '.';
import { OpsgenieConnectorTypeId } from '../../../../common';
import { OpsgenieConnector } from './connector';
import * as utils from '@kbn/actions-plugin/server/lib/axios_utils';

View file

@ -8,6 +8,7 @@
import crypto from 'crypto';
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
import { AxiosError } from 'axios';
import { OpsgenieSubActions } from '../../../../common';
import { CloseAlertParamsSchema, CreateAlertParamsSchema, Response } from './schema';
import { CloseAlertParams, Config, CreateAlertParams, Secrets } from './types';
import * as i18n from './translations';
@ -25,13 +26,13 @@ export class OpsgenieConnector extends SubActionConnector<Config, Secrets> {
this.registerSubAction({
method: this.createAlert.name,
name: 'createAlert',
name: OpsgenieSubActions.CreateAlert,
schema: CreateAlertParamsSchema,
});
this.registerSubAction({
method: this.closeAlert.name,
name: 'closeAlert',
name: OpsgenieSubActions.CloseAlert,
schema: CloseAlertParamsSchema,
});
}

View file

@ -15,13 +15,12 @@ import {
SubActionConnectorType,
ValidatorType,
} from '@kbn/actions-plugin/server/sub_action_framework/types';
import { OpsgenieConnectorTypeId } from '../../../../common';
import { OpsgenieConnector } from './connector';
import { ConfigSchema, SecretsSchema } from './schema';
import { Config, Secrets } from './types';
import * as i18n from './translations';
export const OpsgenieConnectorTypeId = '.opsgenie';
export const getOpsgenieConnectorType = (): SubActionConnectorType<Config, Secrets> => {
return {
Service: OpsgenieConnector,
@ -37,3 +36,13 @@ export const getOpsgenieConnectorType = (): SubActionConnectorType<Config, Secre
],
};
};
export type {
Config as OpsgenieActionConfig,
Secrets as OpsgenieActionSecrets,
Params as OpsgenieActionParams,
CreateAlertSubActionParams as OpsgenieCreateAlertSubActionParams,
CloseAlertSubActionParams as OpsgenieCloseAlertSubActionParams,
CreateAlertParams as OpsgenieCreateAlertParams,
CloseAlertParams as OpsgenieCloseAlertParams,
} from './types';

View file

@ -11,9 +11,22 @@ import {
CreateAlertParamsSchema,
SecretsSchema,
} from './schema';
import { OpsgenieSubActions } from '../../../../common';
export type Config = TypeOf<typeof ConfigSchema>;
export type Secrets = TypeOf<typeof SecretsSchema>;
export type CreateAlertParams = TypeOf<typeof CreateAlertParamsSchema>;
export type CloseAlertParams = TypeOf<typeof CloseAlertParamsSchema>;
export interface CreateAlertSubActionParams {
subAction: OpsgenieSubActions.CreateAlert;
subActionParams: CreateAlertParams;
}
export interface CloseAlertSubActionParams {
subAction: OpsgenieSubActions.CloseAlert;
subActionParams: CloseAlertParams;
}
export type Params = CreateAlertSubActionParams | CloseAlertSubActionParams;

View file

@ -13,7 +13,7 @@ import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { IconType, EuiFlyoutSize } from '@elastic/eui';
import type { IconType, EuiFlyoutSize, RecursivePartial } from '@elastic/eui';
import { EuiDataGridColumn, EuiDataGridControlColumn, EuiDataGridSorting } from '@elastic/eui';
import {
ActionType,
@ -204,8 +204,8 @@ export interface ActionTypeModel<ActionConfig = any, ActionSecrets = any, Action
ComponentType<ActionConnectorFieldsProps>
> | null;
actionParamsFields: React.LazyExoticComponent<ComponentType<ActionParamsProps<ActionParams>>>;
defaultActionParams?: Partial<ActionParams>;
defaultRecoveredActionParams?: Partial<ActionParams>;
defaultActionParams?: RecursivePartial<ActionParams>;
defaultRecoveredActionParams?: RecursivePartial<ActionParams>;
customConnectorSelectItem?: CustomConnectorSelectionItem;
isExperimental?: boolean;
}

View file

@ -0,0 +1,29 @@
/*
* 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 { ProvidedType } from '@kbn/test';
import { FtrProviderContext } from '../../ftr_provider_context';
export type ActionsCommon = ProvidedType<typeof ActionsCommonServiceProvider>;
export function ActionsCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
async openNewConnectorForm(name: string) {
const createBtn = await testSubjects.find('createActionButton');
const createBtnIsVisible = await createBtn.isDisplayed();
if (createBtnIsVisible) {
await createBtn.click();
} else {
await testSubjects.click('createFirstActionButton');
}
await testSubjects.click(`.${name}-card`);
},
};
}

View file

@ -0,0 +1,19 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
import { ActionsCommonServiceProvider } from './common';
import { ActionsOpsgenieServiceProvider } from './opsgenie';
export function ActionsServiceProvider(context: FtrProviderContext) {
const common = ActionsCommonServiceProvider(context);
return {
opsgenie: ActionsOpsgenieServiceProvider(context, common),
common: ActionsCommonServiceProvider(context),
};
}

View file

@ -0,0 +1,48 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import type { ActionsCommon } from './common';
export interface ConnectorFormFields {
name: string;
apiUrl: string;
apiKey: string;
}
export function ActionsOpsgenieServiceProvider(
{ getService, getPageObject }: FtrProviderContext,
common: ActionsCommon
) {
const testSubjects = getService('testSubjects');
return {
async createNewConnector(fields: ConnectorFormFields) {
await common.openNewConnectorForm('opsgenie');
await this.setConnectorFields(fields);
const flyOutSaveButton = await testSubjects.find('create-connector-flyout-save-btn');
expect(await flyOutSaveButton.isEnabled()).to.be(true);
await flyOutSaveButton.click();
},
async setConnectorFields({ name, apiUrl, apiKey }: ConnectorFormFields) {
await testSubjects.setValue('nameInput', name);
await testSubjects.setValue('config.apiUrl-input', apiUrl);
await testSubjects.setValue('secrets.apiKey-input', apiKey);
},
async updateConnectorFields(fields: ConnectorFormFields) {
await this.setConnectorFields(fields);
const editFlyOutSaveButton = await testSubjects.find('edit-connector-flyout-save-btn');
expect(await editFlyOutSaveButton.isEnabled()).to.be(true);
await editFlyOutSaveButton.click();
},
};
}

View file

@ -70,6 +70,8 @@ import { SearchSessionsService } from './search_sessions';
import { ObservabilityProvider } from './observability';
// import { CompareImagesProvider } from './compare_images';
import { CasesServiceProvider } from './cases';
import { ActionsServiceProvider } from './actions';
import { RulesServiceProvider } from './rules';
import { AiopsProvider } from './aiops';
// define the name and providers for services that should be
@ -130,6 +132,8 @@ export const services = {
searchSessions: SearchSessionsService,
observability: ObservabilityProvider,
// compareImages: CompareImagesProvider,
actions: ActionsServiceProvider,
rules: RulesServiceProvider,
cases: CasesServiceProvider,
aiops: AiopsProvider,
};

View file

@ -0,0 +1,83 @@
/*
* 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 expect from '@kbn/expect';
import { ProvidedType } from '@kbn/test';
import { FtrProviderContext } from '../../ftr_provider_context';
export type RulesCommon = ProvidedType<typeof RulesCommonServiceProvider>;
export function RulesCommonServiceProvider({ getService, getPageObject }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
const find = getService('find');
const retry = getService('retry');
const browser = getService('browser');
return {
async clickCreateAlertButton() {
const createBtn = await find.byCssSelector(
'[data-test-subj="createRuleButton"],[data-test-subj="createFirstRuleButton"]'
);
await createBtn.click();
},
async cancelRuleCreation() {
await testSubjects.click('cancelSaveRuleButton');
await testSubjects.existOrFail('confirmRuleCloseModal');
await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton');
await testSubjects.missingOrFail('confirmRuleCloseModal');
},
async setNotifyThrottleInput(value: string = '10') {
await testSubjects.click('notifyWhenSelect');
await testSubjects.click('onThrottleInterval');
await testSubjects.setValue('throttleInput', value);
},
async defineIndexThresholdAlert(alertName: string) {
await browser.refresh();
await this.clickCreateAlertButton();
await testSubjects.scrollIntoView('ruleNameInput');
await testSubjects.setValue('ruleNameInput', alertName);
await testSubjects.click(`.index-threshold-SelectOption`);
await testSubjects.scrollIntoView('selectIndexExpression');
await testSubjects.click('selectIndexExpression');
const indexComboBox = await find.byCssSelector('#indexSelectSearchBox');
await indexComboBox.click();
await indexComboBox.type('k');
const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`);
await filterSelectItem.click();
await testSubjects.click('thresholdAlertTimeFieldSelect');
await retry.try(async () => {
const fieldOptions = await find.allByCssSelector('#thresholdTimeField option');
expect(fieldOptions[1]).not.to.be(undefined);
await fieldOptions[1].click();
});
await testSubjects.click('closePopover');
// need this two out of popup clicks to close them
const nameInput = await testSubjects.find('ruleNameInput');
await nameInput.click();
await testSubjects.click('whenExpression');
await testSubjects.click('whenExpressionSelect');
await retry.try(async () => {
const aggTypeOptions = await find.allByCssSelector('#aggTypeField option');
expect(aggTypeOptions[1]).not.to.be(undefined);
await aggTypeOptions[1].click();
});
await testSubjects.click('ofExpressionPopover');
const ofComboBox = await find.byCssSelector('#ofField');
await ofComboBox.click();
const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox');
const ofOptions = ofOptionsString.trim().split('\n');
expect(ofOptions.length > 0).to.be(true);
await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]);
},
};
}

View file

@ -0,0 +1,15 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
import { RulesCommonServiceProvider } from './common';
export function RulesServiceProvider(context: FtrProviderContext) {
return {
common: RulesCommonServiceProvider(context),
};
}

View file

@ -17,8 +17,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const find = getService('find');
const retry = getService('retry');
const comboBox = getService('comboBox');
const browser = getService('browser');
const rules = getService('rules');
async function getAlertsByName(name: string) {
const {
@ -62,44 +62,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await nameInput.click();
}
async function defineIndexThresholdAlert(alertName: string) {
await pageObjects.triggersActionsUI.clickCreateAlertButton();
await testSubjects.setValue('ruleNameInput', alertName);
await testSubjects.click(`.index-threshold-SelectOption`);
await testSubjects.click('selectIndexExpression');
const indexComboBox = await find.byCssSelector('#indexSelectSearchBox');
await indexComboBox.click();
await indexComboBox.type('k');
const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`);
await filterSelectItem.click();
await testSubjects.click('thresholdAlertTimeFieldSelect');
await retry.try(async () => {
const fieldOptions = await find.allByCssSelector('#thresholdTimeField option');
expect(fieldOptions[1]).not.to.be(undefined);
await fieldOptions[1].click();
});
await testSubjects.click('closePopover');
// need this two out of popup clicks to close them
const nameInput = await testSubjects.find('ruleNameInput');
await nameInput.click();
await testSubjects.click('whenExpression');
await testSubjects.click('whenExpressionSelect');
await retry.try(async () => {
const aggTypeOptions = await find.allByCssSelector('#aggTypeField option');
expect(aggTypeOptions[1]).not.to.be(undefined);
await aggTypeOptions[1].click();
});
await testSubjects.click('ofExpressionPopover');
const ofComboBox = await find.byCssSelector('#ofField');
await ofComboBox.click();
const ofOptionsString = await comboBox.getOptionsList('availablefieldsOptionsComboBox');
const ofOptions = ofOptionsString.trim().split('\n');
expect(ofOptions.length > 0).to.be(true);
await comboBox.set('availablefieldsOptionsComboBox', ofOptions[0]);
}
async function defineAlwaysFiringAlert(alertName: string) {
await pageObjects.triggersActionsUI.clickCreateAlertButton();
await testSubjects.setValue('ruleNameInput', alertName);
@ -107,10 +69,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
}
async function discardNewRuleCreation() {
await testSubjects.click('cancelSaveRuleButton');
await testSubjects.existOrFail('confirmRuleCloseModal');
await testSubjects.click('confirmRuleCloseModal > confirmModalConfirmButton');
await testSubjects.missingOrFail('confirmRuleCloseModal');
await rules.common.cancelRuleCreation();
}
describe('create alert', function () {
@ -128,7 +87,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('should create an alert', async () => {
const alertName = generateUniqueKey();
await defineIndexThresholdAlert(alertName);
await rules.common.defineIndexThresholdAlert(alertName);
await testSubjects.click('notifyWhenSelect');
await testSubjects.click('onThrottleInterval');

View file

@ -6,29 +6,31 @@
*/
import expect from '@kbn/expect';
import { findIndex } from 'lodash';
import { FtrProviderContext } from '../../ftr_provider_context';
import { ObjectRemover } from '../../lib/object_remover';
import { generateUniqueKey, getTestActionData } from '../../lib/get_test_data';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { ObjectRemover } from '../../../lib/object_remover';
import { generateUniqueKey } from '../../../lib/get_test_data';
import {
getConnectorByName,
createSlackConnectorAndObjectRemover,
createSlackConnector,
} from './utils';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
export default ({ getPageObjects, getPageObject, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
const find = getService('find');
const retry = getService('retry');
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
let objectRemover: ObjectRemover;
const browser = getService('browser');
describe('Connectors', function () {
describe('General connector functionality', function () {
before(async () => {
const { body: createdAction } = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send(getTestActionData())
.expect(200);
objectRemover = await createSlackConnectorAndObjectRemover({ getService });
});
beforeEach(async () => {
await pageObjects.common.navigateToApp('triggersActionsConnectors');
objectRemover.add(createdAction.id, 'action', 'actions');
});
after(async () => {
@ -66,14 +68,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
actionType: 'Slack',
},
]);
const connector = await getConnector(connectorName);
const connector = await getConnectorByName(connectorName, supertest);
objectRemover.add(connector.id, 'action', 'actions');
});
it('should edit a connector', async () => {
const connectorName = generateUniqueKey();
const updatedConnectorName = `${connectorName}updated`;
const createdAction = await createConnector(connectorName);
const createdAction = await createSlackConnector({
name: connectorName,
supertest,
});
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
@ -169,7 +174,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('should reset connector when canceling an edit', async () => {
const connectorName = generateUniqueKey();
const createdAction = await createConnector(connectorName);
const createdAction = await createSlackConnector({
name: connectorName,
supertest,
});
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
@ -197,8 +205,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('should delete a connector', async () => {
const connectorName = generateUniqueKey();
await createConnector(connectorName);
const createdAction = await createConnector(generateUniqueKey());
await createSlackConnector({ name: connectorName, supertest });
const createdAction = await createSlackConnector({
name: generateUniqueKey(),
supertest,
});
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
@ -223,8 +234,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('should bulk delete connectors', async () => {
const connectorName = generateUniqueKey();
await createConnector(connectorName);
const createdAction = await createConnector(generateUniqueKey());
await createSlackConnector({ name: connectorName, supertest });
const createdAction = await createSlackConnector({
name: generateUniqueKey(),
supertest,
});
objectRemover.add(createdAction.id, 'action', 'actions');
await browser.refresh();
@ -279,22 +293,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
async function createConnector(connectorName: string) {
const { body: createdAction } = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name: connectorName,
config: {},
secrets: {
webhookUrl: 'https://test.com',
},
connector_type_id: '.slack',
})
.expect(200);
return createdAction;
}
async function createIndexConnector(connectorName: string, indexName: string) {
const { body: createdAction } = await supertest
.post(`/api/actions/connector`)
@ -311,13 +309,4 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
.expect(200);
return createdAction;
}
async function getConnector(name: string) {
const { body } = await supertest
.get(`/api/actions/connectors`)
.set('kbn-xsrf', 'foo')
.expect(200);
const i = findIndex(body, (c: any) => c.name === name);
return body[i];
}
};

View file

@ -0,0 +1,15 @@
/*
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => {
describe('Connectors', function () {
loadTestFile(require.resolve('./general'));
loadTestFile(require.resolve('./opsgenie'));
});
};

View file

@ -0,0 +1,254 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { ObjectRemover } from '../../../lib/object_remover';
import { generateUniqueKey } from '../../../lib/get_test_data';
import { createConnector, createSlackConnectorAndObjectRemover, getConnectorByName } from './utils';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
const find = getService('find');
const retry = getService('retry');
const supertest = getService('supertest');
const actions = getService('actions');
const rules = getService('rules');
const browser = getService('browser');
let objectRemover: ObjectRemover;
describe('Opsgenie', () => {
before(async () => {
objectRemover = await createSlackConnectorAndObjectRemover({ getService });
});
after(async () => {
await objectRemover.removeAll();
});
describe('connector page', () => {
beforeEach(async () => {
await pageObjects.common.navigateToApp('triggersActionsConnectors');
});
it('should create the connector', async () => {
const connectorName = generateUniqueKey();
await actions.opsgenie.createNewConnector({
name: connectorName,
apiUrl: 'https://test.com',
apiKey: 'apiKey',
});
const toastTitle = await pageObjects.common.closeToast();
expect(toastTitle).to.eql(`Created '${connectorName}'`);
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
const searchResults = await pageObjects.triggersActionsUI.getConnectorsList();
expect(searchResults).to.eql([
{
name: connectorName,
actionType: 'Opsgenie',
},
]);
const connector = await getConnectorByName(connectorName, supertest);
objectRemover.add(connector.id, 'action', 'actions');
});
it('should edit the connector', async () => {
const connectorName = generateUniqueKey();
const updatedConnectorName = `${connectorName}updated`;
const createdAction = await createOpsgenieConnector(connectorName);
objectRemover.add(createdAction.id, 'action', 'actions');
browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList();
expect(searchResultsBeforeEdit.length).to.eql(1);
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
await actions.opsgenie.updateConnectorFields({
name: updatedConnectorName,
apiUrl: 'https://test.com',
apiKey: 'apiKey',
});
const toastTitle = await pageObjects.common.closeToast();
expect(toastTitle).to.eql(`Updated '${updatedConnectorName}'`);
await testSubjects.click('euiFlyoutCloseButton');
await pageObjects.triggersActionsUI.searchConnectors(updatedConnectorName);
const searchResultsAfterEdit = await pageObjects.triggersActionsUI.getConnectorsList();
expect(searchResultsAfterEdit).to.eql([
{
name: updatedConnectorName,
actionType: 'Opsgenie',
},
]);
});
it('should reset connector when canceling an edit', async () => {
const connectorName = generateUniqueKey();
const createdAction = await createOpsgenieConnector(connectorName);
objectRemover.add(createdAction.id, 'action', 'actions');
browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList();
expect(searchResultsBeforeEdit.length).to.eql(1);
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
await testSubjects.setValue('nameInput', 'some test name to cancel');
await testSubjects.click('edit-connector-flyout-close-btn');
await testSubjects.click('confirmModalConfirmButton');
await find.waitForDeletedByCssSelector(
'[data-test-subj="edit-connector-flyout-close-btn"]'
);
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
expect(await testSubjects.getAttribute('nameInput', 'value')).to.eql(connectorName);
await testSubjects.click('euiFlyoutCloseButton');
});
it('should disable the run button when the message field is not filled', async () => {
const connectorName = generateUniqueKey();
const createdAction = await createOpsgenieConnector(connectorName);
objectRemover.add(createdAction.id, 'action', 'actions');
browser.refresh();
await pageObjects.triggersActionsUI.searchConnectors(connectorName);
const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList();
expect(searchResultsBeforeEdit.length).to.eql(1);
await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button');
await find.clickByCssSelector('[data-test-subj="testConnectorTab"]');
expect(await (await testSubjects.find('executeActionButton')).isEnabled()).to.be(false);
});
});
describe('alerts page', () => {
const defaultAlias = '{{rule.id}}:{{alert.id}}';
const connectorName = generateUniqueKey();
before(async () => {
const createdAction = await createOpsgenieConnector(connectorName);
objectRemover.add(createdAction.id, 'action', 'actions');
await pageObjects.common.navigateToApp('triggersActions');
});
beforeEach(async () => {
await setupRule();
await selectOpsgenieConnectorInRuleAction(connectorName);
});
afterEach(async () => {
await rules.common.cancelRuleCreation();
});
it('should default to the create alert action', async () => {
expect(await testSubjects.getAttribute('opsgenie-subActionSelect', 'value')).to.eql(
'createAlert'
);
expect(await testSubjects.getAttribute('aliasInput', 'value')).to.eql(defaultAlias);
});
it('should default to the close alert action when setting the run when to recovered', async () => {
await testSubjects.click('addNewActionConnectorActionGroup-0');
await testSubjects.click('addNewActionConnectorActionGroup-0-option-recovered');
expect(await testSubjects.getAttribute('opsgenie-subActionSelect', 'value')).to.eql(
'closeAlert'
);
expect(await testSubjects.getAttribute('aliasInput', 'value')).to.eql(defaultAlias);
});
it('should preserve the alias when switching between create and close alert actions', async () => {
await testSubjects.setValue('aliasInput', 'new alias');
await testSubjects.selectValue('opsgenie-subActionSelect', 'closeAlert');
expect(await testSubjects.getAttribute('opsgenie-subActionSelect', 'value')).to.be(
'closeAlert'
);
expect(await testSubjects.getAttribute('aliasInput', 'value')).to.be('new alias');
});
it('should not preserve the message when switching to close alert and back to create alert', async () => {
await testSubjects.setValue('messageInput', 'a message');
await testSubjects.selectValue('opsgenie-subActionSelect', 'closeAlert');
await testSubjects.missingOrFail('messageInput');
await retry.waitFor('message input to be displayed', async () => {
await testSubjects.selectValue('opsgenie-subActionSelect', 'createAlert');
return await testSubjects.exists('messageInput');
});
expect(await testSubjects.getAttribute('messageInput', 'value')).to.be('');
});
it('should not preserve the alias when switching run when to recover', async () => {
await testSubjects.setValue('aliasInput', 'an alias');
await testSubjects.click('addNewActionConnectorActionGroup-0');
await testSubjects.click('addNewActionConnectorActionGroup-0-option-recovered');
await testSubjects.missingOrFail('messageInput');
expect(await testSubjects.getAttribute('aliasInput', 'value')).to.be(defaultAlias);
});
it('should not preserve the alias when switching run when to threshold met', async () => {
await testSubjects.click('addNewActionConnectorActionGroup-0');
await testSubjects.click('addNewActionConnectorActionGroup-0-option-recovered');
await testSubjects.missingOrFail('messageInput');
await testSubjects.setValue('aliasInput', 'an alias');
await testSubjects.click('addNewActionConnectorActionGroup-0');
await testSubjects.click('addNewActionConnectorActionGroup-0-option-threshold met');
await testSubjects.exists('messageInput');
expect(await testSubjects.getAttribute('aliasInput', 'value')).to.be(defaultAlias);
});
});
const setupRule = async () => {
const alertName = generateUniqueKey();
await retry.try(async () => {
await rules.common.defineIndexThresholdAlert(alertName);
});
await rules.common.setNotifyThrottleInput();
};
const selectOpsgenieConnectorInRuleAction = async (name: string) => {
await testSubjects.click('.opsgenie-alerting-ActionTypeSelectOption');
await testSubjects.selectValue('comboBoxInput', name);
};
const createOpsgenieConnector = async (name: string) => {
return createConnector({
name,
config: { apiUrl: 'https//test.com' },
secrets: { apiKey: '1234' },
connectorTypeId: '.opsgenie',
supertest,
});
};
});
};

View file

@ -0,0 +1,88 @@
/*
* 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 type SuperTest from 'supertest';
import { findIndex } from 'lodash';
import { ObjectRemover } from '../../../lib/object_remover';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { getTestActionData } from '../../../lib/get_test_data';
export const createSlackConnectorAndObjectRemover = async ({
getService,
}: {
getService: FtrProviderContext['getService'];
}) => {
const supertest = getService('supertest');
const objectRemover = new ObjectRemover(supertest);
const testData = getTestActionData();
const createdAction = await createSlackConnector({
name: testData.name,
supertest,
});
objectRemover.add(createdAction.id, 'action', 'actions');
return objectRemover;
};
export const createSlackConnector = async ({
name,
supertest,
}: {
name: string;
supertest: SuperTest.SuperTest<SuperTest.Test>;
}) => {
const connector = await createConnector({
name,
config: {},
secrets: { webhookUrl: 'https://test.com' },
connectorTypeId: '.slack',
supertest,
});
return connector;
};
export const getConnectorByName = async (
name: string,
supertest: SuperTest.SuperTest<SuperTest.Test>
) => {
const { body } = await supertest
.get(`/api/actions/connectors`)
.set('kbn-xsrf', 'foo')
.expect(200);
const i = findIndex(body, (c: any) => c.name === name);
return body[i];
};
export const createConnector = async ({
name,
config,
secrets,
connectorTypeId,
supertest,
}: {
name: string;
config: Record<string, unknown>;
secrets: Record<string, unknown>;
connectorTypeId: string;
supertest: SuperTest.SuperTest<SuperTest.Test>;
}) => {
const { body: createdAction } = await supertest
.post(`/api/actions/connector`)
.set('kbn-xsrf', 'foo')
.send({
name,
config,
secrets,
connector_type_id: connectorTypeId,
})
.expect(200);
return createdAction;
};

View file

@ -13,6 +13,7 @@ import { pageObjects } from './page_objects';
// .server-log is specifically not enabled
const enabledActionTypes = [
'.opsgenie',
'.email',
'.index',
'.pagerduty',

View file

@ -18,6 +18,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
const find = getService('find');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const rules = getService('rules');
function getRowItemData(row: CustomCheerio, $: CustomCheerioStatic) {
return {
@ -164,10 +165,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
await switchBtn.click();
},
async clickCreateAlertButton() {
const createBtn = await find.byCssSelector(
'[data-test-subj="createRuleButton"],[data-test-subj="createFirstRuleButton"]'
);
await createBtn.click();
await rules.common.clickCreateAlertButton();
},
async setAlertName(value: string) {
await testSubjects.setValue('ruleNameInput', value);