[Alerting] Implemented ability to edit an alert from the alert details page (#64273)
* Implemented ability to edit an alert from the alert details page * Fixed refresh and tests * fixed jest tests
This commit is contained in:
parent
60cb9367a6
commit
75fa843652
5 changed files with 209 additions and 10 deletions
|
@ -7,24 +7,55 @@ import * as React from 'react';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { AlertDetails } from './alert_details';
|
import { AlertDetails } from './alert_details';
|
||||||
import { Alert, ActionType } from '../../../../types';
|
import { Alert, ActionType, AlertTypeRegistryContract } from '../../../../types';
|
||||||
import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiBetaBadge } from '@elastic/eui';
|
import {
|
||||||
|
EuiTitle,
|
||||||
|
EuiBadge,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiSwitch,
|
||||||
|
EuiBetaBadge,
|
||||||
|
EuiButtonEmpty,
|
||||||
|
} from '@elastic/eui';
|
||||||
import { times, random } from 'lodash';
|
import { times, random } from 'lodash';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { ViewInApp } from './view_in_app';
|
import { ViewInApp } from './view_in_app';
|
||||||
import { PLUGIN } from '../../../constants/plugin';
|
import { PLUGIN } from '../../../constants/plugin';
|
||||||
|
import { coreMock } from 'src/core/public/mocks';
|
||||||
|
const mockes = coreMock.createSetup();
|
||||||
|
|
||||||
jest.mock('../../../app_context', () => ({
|
jest.mock('../../../app_context', () => ({
|
||||||
useAppDependencies: jest.fn(() => ({
|
useAppDependencies: jest.fn(() => ({
|
||||||
http: jest.fn(),
|
http: jest.fn(),
|
||||||
legacy: {
|
capabilities: {
|
||||||
capabilities: {
|
get: jest.fn(() => ({})),
|
||||||
get: jest.fn(() => ({})),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
actionTypeRegistry: jest.fn(),
|
||||||
|
alertTypeRegistry: jest.fn(() => {
|
||||||
|
const mocked: jest.Mocked<AlertTypeRegistryContract> = {
|
||||||
|
has: jest.fn(),
|
||||||
|
register: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
list: jest.fn(),
|
||||||
|
};
|
||||||
|
return mocked;
|
||||||
|
}),
|
||||||
|
toastNotifications: mockes.notifications.toasts,
|
||||||
|
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||||
|
uiSettings: mockes.uiSettings,
|
||||||
|
dataPlugin: jest.fn(),
|
||||||
|
charts: jest.fn(),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
useHistory: () => ({
|
||||||
|
push: jest.fn(),
|
||||||
|
}),
|
||||||
|
useLocation: () => ({
|
||||||
|
pathname: '/triggersActions/alerts/',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('../../../lib/capabilities', () => ({
|
jest.mock('../../../lib/capabilities', () => ({
|
||||||
hasSaveAlertsCapability: jest.fn(() => true),
|
hasSaveAlertsCapability: jest.fn(() => true),
|
||||||
}));
|
}));
|
||||||
|
@ -232,6 +263,28 @@ describe('alert_details', () => {
|
||||||
).containsMatchingElement(<ViewInApp alert={alert} />)
|
).containsMatchingElement(<ViewInApp alert={alert} />)
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('links to the Edit flyout', () => {
|
||||||
|
const alert = mockAlert();
|
||||||
|
|
||||||
|
const alertType = {
|
||||||
|
id: '.noop',
|
||||||
|
name: 'No Op',
|
||||||
|
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||||
|
actionVariables: { context: [], state: [] },
|
||||||
|
defaultActionGroupId: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shallow(
|
||||||
|
<AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} />
|
||||||
|
)
|
||||||
|
.find(EuiButtonEmpty)
|
||||||
|
.find('[data-test-subj="openEditAlertFlyoutButton"]')
|
||||||
|
.first()
|
||||||
|
.exists()
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
* 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, { useState } from 'react';
|
import React, { useState, Fragment } from 'react';
|
||||||
import { indexBy } from 'lodash';
|
import { indexBy } from 'lodash';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
EuiPageBody,
|
EuiPageBody,
|
||||||
EuiPageContent,
|
EuiPageContent,
|
||||||
|
@ -21,6 +22,7 @@ import {
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
EuiBetaBadge,
|
EuiBetaBadge,
|
||||||
|
EuiButtonEmpty,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
@ -34,6 +36,9 @@ import {
|
||||||
import { AlertInstancesRouteWithApi } from './alert_instances_route';
|
import { AlertInstancesRouteWithApi } from './alert_instances_route';
|
||||||
import { ViewInApp } from './view_in_app';
|
import { ViewInApp } from './view_in_app';
|
||||||
import { PLUGIN } from '../../../constants/plugin';
|
import { PLUGIN } from '../../../constants/plugin';
|
||||||
|
import { AlertEdit } from '../../alert_form';
|
||||||
|
import { AlertsContextProvider } from '../../../context/alerts_context';
|
||||||
|
import { routeToAlertDetails } from '../../../constants';
|
||||||
|
|
||||||
type AlertDetailsProps = {
|
type AlertDetailsProps = {
|
||||||
alert: Alert;
|
alert: Alert;
|
||||||
|
@ -52,7 +57,18 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
muteAlert,
|
muteAlert,
|
||||||
requestRefresh,
|
requestRefresh,
|
||||||
}) => {
|
}) => {
|
||||||
const { capabilities } = useAppDependencies();
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
http,
|
||||||
|
toastNotifications,
|
||||||
|
capabilities,
|
||||||
|
alertTypeRegistry,
|
||||||
|
actionTypeRegistry,
|
||||||
|
uiSettings,
|
||||||
|
docLinks,
|
||||||
|
charts,
|
||||||
|
dataPlugin,
|
||||||
|
} = useAppDependencies();
|
||||||
|
|
||||||
const canSave = hasSaveAlertsCapability(capabilities);
|
const canSave = hasSaveAlertsCapability(capabilities);
|
||||||
|
|
||||||
|
@ -61,6 +77,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
|
|
||||||
const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled);
|
const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled);
|
||||||
const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll);
|
const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll);
|
||||||
|
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const setAlert = async () => {
|
||||||
|
history.push(routeToAlertDetails.replace(`:alertId`, alert.id));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPage>
|
<EuiPage>
|
||||||
|
@ -90,6 +111,42 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
</EuiPageContentHeaderSection>
|
</EuiPageContentHeaderSection>
|
||||||
<EuiPageContentHeaderSection>
|
<EuiPageContentHeaderSection>
|
||||||
<EuiFlexGroup responsive={false} gutterSize="xs">
|
<EuiFlexGroup responsive={false} gutterSize="xs">
|
||||||
|
{canSave ? (
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
|
<Fragment>
|
||||||
|
{' '}
|
||||||
|
<EuiButtonEmpty
|
||||||
|
data-test-subj="openEditAlertFlyoutButton"
|
||||||
|
iconType="pencil"
|
||||||
|
onClick={() => setEditFlyoutVisibility(true)}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel"
|
||||||
|
defaultMessage="Edit"
|
||||||
|
/>
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
<AlertsContextProvider
|
||||||
|
value={{
|
||||||
|
http,
|
||||||
|
actionTypeRegistry,
|
||||||
|
alertTypeRegistry,
|
||||||
|
toastNotifications,
|
||||||
|
uiSettings,
|
||||||
|
docLinks,
|
||||||
|
charts,
|
||||||
|
dataFieldsFormats: dataPlugin.fieldFormats,
|
||||||
|
reloadAlerts: setAlert,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertEdit
|
||||||
|
initialAlert={alert}
|
||||||
|
editFlyoutVisible={editFlyoutVisible}
|
||||||
|
setEditFlyoutVisibility={setEditFlyoutVisibility}
|
||||||
|
/>
|
||||||
|
</AlertsContextProvider>
|
||||||
|
</Fragment>
|
||||||
|
</EuiFlexItem>
|
||||||
|
) : null}
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<ViewInApp alert={alert} />
|
<ViewInApp alert={alert} />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
|
@ -26,13 +26,13 @@ export const AlertInstancesRoute: React.FunctionComponent<WithAlertStateProps> =
|
||||||
requestRefresh,
|
requestRefresh,
|
||||||
loadAlertState,
|
loadAlertState,
|
||||||
}) => {
|
}) => {
|
||||||
const { http, toastNotifications } = useAppDependencies();
|
const { toastNotifications } = useAppDependencies();
|
||||||
|
|
||||||
const [alertState, setAlertState] = useState<AlertTaskState | null>(null);
|
const [alertState, setAlertState] = useState<AlertTaskState | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications);
|
getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications);
|
||||||
}, [alert, http, loadAlertState, toastNotifications]);
|
}, [alert, loadAlertState, toastNotifications]);
|
||||||
|
|
||||||
return alertState ? (
|
return alertState ? (
|
||||||
<AlertInstances requestRefresh={requestRefresh} alert={alert} alertState={alertState} />
|
<AlertInstances requestRefresh={requestRefresh} alert={alert} alertState={alertState} />
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
const log = getService('log');
|
const log = getService('log');
|
||||||
const alerting = getService('alerting');
|
const alerting = getService('alerting');
|
||||||
const retry = getService('retry');
|
const retry = getService('retry');
|
||||||
|
const find = getService('find');
|
||||||
|
|
||||||
describe('Alert Details', function() {
|
describe('Alert Details', function() {
|
||||||
describe('Header', function() {
|
describe('Header', function() {
|
||||||
|
@ -148,6 +149,56 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Edit alert button', function() {
|
||||||
|
const testRunUuid = uuid.v4();
|
||||||
|
|
||||||
|
it('should open edit alert flyout', async () => {
|
||||||
|
await pageObjects.common.navigateToApp('triggersActions');
|
||||||
|
const params = {
|
||||||
|
aggType: 'count',
|
||||||
|
termSize: 5,
|
||||||
|
thresholdComparator: '>',
|
||||||
|
timeWindowSize: 5,
|
||||||
|
timeWindowUnit: 'm',
|
||||||
|
groupBy: 'all',
|
||||||
|
threshold: [1000, 5000],
|
||||||
|
index: ['.kibana_1'],
|
||||||
|
timeField: 'alert',
|
||||||
|
};
|
||||||
|
const alert = await alerting.alerts.createAlertWithActions(
|
||||||
|
testRunUuid,
|
||||||
|
'.index-threshold',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
// refresh to see alert
|
||||||
|
await browser.refresh();
|
||||||
|
|
||||||
|
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||||
|
|
||||||
|
// Verify content
|
||||||
|
await testSubjects.existOrFail('alertsList');
|
||||||
|
|
||||||
|
// click on first alert
|
||||||
|
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
|
||||||
|
|
||||||
|
const editButton = await testSubjects.find('openEditAlertFlyoutButton');
|
||||||
|
await editButton.click();
|
||||||
|
|
||||||
|
const updatedAlertName = `Changed Alert Name ${uuid.v4()}`;
|
||||||
|
await testSubjects.setValue('alertNameInput', updatedAlertName, {
|
||||||
|
clearWithKeyboard: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)');
|
||||||
|
|
||||||
|
const toastTitle = await pageObjects.common.closeToast();
|
||||||
|
expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`);
|
||||||
|
|
||||||
|
const headingText = await pageObjects.alertDetailsUI.getHeadingText();
|
||||||
|
expect(headingText).to.be(updatedAlertName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('View In App', function() {
|
describe('View In App', function() {
|
||||||
const testRunUuid = uuid.v4();
|
const testRunUuid = uuid.v4();
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,44 @@ export class Alerts {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async createAlertWithActions(
|
||||||
|
name: string,
|
||||||
|
alertTypeId: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
actions?: Array<{
|
||||||
|
id: string;
|
||||||
|
group: string;
|
||||||
|
params: Record<string, any>;
|
||||||
|
}>,
|
||||||
|
tags?: string[],
|
||||||
|
consumer?: string,
|
||||||
|
schedule?: Record<string, any>,
|
||||||
|
throttle?: string
|
||||||
|
) {
|
||||||
|
this.log.debug(`creating alert ${name}`);
|
||||||
|
|
||||||
|
const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, {
|
||||||
|
enabled: true,
|
||||||
|
name,
|
||||||
|
tags,
|
||||||
|
alertTypeId,
|
||||||
|
consumer: consumer ?? 'bar',
|
||||||
|
schedule: schedule ?? { interval: '1m' },
|
||||||
|
throttle: throttle ?? '1m',
|
||||||
|
actions: actions ?? [],
|
||||||
|
params: params ?? {},
|
||||||
|
});
|
||||||
|
if (status !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.debug(`created alert ${alert.id}`);
|
||||||
|
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
public async createNoOp(name: string) {
|
public async createNoOp(name: string) {
|
||||||
this.log.debug(`creating alert ${name}`);
|
this.log.debug(`creating alert ${name}`);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue