[Alerting] Showing confirmation modal on Alert Add/Edit when flyout closed without saving and changes made. (#86370)
* Adding hasChanged check and showing confirmation modal if something has changed * Showing confirmation always on close * Adding functional test * Setting name and tags for APM alerts using initial values instead of setAlertProperty * Checking for alert param changes separately * Checking for alert param changes separately * Fixing functional test * Resetting initial alert params on alert type change * Fixing duplicate import * Cloning edited alert * PR fixes * PR fixes * Updating modal wording Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3ef7bd3197
commit
666af32be4
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { AlertType } from '../../../../common/alert_types';
|
||||
import { getInitialAlertValues } from '../get_initial_alert_values';
|
||||
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
|
||||
|
||||
interface Props {
|
||||
addFlyoutVisible: boolean;
|
||||
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
|
@ -20,10 +21,13 @@ interface KibanaDeps {
|
|||
|
||||
export function AlertingFlyout(props: Props) {
|
||||
const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props;
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const {
|
||||
services: { triggersActionsUi },
|
||||
} = useKibana<KibanaDeps>();
|
||||
|
||||
const initialValues = getInitialAlertValues(alertType, serviceName);
|
||||
|
||||
const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [
|
||||
setAddFlyoutVisibility,
|
||||
]);
|
||||
|
@ -36,7 +40,9 @@ export function AlertingFlyout(props: Props) {
|
|||
onClose: onCloseAddFlyout,
|
||||
alertTypeId: alertType,
|
||||
canChangeTrigger: false,
|
||||
initialValues,
|
||||
}),
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[alertType, onCloseAddFlyout, triggersActionsUi]
|
||||
);
|
||||
return <>{addFlyoutVisible && addAlertFlyout}</>;
|
||||
|
|
|
@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n';
|
|||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { asInteger } from '../../../../common/utils/formatters';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
|
@ -110,7 +109,6 @@ export function ErrorCountAlertTrigger(props: Props) {
|
|||
|
||||
return (
|
||||
<ServiceAlertTrigger
|
||||
alertTypeName={ALERT_TYPES_CONFIG[AlertType.ErrorCount].name}
|
||||
defaults={defaults}
|
||||
fields={fields}
|
||||
setAlertParams={setAlertParams}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getInitialAlertValues } from './get_initial_alert_values';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
|
||||
test('handles null alert type and undefined service name', () => {
|
||||
expect(getInitialAlertValues(null, undefined)).toEqual({ tags: ['apm'] });
|
||||
});
|
||||
|
||||
test('handles valid alert type', () => {
|
||||
const alertType = AlertType.ErrorCount;
|
||||
expect(getInitialAlertValues(alertType, undefined)).toEqual({
|
||||
name: ALERT_TYPES_CONFIG[alertType].name,
|
||||
tags: ['apm'],
|
||||
});
|
||||
|
||||
expect(getInitialAlertValues(alertType, 'Service Name')).toEqual({
|
||||
name: `${ALERT_TYPES_CONFIG[alertType].name} | Service Name`,
|
||||
tags: ['apm', `service.name:service name`],
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
|
||||
|
||||
export function getInitialAlertValues(
|
||||
alertType: AlertType | null,
|
||||
serviceName: string | undefined
|
||||
) {
|
||||
const alertTypeName = alertType
|
||||
? ALERT_TYPES_CONFIG[alertType].name
|
||||
: undefined;
|
||||
const alertName = alertTypeName
|
||||
? serviceName
|
||||
? `${alertTypeName} | ${serviceName}`
|
||||
: alertTypeName
|
||||
: undefined;
|
||||
const tags = ['apm'];
|
||||
if (serviceName) {
|
||||
tags.push(`service.name:${serviceName}`.toLowerCase());
|
||||
}
|
||||
|
||||
return {
|
||||
tags,
|
||||
...(alertName ? { name: alertName } : {}),
|
||||
};
|
||||
}
|
|
@ -9,7 +9,6 @@ import React, { useEffect } from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
|
||||
interface Props {
|
||||
alertTypeName: string;
|
||||
setAlertParams: (key: string, value: any) => void;
|
||||
setAlertProperty: (key: string, value: any) => void;
|
||||
defaults: Record<string, any>;
|
||||
|
@ -20,14 +19,7 @@ interface Props {
|
|||
export function ServiceAlertTrigger(props: Props) {
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
|
||||
const {
|
||||
fields,
|
||||
setAlertParams,
|
||||
setAlertProperty,
|
||||
alertTypeName,
|
||||
defaults,
|
||||
chartPreview,
|
||||
} = props;
|
||||
const { fields, setAlertParams, defaults, chartPreview } = props;
|
||||
|
||||
const params: Record<string, any> = {
|
||||
...defaults,
|
||||
|
@ -36,17 +28,6 @@ export function ServiceAlertTrigger(props: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
// we only want to run this on mount to set default values
|
||||
|
||||
const alertName = params.serviceName
|
||||
? `${alertTypeName} | ${params.serviceName}`
|
||||
: alertTypeName;
|
||||
setAlertProperty('name', alertName);
|
||||
|
||||
const tags = ['apm'];
|
||||
if (params.serviceName) {
|
||||
tags.push(`service.name:${params.serviceName}`.toLowerCase());
|
||||
}
|
||||
setAlertProperty('tags', tags);
|
||||
Object.keys(params).forEach((key) => {
|
||||
setAlertParams(key, params[key]);
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@ describe('ServiceAlertTrigger', () => {
|
|||
expect(() =>
|
||||
render(
|
||||
<ServiceAlertTrigger
|
||||
alertTypeName="test alert type name"
|
||||
defaults={{}}
|
||||
fields={[null]}
|
||||
setAlertParams={() => {}}
|
||||
|
|
|
@ -10,7 +10,6 @@ import React from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { useFetcher } from '../../../../../observability/public';
|
||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
||||
import { TimeSeries } from '../../../../typings/timeseries';
|
||||
|
@ -203,7 +202,6 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
|||
|
||||
return (
|
||||
<ServiceAlertTrigger
|
||||
alertTypeName={ALERT_TYPES_CONFIG['apm.transaction_duration'].name}
|
||||
chartPreview={chartPreview}
|
||||
defaults={defaults}
|
||||
fields={fields}
|
||||
|
|
|
@ -8,7 +8,6 @@ import { useParams } from 'react-router-dom';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ANOMALY_SEVERITY } from '../../../../../ml/common';
|
||||
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
|
||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||
|
@ -106,9 +105,6 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
|
|||
|
||||
return (
|
||||
<ServiceAlertTrigger
|
||||
alertTypeName={
|
||||
ALERT_TYPES_CONFIG['apm.transaction_duration_anomaly'].name
|
||||
}
|
||||
fields={fields}
|
||||
defaults={defaults}
|
||||
setAlertParams={setAlertParams}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
||||
import { AlertType, ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { asPercent } from '../../../../common/utils/formatters';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
|
@ -137,7 +136,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
|||
|
||||
return (
|
||||
<ServiceAlertTrigger
|
||||
alertTypeName={ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate].name}
|
||||
fields={fields}
|
||||
defaults={defaultParams}
|
||||
setAlertParams={setAlertParams}
|
||||
|
|
|
@ -3,14 +3,16 @@
|
|||
* 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, { useCallback, useReducer, useMemo, useState, useEffect } from 'react';
|
||||
import React, { useReducer, useMemo, useState, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ActionTypeRegistryContract,
|
||||
Alert,
|
||||
AlertTypeRegistryContract,
|
||||
AlertTypeParams,
|
||||
AlertUpdates,
|
||||
} from '../../../types';
|
||||
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
||||
|
@ -18,10 +20,12 @@ import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer
|
|||
import { createAlert } from '../../lib/alert_api';
|
||||
import { HealthCheck } from '../../components/health_check';
|
||||
import { ConfirmAlertSave } from './confirm_alert_save';
|
||||
import { ConfirmAlertClose } from './confirm_alert_close';
|
||||
import { hasShowActionsCapability } from '../../lib/capabilities';
|
||||
import AlertAddFooter from './alert_add_footer';
|
||||
import { HealthContextProvider } from '../../context/health_context';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { hasAlertChanged, haveAlertParamsChanged } from './has_alert_changed';
|
||||
import { getAlertWithInvalidatedFields } from '../../lib/value_validators';
|
||||
|
||||
export interface AlertAddProps<MetaData = Record<string, any>> {
|
||||
|
@ -66,8 +70,10 @@ const AlertAdd = ({
|
|||
const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, {
|
||||
alert: initialAlert,
|
||||
});
|
||||
const [initialAlertParams, setInitialAlertParams] = useState<AlertTypeParams>({});
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState<boolean>(false);
|
||||
const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState<boolean>(false);
|
||||
|
||||
const setAlert = (value: InitialAlert) => {
|
||||
dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } });
|
||||
|
@ -91,16 +97,35 @@ const AlertAdd = ({
|
|||
}
|
||||
}, [alertTypeId]);
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
setAlert(initialAlert);
|
||||
useEffect(() => {
|
||||
if (isEmpty(alert.params) && !isEmpty(initialAlertParams)) {
|
||||
// alert params are explicitly cleared when the alert type is cleared.
|
||||
// clear the "initial" params in order to capture the
|
||||
// default when a new alert type is selected
|
||||
setInitialAlertParams({});
|
||||
} else if (isEmpty(initialAlertParams)) {
|
||||
// captures the first change to the alert params,
|
||||
// when consumers set a default value for the alert params
|
||||
setInitialAlertParams(alert.params);
|
||||
}
|
||||
}, [alert.params, initialAlertParams, setInitialAlertParams]);
|
||||
|
||||
const checkForChangesAndCloseFlyout = () => {
|
||||
if (
|
||||
hasAlertChanged(alert, initialAlert, false) ||
|
||||
haveAlertParamsChanged(alert.params, initialAlertParams)
|
||||
) {
|
||||
setIsConfirmAlertCloseModalOpen(true);
|
||||
} else {
|
||||
onClose();
|
||||
}, [initialAlert, onClose]);
|
||||
}
|
||||
};
|
||||
|
||||
const saveAlertAndCloseFlyout = async () => {
|
||||
const savedAlert = await onSaveAlert();
|
||||
setIsSaving(false);
|
||||
if (savedAlert) {
|
||||
closeFlyout();
|
||||
onClose();
|
||||
if (reloadAlerts) {
|
||||
reloadAlerts();
|
||||
}
|
||||
|
@ -142,7 +167,7 @@ const AlertAdd = ({
|
|||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
onClose={closeFlyout}
|
||||
onClose={checkForChangesAndCloseFlyout}
|
||||
aria-labelledby="flyoutAlertAddTitle"
|
||||
size="m"
|
||||
maxWidth={620}
|
||||
|
@ -198,7 +223,7 @@ const AlertAdd = ({
|
|||
await saveAlertAndCloseFlyout();
|
||||
}
|
||||
}}
|
||||
onCancel={closeFlyout}
|
||||
onCancel={checkForChangesAndCloseFlyout}
|
||||
/>
|
||||
</HealthCheck>
|
||||
</HealthContextProvider>
|
||||
|
@ -214,6 +239,17 @@ const AlertAdd = ({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{isConfirmAlertCloseModalOpen && (
|
||||
<ConfirmAlertClose
|
||||
onConfirm={() => {
|
||||
setIsConfirmAlertCloseModalOpen(false);
|
||||
onClose();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsConfirmAlertCloseModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionTypeRegistryContract, Alert, AlertTypeRegistryContract } from '../../../types';
|
||||
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
||||
|
@ -27,6 +28,8 @@ import { updateAlert } from '../../lib/alert_api';
|
|||
import { HealthCheck } from '../../components/health_check';
|
||||
import { HealthContextProvider } from '../../context/health_context';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { ConfirmAlertClose } from './confirm_alert_close';
|
||||
import { hasAlertChanged } from './has_alert_changed';
|
||||
import { getAlertWithInvalidatedFields } from '../../lib/value_validators';
|
||||
|
||||
export interface AlertEditProps<MetaData = Record<string, any>> {
|
||||
|
@ -47,13 +50,14 @@ export const AlertEdit = ({
|
|||
metadata,
|
||||
}: AlertEditProps) => {
|
||||
const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, {
|
||||
alert: initialAlert,
|
||||
alert: cloneDeep(initialAlert),
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false);
|
||||
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
http,
|
||||
|
@ -71,6 +75,14 @@ export const AlertEdit = ({
|
|||
alertType
|
||||
);
|
||||
|
||||
const checkForChangesAndCloseFlyout = () => {
|
||||
if (hasAlertChanged(alert, initialAlert, true)) {
|
||||
setIsConfirmAlertCloseModalOpen(true);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
async function onSaveAlert(): Promise<Alert | undefined> {
|
||||
try {
|
||||
if (isValidAlert(alert, alertErrors, alertActionsErrors) && !hasActionsWithBrokenConnector) {
|
||||
|
@ -107,7 +119,7 @@ export const AlertEdit = ({
|
|||
return (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
onClose={() => onClose()}
|
||||
onClose={checkForChangesAndCloseFlyout}
|
||||
aria-labelledby="flyoutAlertEditTitle"
|
||||
size="m"
|
||||
maxWidth={620}
|
||||
|
@ -160,7 +172,7 @@ export const AlertEdit = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="cancelSaveEditedAlertButton"
|
||||
onClick={() => onClose()}
|
||||
onClick={() => checkForChangesAndCloseFlyout()}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel',
|
||||
|
@ -200,6 +212,17 @@ export const AlertEdit = ({
|
|||
</EuiFlyoutFooter>
|
||||
</HealthCheck>
|
||||
</HealthContextProvider>
|
||||
{isConfirmAlertCloseModalOpen && (
|
||||
<ConfirmAlertClose
|
||||
onConfirm={() => {
|
||||
setIsConfirmAlertCloseModalOpen(false);
|
||||
onClose();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsConfirmAlertCloseModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* 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 from 'react';
|
||||
import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Props {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmAlertClose: React.FC<Props> = ({ onConfirm, onCancel }) => {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseTitle',
|
||||
{
|
||||
defaultMessage: 'Discard unsaved changes to alert?',
|
||||
}
|
||||
)}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseConfirmButtonText',
|
||||
{
|
||||
defaultMessage: 'Discard changes',
|
||||
}
|
||||
)}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseCancelButtonText',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="confirmAlertCloseModal"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.confirmAlertClose.confirmAlertCloseMessage"
|
||||
defaultMessage="You can't recover unsaved changes."
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InitialAlert } from './alert_reducer';
|
||||
import { hasAlertChanged } from './has_alert_changed';
|
||||
|
||||
function createAlert(overrides = {}): InitialAlert {
|
||||
return {
|
||||
params: {},
|
||||
consumer: 'test',
|
||||
alertTypeId: 'test',
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
actions: [],
|
||||
tags: [],
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('should return false for same alert', () => {
|
||||
const a = createAlert();
|
||||
expect(hasAlertChanged(a, a, true)).toEqual(false);
|
||||
});
|
||||
|
||||
test('should return true for different alert', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({ alertTypeId: 'differentTest' });
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should correctly compare name field', () => {
|
||||
// name field doesn't exist initially
|
||||
const a = createAlert();
|
||||
// set name to actual value
|
||||
const b = createAlert({ name: 'myAlert' });
|
||||
// set name to different value
|
||||
const c = createAlert({ name: 'anotherAlert' });
|
||||
// set name to various empty/null/undefined states
|
||||
const d = createAlert({ name: '' });
|
||||
const e = createAlert({ name: undefined });
|
||||
const f = createAlert({ name: null });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, c, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, d, true)).toEqual(false);
|
||||
expect(hasAlertChanged(a, e, true)).toEqual(false);
|
||||
expect(hasAlertChanged(a, f, true)).toEqual(false);
|
||||
|
||||
expect(hasAlertChanged(b, c, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, d, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, e, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, f, true)).toEqual(true);
|
||||
|
||||
expect(hasAlertChanged(c, d, true)).toEqual(true);
|
||||
expect(hasAlertChanged(c, e, true)).toEqual(true);
|
||||
expect(hasAlertChanged(c, f, true)).toEqual(true);
|
||||
|
||||
expect(hasAlertChanged(d, e, true)).toEqual(false);
|
||||
expect(hasAlertChanged(d, f, true)).toEqual(false);
|
||||
});
|
||||
|
||||
test('should correctly compare alertTypeId field', () => {
|
||||
const a = createAlert();
|
||||
|
||||
// set alertTypeId to different value
|
||||
const b = createAlert({ alertTypeId: 'myAlertId' });
|
||||
// set alertTypeId to various empty/null/undefined states
|
||||
const c = createAlert({ alertTypeId: '' });
|
||||
const d = createAlert({ alertTypeId: undefined });
|
||||
const e = createAlert({ alertTypeId: null });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, c, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, d, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, e, true)).toEqual(true);
|
||||
|
||||
expect(hasAlertChanged(b, c, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, d, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, e, true)).toEqual(true);
|
||||
|
||||
expect(hasAlertChanged(c, d, true)).toEqual(false);
|
||||
expect(hasAlertChanged(c, e, true)).toEqual(false);
|
||||
expect(hasAlertChanged(d, e, true)).toEqual(false);
|
||||
});
|
||||
|
||||
test('should correctly compare throttle field', () => {
|
||||
// throttle field doesn't exist initially
|
||||
const a = createAlert();
|
||||
// set throttle to actual value
|
||||
const b = createAlert({ throttle: '1m' });
|
||||
// set throttle to different value
|
||||
const c = createAlert({ throttle: '1h' });
|
||||
// set throttle to various empty/null/undefined states
|
||||
const d = createAlert({ throttle: '' });
|
||||
const e = createAlert({ throttle: undefined });
|
||||
const f = createAlert({ throttle: null });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, c, true)).toEqual(true);
|
||||
expect(hasAlertChanged(a, d, true)).toEqual(false);
|
||||
expect(hasAlertChanged(a, e, true)).toEqual(false);
|
||||
expect(hasAlertChanged(a, f, true)).toEqual(false);
|
||||
|
||||
expect(hasAlertChanged(b, c, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, d, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, e, true)).toEqual(true);
|
||||
expect(hasAlertChanged(b, f, true)).toEqual(true);
|
||||
|
||||
expect(hasAlertChanged(c, d, true)).toEqual(true);
|
||||
expect(hasAlertChanged(c, e, true)).toEqual(true);
|
||||
expect(hasAlertChanged(c, f, true)).toEqual(true);
|
||||
|
||||
expect(hasAlertChanged(d, e, true)).toEqual(false);
|
||||
expect(hasAlertChanged(d, f, true)).toEqual(false);
|
||||
});
|
||||
|
||||
test('should correctly compare tags field', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({ tags: ['first'] });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should correctly compare schedule field', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({ schedule: { interval: '3h' } });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should correctly compare actions field', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({
|
||||
actions: [{ actionTypeId: 'action', group: 'group', id: 'actionId', params: {} }],
|
||||
});
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should skip comparing params field if compareParams=false', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({ params: { newParam: 'value' } });
|
||||
|
||||
expect(hasAlertChanged(a, b, false)).toEqual(false);
|
||||
});
|
||||
|
||||
test('should correctly compare params field if compareParams=true', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({ params: { newParam: 'value' } });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
});
|
||||
|
||||
test('should correctly compare notifyWhen field', () => {
|
||||
const a = createAlert();
|
||||
const b = createAlert({ notifyWhen: 'onActiveAlert' });
|
||||
|
||||
expect(hasAlertChanged(a, b, true)).toEqual(true);
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { pick } from 'lodash';
|
||||
import { AlertTypeParams } from '../../../types';
|
||||
import { InitialAlert } from './alert_reducer';
|
||||
|
||||
const DEEP_COMPARE_FIELDS = ['tags', 'schedule', 'actions', 'notifyWhen'];
|
||||
|
||||
function getNonNullCompareFields(alert: InitialAlert) {
|
||||
const { name, alertTypeId, throttle } = alert;
|
||||
return {
|
||||
...(!!(name && name.length > 0) ? { name } : {}),
|
||||
...(!!(alertTypeId && alertTypeId.length > 0) ? { alertTypeId } : {}),
|
||||
...(!!(throttle && throttle.length > 0) ? { throttle } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function hasAlertChanged(a: InitialAlert, b: InitialAlert, compareParams: boolean) {
|
||||
// Deep compare these fields
|
||||
let objectsAreEqual = deepEqual(pick(a, DEEP_COMPARE_FIELDS), pick(b, DEEP_COMPARE_FIELDS));
|
||||
if (compareParams) {
|
||||
objectsAreEqual = objectsAreEqual && deepEqual(a.params, b.params);
|
||||
}
|
||||
|
||||
const nonNullCompareFieldsAreEqual = deepEqual(
|
||||
getNonNullCompareFields(a),
|
||||
getNonNullCompareFields(b)
|
||||
);
|
||||
|
||||
return !objectsAreEqual || !nonNullCompareFieldsAreEqual;
|
||||
}
|
||||
|
||||
export function haveAlertParamsChanged(a: AlertTypeParams, b: AlertTypeParams) {
|
||||
return !deepEqual(a, b);
|
||||
}
|
|
@ -8,11 +8,12 @@ import type { DocLinksStart } from 'kibana/public';
|
|||
import { ComponentType } from 'react';
|
||||
import { ChartsPluginSetup } from 'src/plugins/charts/public';
|
||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { ActionGroup, AlertActionParam, AlertTypeParams } from '../../alerts/common';
|
||||
import { ActionType } from '../../actions/common';
|
||||
import { TypeRegistry } from './application/type_registry';
|
||||
import { AlertType as CommonAlertType } from '../../alerts/common';
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertActionParam,
|
||||
SanitizedAlert,
|
||||
AlertAction,
|
||||
AlertAggregations,
|
||||
|
@ -22,6 +23,7 @@ import {
|
|||
RawAlertInstance,
|
||||
AlertingFrameworkHealth,
|
||||
AlertNotifyWhenType,
|
||||
AlertTypeParams,
|
||||
} from '../../alerts/common';
|
||||
|
||||
// In Triggers and Actions we treat all `Alert`s as `SanitizedAlert<AlertTypeParams>`
|
||||
|
@ -38,6 +40,7 @@ export {
|
|||
RawAlertInstance,
|
||||
AlertingFrameworkHealth,
|
||||
AlertNotifyWhenType,
|
||||
AlertTypeParams,
|
||||
};
|
||||
export { ActionType };
|
||||
|
||||
|
|
|
@ -204,5 +204,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const alertsToDelete = await getAlertsByName(alertName);
|
||||
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
|
||||
});
|
||||
|
||||
it('should show discard confirmation before closing flyout without saving', async () => {
|
||||
await pageObjects.triggersActionsUI.clickCreateAlertButton();
|
||||
await testSubjects.click('cancelSaveAlertButton');
|
||||
await testSubjects.missingOrFail('confirmAlertCloseModal');
|
||||
|
||||
await pageObjects.triggersActionsUI.clickCreateAlertButton();
|
||||
await testSubjects.setValue('intervalInput', '10');
|
||||
await testSubjects.click('cancelSaveAlertButton');
|
||||
await testSubjects.existOrFail('confirmAlertCloseModal');
|
||||
await testSubjects.click('confirmAlertCloseModal > confirmModalCancelButton');
|
||||
await testSubjects.missingOrFail('confirmAlertCloseModal');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -298,6 +298,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
await testSubjects.click('cancelSaveEditedAlertButton');
|
||||
await testSubjects.existOrFail('confirmAlertCloseModal');
|
||||
await testSubjects.click('confirmAlertCloseModal > confirmModalConfirmButton');
|
||||
await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]');
|
||||
|
||||
await editButton.click();
|
||||
|
|
Loading…
Reference in a new issue