Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Alerting] Implemented ability to edit an alert from the alert details page (#64273) #64545

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,55 @@ import * as React from 'react';
import uuid from 'uuid';
import { shallow } from 'enzyme';
import { AlertDetails } from './alert_details';
import { Alert, ActionType } from '../../../../types';
import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiBetaBadge } from '@elastic/eui';
import { Alert, ActionType, AlertTypeRegistryContract } from '../../../../types';
import {
EuiTitle,
EuiBadge,
EuiFlexItem,
EuiSwitch,
EuiBetaBadge,
EuiButtonEmpty,
} from '@elastic/eui';
import { times, random } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ViewInApp } from './view_in_app';
import { PLUGIN } from '../../../constants/plugin';
import { coreMock } from 'src/core/public/mocks';
const mockes = coreMock.createSetup();

jest.mock('../../../app_context', () => ({
useAppDependencies: jest.fn(() => ({
http: jest.fn(),
legacy: {
capabilities: {
get: jest.fn(() => ({})),
},
capabilities: {
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', () => ({
hasSaveAlertsCapability: jest.fn(() => true),
}));
Expand Down Expand Up @@ -232,6 +263,28 @@ describe('alert_details', () => {
).containsMatchingElement(<ViewInApp alert={alert} />)
).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();
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* 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 { useHistory } from 'react-router-dom';
import {
EuiPageBody,
EuiPageContent,
Expand All @@ -21,6 +22,7 @@ import {
EuiCallOut,
EuiSpacer,
EuiBetaBadge,
EuiButtonEmpty,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
Expand All @@ -34,6 +36,9 @@ import {
import { AlertInstancesRouteWithApi } from './alert_instances_route';
import { ViewInApp } from './view_in_app';
import { PLUGIN } from '../../../constants/plugin';
import { AlertEdit } from '../../alert_form';
import { AlertsContextProvider } from '../../../context/alerts_context';
import { routeToAlertDetails } from '../../../constants';

type AlertDetailsProps = {
alert: Alert;
Expand All @@ -52,7 +57,18 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
muteAlert,
requestRefresh,
}) => {
const { capabilities } = useAppDependencies();
const history = useHistory();
const {
http,
toastNotifications,
capabilities,
alertTypeRegistry,
actionTypeRegistry,
uiSettings,
docLinks,
charts,
dataPlugin,
} = useAppDependencies();

const canSave = hasSaveAlertsCapability(capabilities);

Expand All @@ -61,6 +77,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({

const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled);
const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);

const setAlert = async () => {
history.push(routeToAlertDetails.replace(`:alertId`, alert.id));
};

return (
<EuiPage>
Expand Down Expand Up @@ -90,6 +111,42 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<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}>
<ViewInApp alert={alert} />
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ export const AlertInstancesRoute: React.FunctionComponent<WithAlertStateProps> =
requestRefresh,
loadAlertState,
}) => {
const { http, toastNotifications } = useAppDependencies();
const { toastNotifications } = useAppDependencies();

const [alertState, setAlertState] = useState<AlertTaskState | null>(null);

useEffect(() => {
getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications);
}, [alert, http, loadAlertState, toastNotifications]);
}, [alert, loadAlertState, toastNotifications]);

return alertState ? (
<AlertInstances requestRefresh={requestRefresh} alert={alert} alertState={alertState} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const log = getService('log');
const alerting = getService('alerting');
const retry = getService('retry');
const find = getService('find');

describe('Alert Details', function() {
describe('Header', function() {
Expand Down Expand Up @@ -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() {
const testRunUuid = uuid.v4();

Expand Down
38 changes: 38 additions & 0 deletions x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
this.log.debug(`creating alert ${name}`);

Expand Down