[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.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||||
import { AlertType } from '../../../../common/alert_types';
|
import { AlertType } from '../../../../common/alert_types';
|
||||||
|
import { getInitialAlertValues } from '../get_initial_alert_values';
|
||||||
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
|
import { TriggersAndActionsUIPublicPluginStart } from '../../../../../triggers_actions_ui/public';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
addFlyoutVisible: boolean;
|
addFlyoutVisible: boolean;
|
||||||
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
setAddFlyoutVisibility: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
@ -20,10 +21,13 @@ interface KibanaDeps {
|
||||||
|
|
||||||
export function AlertingFlyout(props: Props) {
|
export function AlertingFlyout(props: Props) {
|
||||||
const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props;
|
const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props;
|
||||||
|
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||||
const {
|
const {
|
||||||
services: { triggersActionsUi },
|
services: { triggersActionsUi },
|
||||||
} = useKibana<KibanaDeps>();
|
} = useKibana<KibanaDeps>();
|
||||||
|
|
||||||
|
const initialValues = getInitialAlertValues(alertType, serviceName);
|
||||||
|
|
||||||
const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [
|
const onCloseAddFlyout = useCallback(() => setAddFlyoutVisibility(false), [
|
||||||
setAddFlyoutVisibility,
|
setAddFlyoutVisibility,
|
||||||
]);
|
]);
|
||||||
|
@ -36,7 +40,9 @@ export function AlertingFlyout(props: Props) {
|
||||||
onClose: onCloseAddFlyout,
|
onClose: onCloseAddFlyout,
|
||||||
alertTypeId: alertType,
|
alertTypeId: alertType,
|
||||||
canChangeTrigger: false,
|
canChangeTrigger: false,
|
||||||
|
initialValues,
|
||||||
}),
|
}),
|
||||||
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
[alertType, onCloseAddFlyout, triggersActionsUi]
|
[alertType, onCloseAddFlyout, triggersActionsUi]
|
||||||
);
|
);
|
||||||
return <>{addFlyoutVisible && addAlertFlyout}</>;
|
return <>{addFlyoutVisible && addAlertFlyout}</>;
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
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 { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||||
import { asInteger } from '../../../../common/utils/formatters';
|
import { asInteger } from '../../../../common/utils/formatters';
|
||||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||||
|
@ -110,7 +109,6 @@ export function ErrorCountAlertTrigger(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServiceAlertTrigger
|
<ServiceAlertTrigger
|
||||||
alertTypeName={ALERT_TYPES_CONFIG[AlertType.ErrorCount].name}
|
|
||||||
defaults={defaults}
|
defaults={defaults}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
setAlertParams={setAlertParams}
|
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';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alertTypeName: string;
|
|
||||||
setAlertParams: (key: string, value: any) => void;
|
setAlertParams: (key: string, value: any) => void;
|
||||||
setAlertProperty: (key: string, value: any) => void;
|
setAlertProperty: (key: string, value: any) => void;
|
||||||
defaults: Record<string, any>;
|
defaults: Record<string, any>;
|
||||||
|
@ -20,14 +19,7 @@ interface Props {
|
||||||
export function ServiceAlertTrigger(props: Props) {
|
export function ServiceAlertTrigger(props: Props) {
|
||||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||||
|
|
||||||
const {
|
const { fields, setAlertParams, defaults, chartPreview } = props;
|
||||||
fields,
|
|
||||||
setAlertParams,
|
|
||||||
setAlertProperty,
|
|
||||||
alertTypeName,
|
|
||||||
defaults,
|
|
||||||
chartPreview,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
@ -36,17 +28,6 @@ export function ServiceAlertTrigger(props: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// we only want to run this on mount to set default values
|
// 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) => {
|
Object.keys(params).forEach((key) => {
|
||||||
setAlertParams(key, params[key]);
|
setAlertParams(key, params[key]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,6 @@ describe('ServiceAlertTrigger', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
render(
|
render(
|
||||||
<ServiceAlertTrigger
|
<ServiceAlertTrigger
|
||||||
alertTypeName="test alert type name"
|
|
||||||
defaults={{}}
|
defaults={{}}
|
||||||
fields={[null]}
|
fields={[null]}
|
||||||
setAlertParams={() => {}}
|
setAlertParams={() => {}}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useFetcher } from '../../../../../observability/public';
|
import { useFetcher } from '../../../../../observability/public';
|
||||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/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 { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||||
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
||||||
import { TimeSeries } from '../../../../typings/timeseries';
|
import { TimeSeries } from '../../../../typings/timeseries';
|
||||||
|
@ -203,7 +202,6 @@ export function TransactionDurationAlertTrigger(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServiceAlertTrigger
|
<ServiceAlertTrigger
|
||||||
alertTypeName={ALERT_TYPES_CONFIG['apm.transaction_duration'].name}
|
|
||||||
chartPreview={chartPreview}
|
chartPreview={chartPreview}
|
||||||
defaults={defaults}
|
defaults={defaults}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { useParams } from 'react-router-dom';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ANOMALY_SEVERITY } from '../../../../../ml/common';
|
import { ANOMALY_SEVERITY } from '../../../../../ml/common';
|
||||||
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
|
|
||||||
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher';
|
||||||
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||||
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
import { ServiceAlertTrigger } from '../service_alert_trigger';
|
||||||
|
@ -106,9 +105,6 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServiceAlertTrigger
|
<ServiceAlertTrigger
|
||||||
alertTypeName={
|
|
||||||
ALERT_TYPES_CONFIG['apm.transaction_duration_anomaly'].name
|
|
||||||
}
|
|
||||||
fields={fields}
|
fields={fields}
|
||||||
defaults={defaults}
|
defaults={defaults}
|
||||||
setAlertParams={setAlertParams}
|
setAlertParams={setAlertParams}
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
|
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 { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||||
import { asPercent } from '../../../../common/utils/formatters';
|
import { asPercent } from '../../../../common/utils/formatters';
|
||||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||||
|
@ -137,7 +136,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServiceAlertTrigger
|
<ServiceAlertTrigger
|
||||||
alertTypeName={ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate].name}
|
|
||||||
fields={fields}
|
fields={fields}
|
||||||
defaults={defaultParams}
|
defaults={defaultParams}
|
||||||
setAlertParams={setAlertParams}
|
setAlertParams={setAlertParams}
|
||||||
|
|
|
@ -3,14 +3,16 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
|
import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ActionTypeRegistryContract,
|
ActionTypeRegistryContract,
|
||||||
Alert,
|
Alert,
|
||||||
AlertTypeRegistryContract,
|
AlertTypeRegistryContract,
|
||||||
|
AlertTypeParams,
|
||||||
AlertUpdates,
|
AlertUpdates,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
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 { createAlert } from '../../lib/alert_api';
|
||||||
import { HealthCheck } from '../../components/health_check';
|
import { HealthCheck } from '../../components/health_check';
|
||||||
import { ConfirmAlertSave } from './confirm_alert_save';
|
import { ConfirmAlertSave } from './confirm_alert_save';
|
||||||
|
import { ConfirmAlertClose } from './confirm_alert_close';
|
||||||
import { hasShowActionsCapability } from '../../lib/capabilities';
|
import { hasShowActionsCapability } from '../../lib/capabilities';
|
||||||
import AlertAddFooter from './alert_add_footer';
|
import AlertAddFooter from './alert_add_footer';
|
||||||
import { HealthContextProvider } from '../../context/health_context';
|
import { HealthContextProvider } from '../../context/health_context';
|
||||||
import { useKibana } from '../../../common/lib/kibana';
|
import { useKibana } from '../../../common/lib/kibana';
|
||||||
|
import { hasAlertChanged, haveAlertParamsChanged } from './has_alert_changed';
|
||||||
import { getAlertWithInvalidatedFields } from '../../lib/value_validators';
|
import { getAlertWithInvalidatedFields } from '../../lib/value_validators';
|
||||||
|
|
||||||
export interface AlertAddProps<MetaData = Record<string, any>> {
|
export interface AlertAddProps<MetaData = Record<string, any>> {
|
||||||
|
@ -66,8 +70,10 @@ const AlertAdd = ({
|
||||||
const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, {
|
const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, {
|
||||||
alert: initialAlert,
|
alert: initialAlert,
|
||||||
});
|
});
|
||||||
|
const [initialAlertParams, setInitialAlertParams] = useState<AlertTypeParams>({});
|
||||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState<boolean>(false);
|
const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState<boolean>(false);
|
||||||
|
const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const setAlert = (value: InitialAlert) => {
|
const setAlert = (value: InitialAlert) => {
|
||||||
dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } });
|
dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } });
|
||||||
|
@ -91,16 +97,35 @@ const AlertAdd = ({
|
||||||
}
|
}
|
||||||
}, [alertTypeId]);
|
}, [alertTypeId]);
|
||||||
|
|
||||||
const closeFlyout = useCallback(() => {
|
useEffect(() => {
|
||||||
setAlert(initialAlert);
|
if (isEmpty(alert.params) && !isEmpty(initialAlertParams)) {
|
||||||
onClose();
|
// alert params are explicitly cleared when the alert type is cleared.
|
||||||
}, [initialAlert, onClose]);
|
// 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 saveAlertAndCloseFlyout = async () => {
|
||||||
const savedAlert = await onSaveAlert();
|
const savedAlert = await onSaveAlert();
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
if (savedAlert) {
|
if (savedAlert) {
|
||||||
closeFlyout();
|
onClose();
|
||||||
if (reloadAlerts) {
|
if (reloadAlerts) {
|
||||||
reloadAlerts();
|
reloadAlerts();
|
||||||
}
|
}
|
||||||
|
@ -142,7 +167,7 @@ const AlertAdd = ({
|
||||||
return (
|
return (
|
||||||
<EuiPortal>
|
<EuiPortal>
|
||||||
<EuiFlyout
|
<EuiFlyout
|
||||||
onClose={closeFlyout}
|
onClose={checkForChangesAndCloseFlyout}
|
||||||
aria-labelledby="flyoutAlertAddTitle"
|
aria-labelledby="flyoutAlertAddTitle"
|
||||||
size="m"
|
size="m"
|
||||||
maxWidth={620}
|
maxWidth={620}
|
||||||
|
@ -198,7 +223,7 @@ const AlertAdd = ({
|
||||||
await saveAlertAndCloseFlyout();
|
await saveAlertAndCloseFlyout();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCancel={closeFlyout}
|
onCancel={checkForChangesAndCloseFlyout}
|
||||||
/>
|
/>
|
||||||
</HealthCheck>
|
</HealthCheck>
|
||||||
</HealthContextProvider>
|
</HealthContextProvider>
|
||||||
|
@ -214,6 +239,17 @@ const AlertAdd = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isConfirmAlertCloseModalOpen && (
|
||||||
|
<ConfirmAlertClose
|
||||||
|
onConfirm={() => {
|
||||||
|
setIsConfirmAlertCloseModalOpen(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsConfirmAlertCloseModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</EuiFlyout>
|
</EuiFlyout>
|
||||||
</EuiPortal>
|
</EuiPortal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ActionTypeRegistryContract, Alert, AlertTypeRegistryContract } from '../../../types';
|
import { ActionTypeRegistryContract, Alert, AlertTypeRegistryContract } from '../../../types';
|
||||||
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
import { AlertForm, getAlertErrors, isValidAlert } from './alert_form';
|
||||||
|
@ -27,6 +28,8 @@ import { updateAlert } from '../../lib/alert_api';
|
||||||
import { HealthCheck } from '../../components/health_check';
|
import { HealthCheck } from '../../components/health_check';
|
||||||
import { HealthContextProvider } from '../../context/health_context';
|
import { HealthContextProvider } from '../../context/health_context';
|
||||||
import { useKibana } from '../../../common/lib/kibana';
|
import { useKibana } from '../../../common/lib/kibana';
|
||||||
|
import { ConfirmAlertClose } from './confirm_alert_close';
|
||||||
|
import { hasAlertChanged } from './has_alert_changed';
|
||||||
import { getAlertWithInvalidatedFields } from '../../lib/value_validators';
|
import { getAlertWithInvalidatedFields } from '../../lib/value_validators';
|
||||||
|
|
||||||
export interface AlertEditProps<MetaData = Record<string, any>> {
|
export interface AlertEditProps<MetaData = Record<string, any>> {
|
||||||
|
@ -47,13 +50,14 @@ export const AlertEdit = ({
|
||||||
metadata,
|
metadata,
|
||||||
}: AlertEditProps) => {
|
}: AlertEditProps) => {
|
||||||
const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, {
|
const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, {
|
||||||
alert: initialAlert,
|
alert: cloneDeep(initialAlert),
|
||||||
});
|
});
|
||||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false);
|
const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false);
|
||||||
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState<boolean>(
|
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState<boolean>(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
http,
|
http,
|
||||||
|
@ -71,6 +75,14 @@ export const AlertEdit = ({
|
||||||
alertType
|
alertType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const checkForChangesAndCloseFlyout = () => {
|
||||||
|
if (hasAlertChanged(alert, initialAlert, true)) {
|
||||||
|
setIsConfirmAlertCloseModalOpen(true);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function onSaveAlert(): Promise<Alert | undefined> {
|
async function onSaveAlert(): Promise<Alert | undefined> {
|
||||||
try {
|
try {
|
||||||
if (isValidAlert(alert, alertErrors, alertActionsErrors) && !hasActionsWithBrokenConnector) {
|
if (isValidAlert(alert, alertErrors, alertActionsErrors) && !hasActionsWithBrokenConnector) {
|
||||||
|
@ -107,7 +119,7 @@ export const AlertEdit = ({
|
||||||
return (
|
return (
|
||||||
<EuiPortal>
|
<EuiPortal>
|
||||||
<EuiFlyout
|
<EuiFlyout
|
||||||
onClose={() => onClose()}
|
onClose={checkForChangesAndCloseFlyout}
|
||||||
aria-labelledby="flyoutAlertEditTitle"
|
aria-labelledby="flyoutAlertEditTitle"
|
||||||
size="m"
|
size="m"
|
||||||
maxWidth={620}
|
maxWidth={620}
|
||||||
|
@ -160,7 +172,7 @@ export const AlertEdit = ({
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty
|
||||||
data-test-subj="cancelSaveEditedAlertButton"
|
data-test-subj="cancelSaveEditedAlertButton"
|
||||||
onClick={() => onClose()}
|
onClick={() => checkForChangesAndCloseFlyout()}
|
||||||
>
|
>
|
||||||
{i18n.translate(
|
{i18n.translate(
|
||||||
'xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel',
|
'xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel',
|
||||||
|
@ -200,6 +212,17 @@ export const AlertEdit = ({
|
||||||
</EuiFlyoutFooter>
|
</EuiFlyoutFooter>
|
||||||
</HealthCheck>
|
</HealthCheck>
|
||||||
</HealthContextProvider>
|
</HealthContextProvider>
|
||||||
|
{isConfirmAlertCloseModalOpen && (
|
||||||
|
<ConfirmAlertClose
|
||||||
|
onConfirm={() => {
|
||||||
|
setIsConfirmAlertCloseModalOpen(false);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsConfirmAlertCloseModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</EuiFlyout>
|
</EuiFlyout>
|
||||||
</EuiPortal>
|
</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 { ComponentType } from 'react';
|
||||||
import { ChartsPluginSetup } from 'src/plugins/charts/public';
|
import { ChartsPluginSetup } from 'src/plugins/charts/public';
|
||||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||||
import { ActionGroup, AlertActionParam, AlertTypeParams } from '../../alerts/common';
|
|
||||||
import { ActionType } from '../../actions/common';
|
import { ActionType } from '../../actions/common';
|
||||||
import { TypeRegistry } from './application/type_registry';
|
import { TypeRegistry } from './application/type_registry';
|
||||||
import { AlertType as CommonAlertType } from '../../alerts/common';
|
import { AlertType as CommonAlertType } from '../../alerts/common';
|
||||||
import {
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
AlertActionParam,
|
||||||
SanitizedAlert,
|
SanitizedAlert,
|
||||||
AlertAction,
|
AlertAction,
|
||||||
AlertAggregations,
|
AlertAggregations,
|
||||||
|
@ -22,6 +23,7 @@ import {
|
||||||
RawAlertInstance,
|
RawAlertInstance,
|
||||||
AlertingFrameworkHealth,
|
AlertingFrameworkHealth,
|
||||||
AlertNotifyWhenType,
|
AlertNotifyWhenType,
|
||||||
|
AlertTypeParams,
|
||||||
} from '../../alerts/common';
|
} from '../../alerts/common';
|
||||||
|
|
||||||
// In Triggers and Actions we treat all `Alert`s as `SanitizedAlert<AlertTypeParams>`
|
// In Triggers and Actions we treat all `Alert`s as `SanitizedAlert<AlertTypeParams>`
|
||||||
|
@ -38,6 +40,7 @@ export {
|
||||||
RawAlertInstance,
|
RawAlertInstance,
|
||||||
AlertingFrameworkHealth,
|
AlertingFrameworkHealth,
|
||||||
AlertNotifyWhenType,
|
AlertNotifyWhenType,
|
||||||
|
AlertTypeParams,
|
||||||
};
|
};
|
||||||
export { ActionType };
|
export { ActionType };
|
||||||
|
|
||||||
|
|
|
@ -204,5 +204,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
const alertsToDelete = await getAlertsByName(alertName);
|
const alertsToDelete = await getAlertsByName(alertName);
|
||||||
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
|
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.click('cancelSaveEditedAlertButton');
|
||||||
|
await testSubjects.existOrFail('confirmAlertCloseModal');
|
||||||
|
await testSubjects.click('confirmAlertCloseModal > confirmModalConfirmButton');
|
||||||
await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]');
|
await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]');
|
||||||
|
|
||||||
await editButton.click();
|
await editButton.click();
|
||||||
|
|
Loading…
Reference in a new issue