[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:
ymao1 2021-01-11 16:16:10 -05:00 committed by GitHub
parent 3ef7bd3197
commit 666af32be4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 408 additions and 44 deletions

View file

@ -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}</>;

View file

@ -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}

View file

@ -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`],
});
});

View file

@ -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 } : {}),
};
}

View file

@ -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]);
});

View file

@ -18,7 +18,6 @@ describe('ServiceAlertTrigger', () => {
expect(() =>
render(
<ServiceAlertTrigger
alertTypeName="test alert type name"
defaults={{}}
fields={[null]}
setAlertParams={() => {}}

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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);
onClose();
}, [initialAlert, onClose]);
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();
}
};
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>
);

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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);
});

View file

@ -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);
}

View file

@ -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 };

View file

@ -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');
});
});
};

View file

@ -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();