Skip to content

Commit

Permalink
[Alerting] fixes buggy default message behaviour (elastic#84202)
Browse files Browse the repository at this point in the history
This PR addresses some weird UX we've identified with default values in Action Params components and their inferred defaults when placed inside of an Alerts flyout.
Key changes:

1. Typing of these components has been corrected to reflect that we expect these parameters to only be _partial_, as the form is used to set these values (for example, the `message` field of the Server Log action, might or might not be set, so it should be nullable, but in the typing we treated it as the _final_ valid state, which is message not being nullable).
2. When a default message is set by the params components, the are tracked against the value of the default, which means that if the default changes, then so will the value in the field. Custom values provided by the user will not be overridden when the default changes. This has to be handled by the component itself at the moment (hopefully in the future we can make this a concern of the flyout and not each component).
3. The concept of the "Recovered" action group has been removed from these components - that's an Alerting concern, not actions, and shouldn't appear in the action components' code.
# Conflicts:
#	x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.test.tsx
  • Loading branch information
gmmorris committed Nov 30, 2020
1 parent 9bc8d30 commit c1e885b
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,97 @@ describe('EmailParamsFields renders', () => {
expect(wrapper.find('[data-test-subj="subjectInput"]').length > 0).toBeTruthy();
expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy();
});

test('message param field is rendered with default value if not set', () => {
const actionParams = {
cc: [],
bcc: [],
to: ['test@test.com'],
subject: 'test',
};

const editAction = jest.fn();
mountWithIntl(
<EmailParamsFields
actionParams={actionParams}
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
editAction={editAction}
defaultMessage={'Some default message'}
index={0}
/>
);

expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0);
});

test('when the default message changes, so is the underlying message if it was set by the previous default', () => {
const actionParams = {
cc: [],
bcc: [],
to: ['test@test.com'],
subject: 'test',
};

const editAction = jest.fn();
const wrapper = mountWithIntl(
<EmailParamsFields
actionParams={actionParams}
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
editAction={editAction}
defaultMessage={'Some default message'}
index={0}
/>
);

expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0);

wrapper.setProps({
defaultMessage: 'Some different default message',
});

expect(editAction).toHaveBeenCalledWith('message', 'Some different default message', 0);
});

test('when the default message changes, it doesnt change the underlying message if it wasnt set by a previous default', () => {
const actionParams = {
cc: [],
bcc: [],
to: ['test@test.com'],
subject: 'test',
};

const editAction = jest.fn();
const wrapper = mountWithIntl(
<EmailParamsFields
actionParams={actionParams}
errors={{ to: [], cc: [], bcc: [], subject: [], message: [] }}
editAction={editAction}
defaultMessage={'Some default message'}
index={0}
/>
);

expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0);

// simulate value being updated
const valueToSimulate = 'some new value';
wrapper
.find('[data-test-subj="messageTextArea"]')
.first()
.simulate('change', { target: { value: valueToSimulate } });
expect(editAction).toHaveBeenCalledWith('message', valueToSimulate, 0);
wrapper.setProps({
actionParams: {
...actionParams,
message: valueToSimulate,
},
});

// simulate default changing
wrapper.setProps({
defaultMessage: 'Some different default message',
});

expect(editAction).not.toHaveBeenCalledWith('message', 'Some different default message', 0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ActionParamsProps } from '../../../../types';
import { EmailActionParams } from '../types';
import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables';
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
import { resolvedActionGroupMessage } from '../../../constants';

export const EmailParamsFields = ({
actionParams,
Expand All @@ -28,17 +27,19 @@ export const EmailParamsFields = ({
const [addCC, setAddCC] = useState<boolean>(false);
const [addBCC, setAddBCC] = useState<boolean>(false);

const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState<
[boolean, string | undefined]
>([false, defaultMessage]);
useEffect(() => {
if (defaultMessage === resolvedActionGroupMessage) {
editAction('message', defaultMessage, index);
} else if (
(!message || message === resolvedActionGroupMessage) &&
defaultMessage &&
defaultMessage.length > 0
if (
!actionParams?.message ||
(isUsingDefault &&
actionParams?.message === defaultMessageUsed &&
defaultMessageUsed !== defaultMessage)
) {
setDefaultMessageUsage([true, defaultMessage]);
editAction('message', defaultMessage, index);
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultMessage]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useGetSingleIssue } from './use_get_single_issue';
import * as i18n from './translations';

interface Props {
selectedValue: string | null;
selectedValue?: string | null;
http: HttpSetup;
toastNotifications: Pick<
ToastsApi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface Props {
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
issueType: string;
issueType: string | undefined;
actionConnector?: ActionConnector;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Props {
ToastsApi,
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
>;
id: string | null;
id?: string | null;
actionConnector?: ActionConnector;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('ServerLogParamsFields renders', () => {
const editAction = jest.fn();

test('all params fields is rendered', () => {
const editAction = jest.fn();
const actionParams = {
level: ServerLogLevelOptions.TRACE,
message: 'test',
Expand Down Expand Up @@ -42,23 +43,106 @@ describe('ServerLogParamsFields renders', () => {
test('level param field is rendered with default value if not selected', () => {
const actionParams = {
message: 'test message',
level: ServerLogLevelOptions.INFO,
};
const editAction = jest.fn();

mountWithIntl(
<ServerLogParamsFields
actionParams={actionParams}
errors={{ message: [] }}
editAction={editAction}
index={0}
/>
);

expect(editAction).toHaveBeenCalledWith('level', 'info', 0);
});

test('message param field is rendered with default value if not set', () => {
const actionParams = {
level: ServerLogLevelOptions.TRACE,
};

const editAction = jest.fn();

mountWithIntl(
<ServerLogParamsFields
actionParams={actionParams}
defaultMessage={'Some default message'}
errors={{ message: [] }}
editAction={editAction}
index={0}
/>
);

expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0);
});

test('when the default message changes, so is the underlying message if it was set by the previous default', () => {
const actionParams = {
level: ServerLogLevelOptions.TRACE,
};

const editAction = jest.fn();
const wrapper = mountWithIntl(
<ServerLogParamsFields
actionParams={actionParams}
defaultMessage={'Some default message'}
errors={{ message: [] }}
editAction={() => {}}
editAction={editAction}
index={0}
docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart}
toastNotifications={mocks.notifications.toasts}
http={mocks.http}
/>
);
expect(wrapper.find('[data-test-subj="loggingLevelSelect"]').length > 0).toBeTruthy();
expect(
wrapper.find('[data-test-subj="loggingLevelSelect"]').first().prop('value')
).toStrictEqual('info');
expect(wrapper.find('[data-test-subj="messageTextArea"]').length > 0).toBeTruthy();

expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0);

wrapper.setProps({
defaultMessage: 'Some different default message',
});

expect(editAction).toHaveBeenCalledWith('message', 'Some different default message', 0);
});

test('when the default message changes, it doesnt change the underlying message if it wasnt set by a previous default', () => {
const actionParams = {
level: ServerLogLevelOptions.TRACE,
};

const editAction = jest.fn();
const wrapper = mountWithIntl(
<ServerLogParamsFields
actionParams={actionParams}
defaultMessage={'Some default message'}
errors={{ message: [] }}
editAction={editAction}
index={0}
/>
);

expect(editAction).toHaveBeenCalledWith('message', 'Some default message', 0);

// simulate value being updated
const valueToSimulate = 'some new value';
wrapper
.find('[data-test-subj="messageTextArea"]')
.first()
.simulate('change', { target: { value: valueToSimulate } });
expect(editAction).toHaveBeenCalledWith('message', valueToSimulate, 0);
wrapper.setProps({
actionParams: {
...actionParams,
message: valueToSimulate,
},
});

// simulate default changing
wrapper.setProps({
defaultMessage: 'Some different default message',
});

expect(editAction).not.toHaveBeenCalledWith('message', 'Some different default message', 0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, useEffect } from 'react';
import React, { Fragment, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSelect, EuiFormRow } from '@elastic/eui';
import { ActionParamsProps } from '../../../../types';
import { ServerLogActionParams } from '.././types';
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
import { resolvedActionGroupMessage } from '../../../constants';

export const ServerLogParamsFields: React.FunctionComponent<
ActionParamsProps<ServerLogActionParams>
Expand All @@ -23,25 +22,26 @@ export const ServerLogParamsFields: React.FunctionComponent<
{ value: 'error', text: 'Error' },
{ value: 'fatal', text: 'Fatal' },
];

useEffect(() => {
if (!actionParams?.level) {
if (!actionParams.level) {
editAction('level', 'info', index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState<
[boolean, string | undefined]
>([false, defaultMessage]);
useEffect(() => {
if (defaultMessage === resolvedActionGroupMessage) {
editAction('message', defaultMessage, index);
} else if (
(!message || message === resolvedActionGroupMessage) &&
defaultMessage &&
defaultMessage.length > 0
if (
!actionParams?.message ||
(isUsingDefault &&
actionParams?.message === defaultMessageUsed &&
defaultMessageUsed !== defaultMessage)
) {
setDefaultMessageUsage([true, defaultMessage]);
editAction('message', defaultMessage, index);
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultMessage]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { ActionParamsProps } from '../../../../types';
import { SlackActionParams } from '../types';
import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables';
import { resolvedActionGroupMessage } from '../../../constants';

const SlackParamsFields: React.FunctionComponent<ActionParamsProps<SlackActionParams>> = ({
actionParams,
Expand All @@ -19,17 +18,19 @@ const SlackParamsFields: React.FunctionComponent<ActionParamsProps<SlackActionPa
defaultMessage,
}) => {
const { message } = actionParams;
const [[isUsingDefault, defaultMessageUsed], setDefaultMessageUsage] = useState<
[boolean, string | undefined]
>([false, defaultMessage]);
useEffect(() => {
if (defaultMessage === resolvedActionGroupMessage) {
editAction('message', defaultMessage, index);
} else if (
(!message || message === resolvedActionGroupMessage) &&
defaultMessage &&
defaultMessage.length > 0
if (
!actionParams?.message ||
(isUsingDefault &&
actionParams?.message === defaultMessageUsed &&
defaultMessageUsed !== defaultMessage)
) {
setDefaultMessageUsage([true, defaultMessage]);
editAction('message', defaultMessage, index);
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultMessage]);

Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/triggers_actions_ui/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface ActionConnectorFieldsProps<TActionConnector> {
}

export interface ActionParamsProps<TParams> {
actionParams: TParams;
actionParams: Partial<TParams>;
index: number;
editAction: (key: string, value: AlertActionParam, index: number) => void;
errors: IErrorObject;
Expand Down

0 comments on commit c1e885b

Please sign in to comment.