mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.6`: - [Fixing delete action and empty message bugs (#145463)](https://github.com/elastic/kibana/pull/145463) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Jonathan Buttner","email":"56361221+jonathan-buttner@users.noreply.github.com"},"sourceCommit":{"committedDate":"2022-11-23T11:40:12Z","message":"Fixing delete action and empty message bugs (#145463)\n\nThis PR addresses two issues:\r\n\r\n1. This bug: https://github.com/elastic/kibana/issues/144128 when a user\r\ndeletes an action at say index 0 within the rule form , if the action at\r\nindex 1 was a different type (`createAlert` vs `closeAlert`) the user\r\nwould lose the information in the action.\r\n - Now the information from the action at index 1 should persistent\r\n2. A user could save the rule when the `message` field was a string of\r\nall spaces\r\n - Now the user should not be able to save the rule\r\n\r\nFixes: https://github.com/elastic/kibana/issues/144128\r\n\r\n### Deleting the action\r\n\r\n\r\n202298074
-34878a4c-5fa0-4d8d-8192-ae5f43439cd0.mov\r\n\r\n\r\n\r\n### Whitespace message field\r\n\r\n\r\n\r\nhttps://user-images.githubusercontent.com/56361221/202298085-95f1fe3f-2652-4b92-86c7-b49b0b23d913.mov","sha":"ba16f55c5e3357737ae7f4ffc5381988a1c31b8d","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Feature:Actions","Team:ResponseOps","v8.6.0","v8.7.0"],"number":145463,"url":"https://github.com/elastic/kibana/pull/145463","mergeCommit":{"message":"Fixing delete action and empty message bugs (#145463)\n\nThis PR addresses two issues:\r\n\r\n1. This bug: https://github.com/elastic/kibana/issues/144128 when a user\r\ndeletes an action at say index 0 within the rule form , if the action at\r\nindex 1 was a different type (`createAlert` vs `closeAlert`) the user\r\nwould lose the information in the action.\r\n - Now the information from the action at index 1 should persistent\r\n2. A user could save the rule when the `message` field was a string of\r\nall spaces\r\n - Now the user should not be able to save the rule\r\n\r\nFixes: https://github.com/elastic/kibana/issues/144128\r\n\r\n### Deleting the action\r\n\r\n\r\n202298074
-34878a4c-5fa0-4d8d-8192-ae5f43439cd0.mov\r\n\r\n\r\n\r\n### Whitespace message field\r\n\r\n\r\n\r\nhttps://user-images.githubusercontent.com/56361221/202298085-95f1fe3f-2652-4b92-86c7-b49b0b23d913.mov","sha":"ba16f55c5e3357737ae7f4ffc5381988a1c31b8d"}},"sourceBranch":"main","suggestedTargetBranches":["8.6"],"targetPullRequestStates":[{"branch":"8.6","label":"v8.6.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/145463","number":145463,"mergeCommit":{"message":"Fixing delete action and empty message bugs (#145463)\n\nThis PR addresses two issues:\r\n\r\n1. This bug: https://github.com/elastic/kibana/issues/144128 when a user\r\ndeletes an action at say index 0 within the rule form , if the action at\r\nindex 1 was a different type (`createAlert` vs `closeAlert`) the user\r\nwould lose the information in the action.\r\n - Now the information from the action at index 1 should persistent\r\n2. A user could save the rule when the `message` field was a string of\r\nall spaces\r\n - Now the user should not be able to save the rule\r\n\r\nFixes: https://github.com/elastic/kibana/issues/144128\r\n\r\n### Deleting the action\r\n\r\n\r\n202298074
-34878a4c-5fa0-4d8d-8192-ae5f43439cd0.mov\r\n\r\n\r\n\r\n### Whitespace message field\r\n\r\n\r\n\r\nhttps://user-images.githubusercontent.com/56361221/202298085-95f1fe3f-2652-4b92-86c7-b49b0b23d913.mov","sha":"ba16f55c5e3357737ae7f4ffc5381988a1c31b8d"}}]}] BACKPORT--> Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
This commit is contained in:
parent
411da5f0db
commit
d2bd1a4992
15 changed files with 486 additions and 213 deletions
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { OpsgenieCloseAlertExample } from '../../../../server/connector_types/stack/opsgenie/test_schema';
|
||||
import { isPartialCloseAlertSchema } from './close_alert_schema';
|
||||
|
||||
describe('close_alert_schema', () => {
|
||||
describe('isPartialCloseAlertSchema', () => {
|
||||
it('returns true with an empty object', () => {
|
||||
expect(isPartialCloseAlertSchema({})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false with undefined', () => {
|
||||
expect(isPartialCloseAlertSchema(undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns false with an invalid field', () => {
|
||||
expect(isPartialCloseAlertSchema({ invalidField: 'a' })).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns true with only the note field', () => {
|
||||
expect(isPartialCloseAlertSchema({ note: 'a' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true with the Opsgenie close alert example', () => {
|
||||
expect(isPartialCloseAlertSchema(OpsgenieCloseAlertExample)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
import { decodeSchema } from './schema_utils';
|
||||
|
||||
/**
|
||||
* This schema must match the CloseAlertParamsSchema in x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/schema.ts
|
||||
* except that it makes all fields partial.
|
||||
*/
|
||||
const CloseAlertSchema = rt.exact(
|
||||
rt.partial({
|
||||
alias: rt.string,
|
||||
user: rt.string,
|
||||
source: rt.string,
|
||||
note: rt.string,
|
||||
})
|
||||
);
|
||||
|
||||
type CloseAlertSchemaType = rt.TypeOf<typeof CloseAlertSchema>;
|
||||
|
||||
export const isPartialCloseAlertSchema = (data: unknown): data is CloseAlertSchemaType => {
|
||||
try {
|
||||
decodeSchema(CloseAlertSchema, data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -207,3 +207,5 @@ const CreateAlertComponent: React.FC<CreateAlertProps> = ({
|
|||
CreateAlertComponent.displayName = 'CreateAlert';
|
||||
|
||||
export const CreateAlert = React.memo(CreateAlertComponent);
|
||||
|
||||
export { isPartialCreateAlertSchema } from './schema';
|
||||
|
|
|
@ -11,7 +11,8 @@ import { JsonEditorWithMessageVariables } from '@kbn/triggers-actions-ui-plugin/
|
|||
import type { OpsgenieCreateAlertParams } from '../../../../../server/connector_types/stack';
|
||||
import * as i18n from './translations';
|
||||
import { CreateAlertProps } from '.';
|
||||
import { decodeCreateAlert, isDecodeError } from './schema';
|
||||
import { decodeCreateAlert } from './schema';
|
||||
import { isDecodeError } from '../schema_utils';
|
||||
|
||||
export type JsonEditorProps = Pick<
|
||||
CreateAlertProps,
|
||||
|
|
|
@ -5,104 +5,137 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { decodeCreateAlert } from './schema';
|
||||
import { decodeCreateAlert, isPartialCreateAlertSchema } from './schema';
|
||||
import {
|
||||
OpsgenieCreateAlertExample,
|
||||
ValidCreateAlertSchema,
|
||||
} from '../../../../../server/connector_types/stack/opsgenie/test_schema';
|
||||
|
||||
describe('decodeCreateAlert', () => {
|
||||
it('throws an error when the message field is not present', () => {
|
||||
expect(() => decodeCreateAlert({ alias: '123' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[message]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
describe('schema', () => {
|
||||
describe('decodeCreateAlert', () => {
|
||||
it('throws an error when the message field is not present', () => {
|
||||
expect(() => decodeCreateAlert({ alias: '123' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[message]: expected value of type [string] but got [undefined]"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when the message field is only spaces', () => {
|
||||
expect(() => decodeCreateAlert({ message: ' ' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[message]: must be populated with a value other than just whitespace"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when the message field is an empty string', () => {
|
||||
expect(() => decodeCreateAlert({ message: '' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[message]: must be populated with a value other than just whitespace"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in the data that are not defined in the schema', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({ invalidField: 'hi', message: 'hi' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in responders with name field than in the schema', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
responders: [{ name: 'sam', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in responders with id field than in the schema', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
responders: [{ id: 'id', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with name and type=team', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ name: 'sam', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with id and type=team', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ id: 'id', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with id and type=user', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ id: 'id', type: 'user', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with username and type=user', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ username: 'sam', type: 'user', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when details is a record of string to number', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
details: { id: 1 },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Invalid value \\"1\\" supplied to \\"details.id\\""`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['ValidCreateAlertSchema', ValidCreateAlertSchema],
|
||||
['OpsgenieCreateAlertExample', OpsgenieCreateAlertExample],
|
||||
])('validates the test object [%s] correctly', (objectName, testObject) => {
|
||||
expect(() => decodeCreateAlert(testObject)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error when the message field is only spaces', () => {
|
||||
expect(() => decodeCreateAlert({ message: ' ' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[message]: must be populated with a value other than just whitespace"`
|
||||
);
|
||||
});
|
||||
describe('isPartialCreateAlertSchema', () => {
|
||||
const { message, ...createAlertSchemaWithoutMessage } = ValidCreateAlertSchema;
|
||||
const { message: ignoreMessage2, ...opsgenieCreateAlertExampleWithoutMessage } =
|
||||
OpsgenieCreateAlertExample;
|
||||
|
||||
it('throws an error when the message field is an empty string', () => {
|
||||
expect(() => decodeCreateAlert({ message: '' })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[message]: must be populated with a value other than just whitespace"`
|
||||
);
|
||||
});
|
||||
it('returns true with an empty object', () => {
|
||||
expect(isPartialCreateAlertSchema({})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in the data that are not defined in the schema', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({ invalidField: 'hi', message: 'hi' })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
it('returns false with undefined', () => {
|
||||
expect(isPartialCreateAlertSchema(undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in responders with name field than in the schema', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
responders: [{ name: 'sam', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
it('returns true with only alias', () => {
|
||||
expect(isPartialCreateAlertSchema({ alias: 'abc' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in responders with id field than in the schema', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
responders: [{ id: 'id', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
it.each([
|
||||
['ValidCreateAlertSchema', ValidCreateAlertSchema],
|
||||
['OpsgenieCreateAlertExample', OpsgenieCreateAlertExample],
|
||||
['createAlertSchemaWithoutMessage', createAlertSchemaWithoutMessage],
|
||||
['opsgenieCreateAlertExampleWithoutMessage', opsgenieCreateAlertExampleWithoutMessage],
|
||||
])('returns true with the test object [%s]', (objectName, testObject) => {
|
||||
expect(isPartialCreateAlertSchema(testObject)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with name and type=team', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ name: 'sam', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with id and type=team', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ id: 'id', type: 'team', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with id and type=user', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ id: 'id', type: 'user', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when additional fields are present in visibleTo with username and type=user', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
visibleTo: [{ username: 'sam', type: 'user', invalidField: 'scott' }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"invalidField\\""`);
|
||||
});
|
||||
|
||||
it('throws an error when details is a record of string to number', () => {
|
||||
expect(() =>
|
||||
decodeCreateAlert({
|
||||
message: 'hi',
|
||||
details: { id: 1 },
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Invalid value \\"1\\" supplied to \\"details.id\\""`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['ValidCreateAlertSchema', ValidCreateAlertSchema],
|
||||
['OpsgenieCreateAlertExample', OpsgenieCreateAlertExample],
|
||||
])('validates the test object [%s] correctly', (objectName, testObject) => {
|
||||
expect(() => decodeCreateAlert(testObject)).not.toThrow();
|
||||
it('returns false with an additional property', () => {
|
||||
expect(isPartialCreateAlertSchema({ anInvalidField: 'a' })).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,12 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Either, fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
import * as rt from 'io-ts';
|
||||
import { exactCheck } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { isEmpty, isObject } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { decodeSchema } from '../schema_utils';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const MessageNonEmptyString = new rt.Type<string, string, unknown>(
|
||||
|
@ -37,6 +35,42 @@ const ResponderTypes = rt.union([
|
|||
rt.literal('schedule'),
|
||||
]);
|
||||
|
||||
const CreateAlertSchemaOptionalProps = rt.partial(
|
||||
rt.type({
|
||||
alias: rt.string,
|
||||
description: rt.string,
|
||||
responders: rt.array(
|
||||
rt.union([
|
||||
rt.strict({ name: rt.string, type: ResponderTypes }),
|
||||
rt.strict({ id: rt.string, type: ResponderTypes }),
|
||||
rt.strict({ username: rt.string, type: rt.literal('user') }),
|
||||
])
|
||||
),
|
||||
visibleTo: rt.array(
|
||||
rt.union([
|
||||
rt.strict({ name: rt.string, type: rt.literal('team') }),
|
||||
rt.strict({ id: rt.string, type: rt.literal('team') }),
|
||||
rt.strict({ id: rt.string, type: rt.literal('user') }),
|
||||
rt.strict({ username: rt.string, type: rt.literal('user') }),
|
||||
])
|
||||
),
|
||||
actions: rt.array(rt.string),
|
||||
tags: rt.array(rt.string),
|
||||
details: rt.record(rt.string, rt.string),
|
||||
entity: rt.string,
|
||||
source: rt.string,
|
||||
priority: rt.union([
|
||||
rt.literal('P1'),
|
||||
rt.literal('P2'),
|
||||
rt.literal('P3'),
|
||||
rt.literal('P4'),
|
||||
rt.literal('P5'),
|
||||
]),
|
||||
user: rt.string,
|
||||
note: rt.string,
|
||||
}).props
|
||||
);
|
||||
|
||||
/**
|
||||
* This schema is duplicated from the server. The only difference is that it is using io-ts vs kbn-schema.
|
||||
* NOTE: This schema must be the same as defined here: x-pack/plugins/stack_connectors/server/connector_types/stack/opsgenie/schema.ts
|
||||
|
@ -51,92 +85,38 @@ const ResponderTypes = rt.union([
|
|||
*/
|
||||
const CreateAlertSchema = rt.intersection([
|
||||
rt.strict({ message: MessageNonEmptyString }),
|
||||
rt.exact(
|
||||
rt.partial({
|
||||
alias: rt.string,
|
||||
description: rt.string,
|
||||
responders: rt.array(
|
||||
rt.union([
|
||||
rt.strict({ name: rt.string, type: ResponderTypes }),
|
||||
rt.strict({ id: rt.string, type: ResponderTypes }),
|
||||
rt.strict({ username: rt.string, type: rt.literal('user') }),
|
||||
])
|
||||
),
|
||||
visibleTo: rt.array(
|
||||
rt.union([
|
||||
rt.strict({ name: rt.string, type: rt.literal('team') }),
|
||||
rt.strict({ id: rt.string, type: rt.literal('team') }),
|
||||
rt.strict({ id: rt.string, type: rt.literal('user') }),
|
||||
rt.strict({ username: rt.string, type: rt.literal('user') }),
|
||||
])
|
||||
),
|
||||
actions: rt.array(rt.string),
|
||||
tags: rt.array(rt.string),
|
||||
details: rt.record(rt.string, rt.string),
|
||||
entity: rt.string,
|
||||
source: rt.string,
|
||||
priority: rt.union([
|
||||
rt.literal('P1'),
|
||||
rt.literal('P2'),
|
||||
rt.literal('P3'),
|
||||
rt.literal('P4'),
|
||||
rt.literal('P5'),
|
||||
]),
|
||||
user: rt.string,
|
||||
note: rt.string,
|
||||
})
|
||||
),
|
||||
rt.exact(CreateAlertSchemaOptionalProps),
|
||||
]);
|
||||
|
||||
export const formatErrors = (errors: rt.Errors): string[] => {
|
||||
const err = errors.map((error) => {
|
||||
if (error.message != null) {
|
||||
return error.message;
|
||||
} else {
|
||||
const keyContext = error.context
|
||||
.filter(
|
||||
(entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== ''
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.join('.');
|
||||
|
||||
const nameContext = error.context.find(
|
||||
(entry) => entry.type != null && entry.type.name != null && entry.type.name.length > 0
|
||||
);
|
||||
|
||||
const suppliedValue =
|
||||
keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : '';
|
||||
const value = isObject(error.value) ? JSON.stringify(error.value) : error.value;
|
||||
return `Invalid value "${value}" supplied to "${suppliedValue}"`;
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(err)];
|
||||
};
|
||||
|
||||
type CreateAlertSchemaType = rt.TypeOf<typeof CreateAlertSchema>;
|
||||
|
||||
export const decodeCreateAlert = (data: unknown): CreateAlertSchemaType => {
|
||||
const onLeft = (errors: rt.Errors) => {
|
||||
throw new DecodeError(formatErrors(errors));
|
||||
};
|
||||
/**
|
||||
* This schema should match CreateAlertSchema except that all fields are optional and message is only enforced as a string.
|
||||
* Enforcing message as only a string accommodates the following scenario:
|
||||
*
|
||||
* If a user deletes an action in the rule form at index 0, and the
|
||||
* action at index 1 had the message field specified with all spaces, the message field is technically invalid but
|
||||
* we want to allow it to pass the partial check so that the form is still populated with the invalid value. Otherwise the
|
||||
* forum will be reset and the user would lose the information (although it is invalid) they had entered
|
||||
*/
|
||||
const PartialCreateAlertSchema = rt.exact(
|
||||
rt.intersection([
|
||||
rt.partial(rt.type({ message: rt.string }).props),
|
||||
CreateAlertSchemaOptionalProps,
|
||||
])
|
||||
);
|
||||
|
||||
const onRight = (a: CreateAlertSchemaType): CreateAlertSchemaType => identity(a);
|
||||
type PartialCreateAlertSchemaType = rt.TypeOf<typeof PartialCreateAlertSchema>;
|
||||
|
||||
return pipe(
|
||||
CreateAlertSchema.decode(data),
|
||||
(decoded) => exactCheck(data, decoded),
|
||||
fold(onLeft, onRight)
|
||||
);
|
||||
export const isPartialCreateAlertSchema = (data: unknown): data is PartialCreateAlertSchemaType => {
|
||||
try {
|
||||
decodeSchema(PartialCreateAlertSchema, data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export class DecodeError extends Error {
|
||||
constructor(public readonly decodeErrors: string[]) {
|
||||
super(decodeErrors.join());
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDecodeError(error: unknown): error is DecodeError {
|
||||
return error instanceof DecodeError;
|
||||
}
|
||||
export const decodeCreateAlert = (data: unknown): CreateAlertSchemaType => {
|
||||
return decodeSchema(CreateAlertSchema, data);
|
||||
};
|
||||
|
|
|
@ -37,13 +37,6 @@ export const DESCRIPTION_FIELD_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const MESSAGE_FIELD_IS_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.components.opsgenie.messageFieldRequired',
|
||||
{
|
||||
defaultMessage: '"message" field must be populated with a value other than just whitespace',
|
||||
}
|
||||
);
|
||||
|
||||
export const USE_JSON_EDITOR_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.opsgenie.useJsonEditorLabel',
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
ActionTypeModel as ConnectorTypeModel,
|
||||
GenericValidationResult,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { RULE_TAGS_TEMPLATE } from '../../../../common/opsgenie';
|
||||
import { OpsgenieSubActions } from '../../../../common';
|
||||
import type {
|
||||
|
@ -74,13 +75,13 @@ const validateParams = async (
|
|||
errors,
|
||||
};
|
||||
|
||||
if (
|
||||
actionParams.subAction === OpsgenieSubActions.CreateAlert &&
|
||||
!actionParams?.subActionParams?.message?.length
|
||||
) {
|
||||
errors['subActionParams.message'].push(translations.MESSAGE_IS_REQUIRED);
|
||||
if (actionParams.subAction === OpsgenieSubActions.CreateAlert) {
|
||||
if (!actionParams?.subActionParams?.message?.length) {
|
||||
errors['subActionParams.message'].push(translations.MESSAGE_IS_REQUIRED);
|
||||
} else if (isEmpty(actionParams?.subActionParams?.message?.trim())) {
|
||||
errors['subActionParams.message'].push(translations.MESSAGE_NON_WHITESPACE);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
actionParams.subAction === OpsgenieSubActions.CloseAlert &&
|
||||
!actionParams?.subActionParams?.alias?.length
|
||||
|
|
|
@ -195,7 +195,7 @@ describe('OpsgenieParamFields', () => {
|
|||
expect(screen.queryByText('Message')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('preserves the previous alias value when switching between the create and close alert event actions', async () => {
|
||||
it('does not call edit action when a component rerenders with subActionParams that match the new subAction', async () => {
|
||||
const { rerender } = render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
|
||||
|
||||
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
|
||||
|
@ -220,17 +220,78 @@ describe('OpsgenieParamFields', () => {
|
|||
|
||||
expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument();
|
||||
|
||||
expect(editAction).toBeCalledTimes(2);
|
||||
expect(editAction).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(editAction.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"subActionParams",
|
||||
Object {
|
||||
"alias": "a new alias",
|
||||
},
|
||||
0,
|
||||
]
|
||||
`);
|
||||
it('calls editAction with only the alias when the component is rerendered with mismatched closeAlert and params', async () => {
|
||||
const { rerender } = render(<OpsgenieParamFields {...defaultCreateAlertProps} />);
|
||||
|
||||
expect(screen.getByDisplayValue('hello')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('123')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<OpsgenieParamFields
|
||||
{...{
|
||||
...defaultCloseAlertProps,
|
||||
actionParams: {
|
||||
...defaultCloseAlertProps.actionParams,
|
||||
subActionParams: {
|
||||
alias: 'a new alias',
|
||||
message: 'a message',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByDisplayValue('hello')).not.toBeInTheDocument();
|
||||
|
||||
expect(editAction).toBeCalledTimes(1);
|
||||
expect(editAction.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"subActionParams",
|
||||
Object {
|
||||
"alias": "a new alias",
|
||||
},
|
||||
0,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('calls editAction with only the alias when the component is rerendered with mismatched createAlert and params', async () => {
|
||||
const { rerender } = render(<OpsgenieParamFields {...defaultCloseAlertProps} />);
|
||||
|
||||
expect(screen.queryByText('Message')).not.toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('456')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<OpsgenieParamFields
|
||||
{...{
|
||||
...defaultCreateAlertProps,
|
||||
actionParams: {
|
||||
...defaultCreateAlertProps.actionParams,
|
||||
subActionParams: {
|
||||
message: 'a message',
|
||||
alias: 'a new alias',
|
||||
invalidField: 'a note',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByDisplayValue('456')).not.toBeInTheDocument();
|
||||
|
||||
expect(editAction).toBeCalledTimes(1);
|
||||
expect(editAction.mock.calls[0]).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 () => {
|
||||
|
@ -262,14 +323,14 @@ describe('OpsgenieParamFields', () => {
|
|||
expect(editAction).toBeCalledTimes(2);
|
||||
|
||||
expect(editAction.mock.calls[1]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"subActionParams",
|
||||
Object {
|
||||
"alias": "a new alias",
|
||||
},
|
||||
0,
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
"subActionParams",
|
||||
Object {
|
||||
"alias": "a new alias",
|
||||
},
|
||||
0,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('calls editAction when changing the subAction', async () => {
|
||||
|
|
|
@ -19,8 +19,9 @@ import type {
|
|||
OpsgenieCreateAlertSubActionParams,
|
||||
} from '../../../../server/connector_types/stack';
|
||||
import * as i18n from './translations';
|
||||
import { CreateAlert } from './create_alert';
|
||||
import { CreateAlert, isPartialCreateAlertSchema } from './create_alert';
|
||||
import { CloseAlert } from './close_alert';
|
||||
import { isPartialCloseAlertSchema } from './close_alert_schema';
|
||||
|
||||
const actionOptions = [
|
||||
{
|
||||
|
@ -85,11 +86,20 @@ const OpsgenieParamFields: React.FC<ActionParamsProps<OpsgenieActionParams>> = (
|
|||
useEffect(() => {
|
||||
if (subAction != null && currentSubAction.current !== subAction) {
|
||||
currentSubAction.current = subAction;
|
||||
const params = subActionParams?.alias ? { alias: subActionParams.alias } : undefined;
|
||||
editAction('subActionParams', params, index);
|
||||
|
||||
// check for a mismatch in the subAction and params, if the subAction does not match the params then we need to
|
||||
// clear them by calling editAction. We can carry over the alias if it exists
|
||||
if (
|
||||
(subAction === OpsgenieSubActions.CreateAlert &&
|
||||
!isPartialCreateAlertSchema(subActionParams)) ||
|
||||
(subAction === OpsgenieSubActions.CloseAlert && !isPartialCloseAlertSchema(subActionParams))
|
||||
) {
|
||||
const params = subActionParams?.alias ? { alias: subActionParams.alias } : undefined;
|
||||
editAction('subActionParams', params, index);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [subAction, currentSubAction, subActionParams?.alias, index]);
|
||||
}, [subAction, currentSubAction, index, subActionParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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 * as rt from 'io-ts';
|
||||
import { DecodeError, decodeSchema } from './schema_utils';
|
||||
|
||||
describe('schema_utils', () => {
|
||||
describe('decodeSchema', () => {
|
||||
const testSchema = rt.strict({ stringField: rt.string });
|
||||
|
||||
it('throws an error when stringField is not present', () => {
|
||||
expect(() => decodeSchema(testSchema, { a: 1 })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid value \\"undefined\\" supplied to \\"stringField\\""`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when stringField is present but excess properties are also present', () => {
|
||||
expect(() =>
|
||||
decodeSchema(testSchema, { stringField: 'abc', a: 1 })
|
||||
).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"a\\""`);
|
||||
});
|
||||
|
||||
it('does not throw an error when the data matches the schema', () => {
|
||||
expect(() => decodeSchema(testSchema, { stringField: 'abc' })).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws a DecodeError instance', () => {
|
||||
expect(() => decodeSchema(testSchema, { a: 1 })).toThrowError(
|
||||
new DecodeError([`Invalid value \"undefined\" supplied to \"stringField\"`])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { fold } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import * as rt from 'io-ts';
|
||||
import { exactCheck } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { isObject } from 'lodash';
|
||||
|
||||
const formatErrors = (errors: rt.Errors): string[] => {
|
||||
const err = errors.map((error) => {
|
||||
if (error.message != null) {
|
||||
return error.message;
|
||||
} else {
|
||||
const keyContext = error.context
|
||||
.filter(
|
||||
(entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== ''
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.join('.');
|
||||
|
||||
const nameContext = error.context.find(
|
||||
(entry) => entry.type != null && entry.type.name != null && entry.type.name.length > 0
|
||||
);
|
||||
|
||||
const suppliedValue =
|
||||
keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : '';
|
||||
const value = isObject(error.value) ? JSON.stringify(error.value) : error.value;
|
||||
return `Invalid value "${value}" supplied to "${suppliedValue}"`;
|
||||
}
|
||||
});
|
||||
|
||||
return [...new Set(err)];
|
||||
};
|
||||
|
||||
export const decodeSchema = <T>(schema: rt.Type<T>, data: unknown): T => {
|
||||
const onLeft = (errors: rt.Errors) => {
|
||||
throw new DecodeError(formatErrors(errors));
|
||||
};
|
||||
|
||||
const onRight = (schemaType: T): T => identity(schemaType);
|
||||
|
||||
return pipe(schema.decode(data), (decoded) => exactCheck(data, decoded), fold(onLeft, onRight));
|
||||
};
|
||||
|
||||
export class DecodeError extends Error {
|
||||
constructor(public readonly decodeErrors: string[]) {
|
||||
super(decodeErrors.join());
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDecodeError(error: unknown): error is DecodeError {
|
||||
return error instanceof DecodeError;
|
||||
}
|
|
@ -28,6 +28,11 @@ export const MESSAGE_IS_REQUIRED = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const MESSAGE_NON_WHITESPACE = i18n.translate(
|
||||
'xpack.stackConnectors.components.opsgenie.messageNotWhitespaceForm',
|
||||
{ defaultMessage: 'Message must be populated with a value other than just whitespace' }
|
||||
);
|
||||
|
||||
export const ACTION_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.opsgenie.actionLabel',
|
||||
{
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CreateAlertParamsSchema } from './schema';
|
||||
import { OpsgenieCreateAlertExample, ValidCreateAlertSchema } from './test_schema';
|
||||
import { CloseAlertParamsSchema, CreateAlertParamsSchema } from './schema';
|
||||
import {
|
||||
OpsgenieCloseAlertExample,
|
||||
OpsgenieCreateAlertExample,
|
||||
ValidCreateAlertSchema,
|
||||
} from './test_schema';
|
||||
|
||||
describe('opsgenie schema', () => {
|
||||
describe('CreateAlertParamsSchema', () => {
|
||||
|
@ -17,4 +21,13 @@ describe('opsgenie schema', () => {
|
|||
expect(() => CreateAlertParamsSchema.validate(testObject)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CloseAlertParamsSchema', () => {
|
||||
it.each([['OpsgenieCloseAlertExample', OpsgenieCloseAlertExample]])(
|
||||
'validates the test object [%s] correctly',
|
||||
(objectName, testObject) => {
|
||||
expect(() => CloseAlertParamsSchema.validate(testObject)).not.toThrow();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CreateAlertParams } from './types';
|
||||
import { CloseAlertParams, CreateAlertParams } from './types';
|
||||
|
||||
export const ValidCreateAlertSchema: CreateAlertParams = {
|
||||
message: 'a message',
|
||||
|
@ -94,3 +94,14 @@ export const OpsgenieCreateAlertExample: CreateAlertParams = {
|
|||
entity: 'An example entity',
|
||||
priority: 'P1',
|
||||
};
|
||||
|
||||
/**
|
||||
* This example is pulled from the sample curl request here: https://docs.opsgenie.com/docs/alert-api#close-alert
|
||||
* with the addition of the alias field.
|
||||
*/
|
||||
export const OpsgenieCloseAlertExample: CloseAlertParams = {
|
||||
alias: '123',
|
||||
user: 'Monitoring Script',
|
||||
source: 'AWS Lambda',
|
||||
note: 'Action executed via Alert API',
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue