mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
Adds a list of Alert Instances to the Alert Details page based off of the current state of the Alert. Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
7339d02ccb
commit
9017c99bbd
27 changed files with 1394 additions and 176 deletions
|
@ -5,12 +5,13 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectAttributes } from 'kibana/server';
|
||||
import { AlertActionParams } from '../server/types';
|
||||
|
||||
export interface IntervalSchedule extends SavedObjectAttributes {
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export type AlertActionParams = SavedObjectAttributes;
|
||||
|
||||
export interface AlertAction {
|
||||
group: string;
|
||||
id: string;
|
24
x-pack/legacy/plugins/alerting/common/alert_instance.ts
Normal file
24
x-pack/legacy/plugins/alerting/common/alert_instance.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { DateFromString } from './date_from_string';
|
||||
|
||||
const metaSchema = t.partial({
|
||||
lastScheduledActions: t.type({
|
||||
group: t.string,
|
||||
date: DateFromString,
|
||||
}),
|
||||
});
|
||||
export type AlertInstanceMeta = t.TypeOf<typeof metaSchema>;
|
||||
|
||||
const stateSchema = t.record(t.string, t.unknown);
|
||||
export type AlertInstanceState = t.TypeOf<typeof stateSchema>;
|
||||
|
||||
export const rawAlertInstance = t.partial({
|
||||
state: stateSchema,
|
||||
meta: metaSchema,
|
||||
});
|
||||
export type RawAlertInstance = t.TypeOf<typeof rawAlertInstance>;
|
26
x-pack/legacy/plugins/alerting/common/alert_task_instance.ts
Normal file
26
x-pack/legacy/plugins/alerting/common/alert_task_instance.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { rawAlertInstance } from './alert_instance';
|
||||
import { DateFromString } from './date_from_string';
|
||||
|
||||
export const alertStateSchema = t.partial({
|
||||
alertTypeState: t.record(t.string, t.unknown),
|
||||
alertInstances: t.record(t.string, rawAlertInstance),
|
||||
previousStartedAt: t.union([t.null, DateFromString]),
|
||||
});
|
||||
|
||||
export type AlertTaskState = t.TypeOf<typeof alertStateSchema>;
|
||||
|
||||
export const alertParamsSchema = t.intersection([
|
||||
t.type({
|
||||
alertId: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
spaceId: t.string,
|
||||
}),
|
||||
]);
|
||||
export type AlertTaskParams = t.TypeOf<typeof alertParamsSchema>;
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { DateFromString } from './date_from_string';
|
||||
import { right, isLeft } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('DateFromString', () => {
|
||||
test('validated and parses a string into a Date', () => {
|
||||
const date = new Date(1973, 10, 30);
|
||||
expect(DateFromString.decode(date.toISOString())).toEqual(right(date));
|
||||
});
|
||||
|
||||
test('validated and returns a failure for an actual Date', () => {
|
||||
const date = new Date(1973, 10, 30);
|
||||
expect(isLeft(DateFromString.decode(date))).toEqual(true);
|
||||
});
|
||||
|
||||
test('validated and returns a failure for an invalid Date string', () => {
|
||||
expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true);
|
||||
});
|
||||
|
||||
test('validated and returns a failure for a null value', () => {
|
||||
expect(isLeft(DateFromString.decode(null))).toEqual(true);
|
||||
});
|
||||
});
|
26
x-pack/legacy/plugins/alerting/common/date_from_string.ts
Normal file
26
x-pack/legacy/plugins/alerting/common/date_from_string.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
import { either } from 'fp-ts/lib/Either';
|
||||
|
||||
// represents a Date from an ISO string
|
||||
export const DateFromString = new t.Type<Date, string, unknown>(
|
||||
'DateFromString',
|
||||
// detect the type
|
||||
(value): value is Date => value instanceof Date,
|
||||
(valueToDecode, context) =>
|
||||
either.chain(
|
||||
// validate this is a string
|
||||
t.string.validate(valueToDecode, context),
|
||||
// decode
|
||||
value => {
|
||||
const decoded = new Date(value);
|
||||
return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded);
|
||||
}
|
||||
),
|
||||
valueToEncode => valueToEncode.toISOString()
|
||||
);
|
|
@ -4,4 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './alert';
|
||||
export * from './alert_instance';
|
||||
export * from './alert_task_instance';
|
||||
|
|
|
@ -3,10 +3,14 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import {
|
||||
AlertInstanceMeta,
|
||||
AlertInstanceState,
|
||||
RawAlertInstance,
|
||||
rawAlertInstance,
|
||||
} from '../../common';
|
||||
|
||||
import { State, Context } from '../types';
|
||||
import { DateFromString } from '../lib/types';
|
||||
import { parseDuration } from '../lib';
|
||||
|
||||
interface ScheduledExecutionOptions {
|
||||
|
@ -14,24 +18,7 @@ interface ScheduledExecutionOptions {
|
|||
context: Context;
|
||||
state: State;
|
||||
}
|
||||
|
||||
const metaSchema = t.partial({
|
||||
lastScheduledActions: t.type({
|
||||
group: t.string,
|
||||
date: DateFromString,
|
||||
}),
|
||||
});
|
||||
type AlertInstanceMeta = t.TypeOf<typeof metaSchema>;
|
||||
|
||||
const stateSchema = t.record(t.string, t.unknown);
|
||||
type AlertInstanceState = t.TypeOf<typeof stateSchema>;
|
||||
|
||||
export const rawAlertInstance = t.partial({
|
||||
state: stateSchema,
|
||||
meta: metaSchema,
|
||||
});
|
||||
export type RawAlertInstance = t.TypeOf<typeof rawAlertInstance>;
|
||||
|
||||
export type AlertInstances = Record<string, AlertInstance>;
|
||||
export class AlertInstance {
|
||||
private scheduledExecutionOptions?: ScheduledExecutionOptions;
|
||||
private meta: AlertInstanceMeta;
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { AlertInstance, RawAlertInstance, rawAlertInstance } from './alert_instance';
|
||||
export { AlertInstance } from './alert_instance';
|
||||
export { createAlertInstanceFactory } from './create_alert_instance_factory';
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
AlertType,
|
||||
IntervalSchedule,
|
||||
SanitizedAlert,
|
||||
AlertTaskState,
|
||||
} from './types';
|
||||
import { validateAlertTypeParams } from './lib';
|
||||
import {
|
||||
|
@ -31,7 +32,7 @@ import {
|
|||
} from '../../../../plugins/security/server';
|
||||
import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server';
|
||||
import { TaskManagerStartContract } from '../../../../plugins/task_manager/server';
|
||||
import { AlertTaskState, taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance';
|
||||
import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance';
|
||||
|
||||
type NormalizedAlertAction = Omit<AlertAction, 'actionTypeId'>;
|
||||
export type CreateAPIKeyResult =
|
||||
|
|
|
@ -7,32 +7,12 @@ import * as t from 'io-ts';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
|
||||
import { SanitizedAlert } from '../types';
|
||||
import { DateFromString } from '../lib/types';
|
||||
import { AlertInstance, rawAlertInstance } from '../alert_instance';
|
||||
import { SanitizedAlert, AlertTaskState, alertParamsSchema, alertStateSchema } from '../../common';
|
||||
|
||||
export interface AlertTaskInstance extends ConcreteTaskInstance {
|
||||
state: AlertTaskState;
|
||||
}
|
||||
|
||||
export const alertStateSchema = t.partial({
|
||||
alertTypeState: t.record(t.string, t.unknown),
|
||||
alertInstances: t.record(t.string, rawAlertInstance),
|
||||
previousStartedAt: t.union([t.null, DateFromString]),
|
||||
});
|
||||
export type AlertInstances = Record<string, AlertInstance>;
|
||||
export type AlertTaskState = t.TypeOf<typeof alertStateSchema>;
|
||||
|
||||
const alertParamsSchema = t.intersection([
|
||||
t.type({
|
||||
alertId: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
spaceId: t.string,
|
||||
}),
|
||||
]);
|
||||
export type AlertTaskParams = t.TypeOf<typeof alertParamsSchema>;
|
||||
|
||||
const enumerateErrorFields = (e: t.Errors) =>
|
||||
`${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`;
|
||||
|
||||
|
|
|
@ -10,16 +10,21 @@ import { SavedObject } from '../../../../../../src/core/server';
|
|||
import { TaskRunnerContext } from './task_runner_factory';
|
||||
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
|
||||
import { createExecutionHandler } from './create_execution_handler';
|
||||
import { AlertInstance, createAlertInstanceFactory, RawAlertInstance } from '../alert_instance';
|
||||
import { AlertInstance, createAlertInstanceFactory } from '../alert_instance';
|
||||
import { getNextRunAt } from './get_next_run_at';
|
||||
import { validateAlertTypeParams } from '../lib';
|
||||
import { AlertType, RawAlert, IntervalSchedule, Services, AlertInfoParams } from '../types';
|
||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||
import {
|
||||
AlertType,
|
||||
RawAlert,
|
||||
IntervalSchedule,
|
||||
Services,
|
||||
AlertInfoParams,
|
||||
RawAlertInstance,
|
||||
AlertTaskState,
|
||||
AlertInstances,
|
||||
taskInstanceToAlertTaskInstance,
|
||||
} from './alert_task_instance';
|
||||
} from '../types';
|
||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
|
||||
import { AlertInstances } from '../alert_instance/alert_instance';
|
||||
|
||||
const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ import { AlertInstance } from './alert_instance';
|
|||
import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
|
||||
import { PluginSetupContract, PluginStartContract } from './plugin';
|
||||
import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
|
||||
import { Alert } from '../common';
|
||||
|
||||
import { Alert, AlertActionParams } from '../common';
|
||||
export * from '../common';
|
||||
|
||||
export type State = Record<string, any>;
|
||||
|
@ -53,8 +52,6 @@ export interface AlertType {
|
|||
executor: ({ services, params, state }: AlertExecutorOptions) => Promise<State | void>;
|
||||
}
|
||||
|
||||
export type AlertActionParams = SavedObjectAttributes;
|
||||
|
||||
export interface RawAlertAction extends SavedObjectAttributes {
|
||||
group: string;
|
||||
actionRef: string;
|
||||
|
|
|
@ -16,12 +16,15 @@ import {
|
|||
enableAlert,
|
||||
loadAlert,
|
||||
loadAlerts,
|
||||
loadAlertState,
|
||||
loadAlertTypes,
|
||||
muteAlerts,
|
||||
unmuteAlerts,
|
||||
muteAlert,
|
||||
unmuteAlert,
|
||||
updateAlert,
|
||||
muteAlertInstance,
|
||||
unmuteAlertInstance,
|
||||
} from './alert_api';
|
||||
import uuid from 'uuid';
|
||||
|
||||
|
@ -76,6 +79,70 @@ describe('loadAlert', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('loadAlertState', () => {
|
||||
test('should call get API with base parameters', async () => {
|
||||
const alertId = uuid.v4();
|
||||
const resolvedValue = {
|
||||
alertTypeState: {
|
||||
some: 'value',
|
||||
},
|
||||
alertInstances: {
|
||||
first_instance: {},
|
||||
second_instance: {},
|
||||
},
|
||||
};
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
expect(await loadAlertState({ http, alertId })).toEqual(resolvedValue);
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`);
|
||||
});
|
||||
|
||||
test('should parse AlertInstances', async () => {
|
||||
const alertId = uuid.v4();
|
||||
const resolvedValue = {
|
||||
alertTypeState: {
|
||||
some: 'value',
|
||||
},
|
||||
alertInstances: {
|
||||
first_instance: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'first_group',
|
||||
date: '2020-02-09T23:15:41.941Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
expect(await loadAlertState({ http, alertId })).toEqual({
|
||||
...resolvedValue,
|
||||
alertInstances: {
|
||||
first_instance: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'first_group',
|
||||
date: new Date('2020-02-09T23:15:41.941Z'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`);
|
||||
});
|
||||
|
||||
test('should handle empty response from api', async () => {
|
||||
const alertId = uuid.v4();
|
||||
http.get.mockResolvedValueOnce('');
|
||||
|
||||
expect(await loadAlertState({ http, alertId })).toEqual({});
|
||||
expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}/state`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAlerts', () => {
|
||||
test('should call find API with base parameters', async () => {
|
||||
const resolvedValue = {
|
||||
|
@ -410,6 +477,34 @@ describe('disableAlert', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('muteAlertInstance', () => {
|
||||
test('should call mute instance alert API', async () => {
|
||||
const result = await muteAlertInstance({ http, id: '1', instanceId: '123' });
|
||||
expect(result).toEqual(undefined);
|
||||
expect(http.post.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/api/alert/1/alert_instance/123/_mute",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unmuteAlertInstance', () => {
|
||||
test('should call mute instance alert API', async () => {
|
||||
const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' });
|
||||
expect(result).toEqual(undefined);
|
||||
expect(http.post.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/api/alert/1/alert_instance/123/_unmute",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('muteAlert', () => {
|
||||
test('should call mute alert API', async () => {
|
||||
const result = await muteAlert({ http, id: '1' });
|
||||
|
|
|
@ -5,8 +5,12 @@
|
|||
*/
|
||||
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import * as t from 'io-ts';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { BASE_ALERT_API_PATH } from '../constants';
|
||||
import { Alert, AlertType, AlertWithoutId } from '../../types';
|
||||
import { Alert, AlertType, AlertWithoutId, AlertTaskState } from '../../types';
|
||||
import { alertStateSchema } from '../../../../../legacy/plugins/alerting/common';
|
||||
|
||||
export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<AlertType[]> {
|
||||
return await http.get(`${BASE_ALERT_API_PATH}/types`);
|
||||
|
@ -22,6 +26,27 @@ export async function loadAlert({
|
|||
return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`);
|
||||
}
|
||||
|
||||
type EmptyHttpResponse = '';
|
||||
export async function loadAlertState({
|
||||
http,
|
||||
alertId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
alertId: string;
|
||||
}): Promise<AlertTaskState> {
|
||||
return await http
|
||||
.get(`${BASE_ALERT_API_PATH}/${alertId}/state`)
|
||||
.then((state: AlertTaskState | EmptyHttpResponse) => (state ? state : {}))
|
||||
.then((state: AlertTaskState) => {
|
||||
return pipe(
|
||||
alertStateSchema.decode(state),
|
||||
fold((e: t.Errors) => {
|
||||
throw new Error(`Alert "${alertId}" has invalid state`);
|
||||
}, t.identity)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadAlerts({
|
||||
http,
|
||||
page,
|
||||
|
@ -133,6 +158,30 @@ export async function disableAlerts({
|
|||
await Promise.all(ids.map(id => disableAlert({ id, http })));
|
||||
}
|
||||
|
||||
export async function muteAlertInstance({
|
||||
id,
|
||||
instanceId,
|
||||
http,
|
||||
}: {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
http: HttpSetup;
|
||||
}): Promise<void> {
|
||||
await http.post(`${BASE_ALERT_API_PATH}/${id}/alert_instance/${instanceId}/_mute`);
|
||||
}
|
||||
|
||||
export async function unmuteAlertInstance({
|
||||
id,
|
||||
instanceId,
|
||||
http,
|
||||
}: {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
http: HttpSetup;
|
||||
}): Promise<void> {
|
||||
await http.post(`${BASE_ALERT_API_PATH}/${id}/alert_instance/${instanceId}/_unmute`);
|
||||
}
|
||||
|
||||
export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
|
||||
await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ const mockAlertApis = {
|
|||
unmuteAlert: jest.fn(),
|
||||
enableAlert: jest.fn(),
|
||||
disableAlert: jest.fn(),
|
||||
requestRefresh: jest.fn(),
|
||||
};
|
||||
|
||||
// const AlertDetails = withBulkAlertOperations(RawAlertDetails);
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
EuiPageContentBody,
|
||||
EuiButtonEmpty,
|
||||
EuiSwitch,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
|
@ -28,11 +30,13 @@ import {
|
|||
ComponentOpts as BulkOperationsComponentOpts,
|
||||
withBulkAlertOperations,
|
||||
} from '../../common/components/with_bulk_alert_api_operations';
|
||||
import { AlertInstancesRouteWithApi } from './alert_instances_route';
|
||||
|
||||
type AlertDetailsProps = {
|
||||
alert: Alert;
|
||||
alertType: AlertType;
|
||||
actionTypes: ActionType[];
|
||||
requestRefresh: () => Promise<void>;
|
||||
} & Pick<BulkOperationsComponentOpts, 'disableAlert' | 'enableAlert' | 'unmuteAlert' | 'muteAlert'>;
|
||||
|
||||
export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||
|
@ -43,6 +47,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
|||
enableAlert,
|
||||
unmuteAlert,
|
||||
muteAlert,
|
||||
requestRefresh,
|
||||
}) => {
|
||||
const { capabilities } = useAppDependencies();
|
||||
|
||||
|
@ -131,10 +136,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
|||
setIsEnabled(true);
|
||||
await enableAlert(alert);
|
||||
}
|
||||
requestRefresh();
|
||||
}}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle"
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
}
|
||||
|
@ -154,10 +160,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
|||
setIsMuted(true);
|
||||
await muteAlert(alert);
|
||||
}
|
||||
requestRefresh();
|
||||
}}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle"
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle"
|
||||
defaultMessage="Mute"
|
||||
/>
|
||||
}
|
||||
|
@ -166,6 +173,23 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexItem>
|
||||
{alert.enabled ? (
|
||||
<AlertInstancesRouteWithApi requestRefresh={requestRefresh} alert={alert} />
|
||||
) : (
|
||||
<EuiCallOut title="Disabled Alert" color="warning" iconType="help">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert"
|
||||
defaultMessage="Disabled Alerts do not have an active state, hence Alert Instances cannot be displayed."
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
|
|
|
@ -41,7 +41,7 @@ export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps>
|
|||
const [alert, setAlert] = useState<Alert | null>(null);
|
||||
const [alertType, setAlertType] = useState<AlertType | null>(null);
|
||||
const [actionTypes, setActionTypes] = useState<ActionType[] | null>(null);
|
||||
|
||||
const [refreshToken, requestRefresh] = React.useState<number>();
|
||||
useEffect(() => {
|
||||
getAlertData(
|
||||
alertId,
|
||||
|
@ -53,10 +53,15 @@ export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps>
|
|||
setActionTypes,
|
||||
toastNotifications
|
||||
);
|
||||
}, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]);
|
||||
}, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications, refreshToken]);
|
||||
|
||||
return alert && alertType && actionTypes ? (
|
||||
<AlertDetails alert={alert} alertType={alertType} actionTypes={actionTypes} />
|
||||
<AlertDetails
|
||||
alert={alert}
|
||||
alertType={alertType}
|
||||
actionTypes={actionTypes}
|
||||
requestRefresh={async () => requestRefresh(Date.now())}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AlertInstances, AlertInstanceListItem, alertInstanceToListItem } from './alert_instances';
|
||||
import { Alert, AlertTaskState, RawAlertInstance } from '../../../../types';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
|
||||
const fakeNow = new Date('2020-02-09T23:15:41.941Z');
|
||||
const fake2MinutesAgo = new Date('2020-02-09T23:13:41.941Z');
|
||||
|
||||
const mockAPIs = {
|
||||
muteAlertInstance: jest.fn(),
|
||||
unmuteAlertInstance: jest.fn(),
|
||||
requestRefresh: jest.fn(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
jest.resetAllMocks();
|
||||
global.Date.now = jest.fn(() => fakeNow.getTime());
|
||||
});
|
||||
|
||||
jest.mock('../../../app_context', () => {
|
||||
const toastNotifications = jest.fn();
|
||||
return {
|
||||
useAppDependencies: jest.fn(() => ({ toastNotifications })),
|
||||
};
|
||||
});
|
||||
|
||||
describe('alert_instances', () => {
|
||||
it('render a list of alert instances', () => {
|
||||
const alert = mockAlert();
|
||||
|
||||
const alertState = mockAlertState();
|
||||
const instances: AlertInstanceListItem[] = [
|
||||
alertInstanceToListItem(alert, 'first_instance', alertState.alertInstances!.first_instance),
|
||||
alertInstanceToListItem(alert, 'second_instance', alertState.alertInstances!.second_instance),
|
||||
];
|
||||
|
||||
expect(
|
||||
shallow(<AlertInstances {...mockAPIs} alert={alert} alertState={alertState} />)
|
||||
.find(EuiBasicTable)
|
||||
.prop('items')
|
||||
).toEqual(instances);
|
||||
});
|
||||
|
||||
it('render all active alert instances', () => {
|
||||
const alert = mockAlert();
|
||||
const instances = {
|
||||
['us-central']: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'warning',
|
||||
date: fake2MinutesAgo,
|
||||
},
|
||||
},
|
||||
},
|
||||
['us-east']: {},
|
||||
};
|
||||
expect(
|
||||
shallow(
|
||||
<AlertInstances
|
||||
{...mockAPIs}
|
||||
alert={alert}
|
||||
alertState={mockAlertState({
|
||||
alertInstances: instances,
|
||||
})}
|
||||
/>
|
||||
)
|
||||
.find(EuiBasicTable)
|
||||
.prop('items')
|
||||
).toEqual([
|
||||
alertInstanceToListItem(alert, 'us-central', instances['us-central']),
|
||||
alertInstanceToListItem(alert, 'us-east', instances['us-east']),
|
||||
]);
|
||||
});
|
||||
|
||||
it('render all inactive alert instances', () => {
|
||||
const alert = mockAlert({
|
||||
mutedInstanceIds: ['us-west', 'us-east'],
|
||||
});
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<AlertInstances
|
||||
{...mockAPIs}
|
||||
alert={alert}
|
||||
alertState={mockAlertState({
|
||||
alertInstances: {},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
.find(EuiBasicTable)
|
||||
.prop('items')
|
||||
).toEqual([
|
||||
alertInstanceToListItem(alert, 'us-west'),
|
||||
alertInstanceToListItem(alert, 'us-east'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alertInstanceToListItem', () => {
|
||||
it('handles active instances', () => {
|
||||
const alert = mockAlert();
|
||||
const start = fake2MinutesAgo;
|
||||
const instance: RawAlertInstance = {
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: start,
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({
|
||||
instance: 'id',
|
||||
status: { label: 'Active', healthColor: 'primary' },
|
||||
start,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
isMuted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles active muted instances', () => {
|
||||
const alert = mockAlert({
|
||||
mutedInstanceIds: ['id'],
|
||||
});
|
||||
const start = fake2MinutesAgo;
|
||||
const instance: RawAlertInstance = {
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
date: start,
|
||||
group: 'default',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({
|
||||
instance: 'id',
|
||||
status: { label: 'Active', healthColor: 'primary' },
|
||||
start,
|
||||
duration: fakeNow.getTime() - fake2MinutesAgo.getTime(),
|
||||
isMuted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles active instances with no meta', () => {
|
||||
const alert = mockAlert();
|
||||
const instance: RawAlertInstance = {};
|
||||
|
||||
expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({
|
||||
instance: 'id',
|
||||
status: { label: 'Active', healthColor: 'primary' },
|
||||
start: undefined,
|
||||
duration: 0,
|
||||
isMuted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles active instances with no lastScheduledActions', () => {
|
||||
const alert = mockAlert();
|
||||
const instance: RawAlertInstance = {
|
||||
meta: {},
|
||||
};
|
||||
|
||||
expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({
|
||||
instance: 'id',
|
||||
status: { label: 'Active', healthColor: 'primary' },
|
||||
start: undefined,
|
||||
duration: 0,
|
||||
isMuted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles muted inactive instances', () => {
|
||||
const alert = mockAlert({
|
||||
mutedInstanceIds: ['id'],
|
||||
});
|
||||
expect(alertInstanceToListItem(alert, 'id')).toEqual({
|
||||
instance: 'id',
|
||||
status: { label: 'Inactive', healthColor: 'subdued' },
|
||||
start: undefined,
|
||||
duration: 0,
|
||||
isMuted: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `alert-${uuid.v4()}`,
|
||||
tags: [],
|
||||
alertTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
...overloads,
|
||||
};
|
||||
}
|
||||
|
||||
function mockAlertState(overloads: Partial<any> = {}): AlertTaskState {
|
||||
return {
|
||||
alertTypeState: {
|
||||
some: 'value',
|
||||
},
|
||||
alertInstances: {
|
||||
first_instance: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'first_group',
|
||||
date: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
second_instance: {},
|
||||
},
|
||||
...overloads,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import moment, { Duration } from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTable, EuiButtonToggle, EuiBadge, EuiHealth } from '@elastic/eui';
|
||||
// @ts-ignore
|
||||
import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||
import { padLeft, difference } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { RawAlertInstance } from '../../../../../../../legacy/plugins/alerting/common';
|
||||
import { Alert, AlertTaskState } from '../../../../types';
|
||||
import {
|
||||
ComponentOpts as AlertApis,
|
||||
withBulkAlertOperations,
|
||||
} from '../../common/components/with_bulk_alert_api_operations';
|
||||
|
||||
type AlertInstancesProps = {
|
||||
alert: Alert;
|
||||
alertState: AlertTaskState;
|
||||
requestRefresh: () => Promise<void>;
|
||||
} & Pick<AlertApis, 'muteAlertInstance' | 'unmuteAlertInstance'>;
|
||||
|
||||
export const alertInstancesTableColumns = (
|
||||
onMuteAction: (instance: AlertInstanceListItem) => Promise<void>
|
||||
) => [
|
||||
{
|
||||
field: 'instance',
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance',
|
||||
{ defaultMessage: 'Instance' }
|
||||
),
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
'data-test-subj': 'alertInstancesTableCell-instance',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.status',
|
||||
{ defaultMessage: 'Status' }
|
||||
),
|
||||
render: (value: AlertInstanceListItemStatus, instance: AlertInstanceListItem) => {
|
||||
return <EuiHealth color={value.healthColor}>{value.label}</EuiHealth>;
|
||||
},
|
||||
sortable: false,
|
||||
'data-test-subj': 'alertInstancesTableCell-status',
|
||||
},
|
||||
{
|
||||
field: 'start',
|
||||
render: (value: Date | undefined, instance: AlertInstanceListItem) => {
|
||||
return value ? moment(value).format('D MMM YYYY @ HH:mm:ss') : '';
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.start',
|
||||
{ defaultMessage: 'Start' }
|
||||
),
|
||||
sortable: false,
|
||||
'data-test-subj': 'alertInstancesTableCell-start',
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
align: CENTER_ALIGNMENT,
|
||||
render: (value: number, instance: AlertInstanceListItem) => {
|
||||
return value ? durationAsString(moment.duration(value)) : '';
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration',
|
||||
{ defaultMessage: 'Duration' }
|
||||
),
|
||||
sortable: false,
|
||||
'data-test-subj': 'alertInstancesTableCell-duration',
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.actions',
|
||||
{ defaultMessage: 'Actions' }
|
||||
),
|
||||
render: (alertInstance: AlertInstanceListItem) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{alertInstance.isMuted ? (
|
||||
<EuiBadge data-test-subj={`mutedAlertInstanceLabel_${alertInstance.instance}`}>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertDetails.alertInstances.mutedAlert"
|
||||
defaultMessage="Muted"
|
||||
/>
|
||||
</EuiBadge>
|
||||
) : (
|
||||
<Fragment />
|
||||
)}
|
||||
<EuiButtonToggle
|
||||
label={
|
||||
alertInstance.isMuted
|
||||
? i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.actions.unmute',
|
||||
{ defaultMessage: 'Unmute' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.actions.mute',
|
||||
{ defaultMessage: 'Mute' }
|
||||
)
|
||||
}
|
||||
data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`}
|
||||
iconType={alertInstance.isMuted ? 'eyeClosed' : 'eye'}
|
||||
onChange={() => onMuteAction(alertInstance)}
|
||||
isSelected={alertInstance.isMuted}
|
||||
isEmpty
|
||||
isIconOnly
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
},
|
||||
sortable: false,
|
||||
'data-test-subj': 'alertInstancesTableCell-actions',
|
||||
},
|
||||
];
|
||||
|
||||
function durationAsString(duration: Duration): string {
|
||||
return [duration.hours(), duration.minutes(), duration.seconds()]
|
||||
.map(value => padLeft(`${value}`, 2, '0'))
|
||||
.join(':');
|
||||
}
|
||||
|
||||
export function AlertInstances({
|
||||
alert,
|
||||
alertState: { alertInstances = {} },
|
||||
muteAlertInstance,
|
||||
unmuteAlertInstance,
|
||||
requestRefresh,
|
||||
}: AlertInstancesProps) {
|
||||
const onMuteAction = async (instance: AlertInstanceListItem) => {
|
||||
await (instance.isMuted
|
||||
? unmuteAlertInstance(alert, instance.instance)
|
||||
: muteAlertInstance(alert, instance.instance));
|
||||
requestRefresh();
|
||||
};
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={[
|
||||
...Object.entries(alertInstances).map(([instanceId, instance]) =>
|
||||
alertInstanceToListItem(alert, instanceId, instance)
|
||||
),
|
||||
...difference(alert.mutedInstanceIds, Object.keys(alertInstances)).map(instanceId =>
|
||||
alertInstanceToListItem(alert, instanceId)
|
||||
),
|
||||
]}
|
||||
rowProps={() => ({
|
||||
'data-test-subj': 'alert-instance-row',
|
||||
})}
|
||||
cellProps={() => ({
|
||||
'data-test-subj': 'cell',
|
||||
})}
|
||||
columns={alertInstancesTableColumns(onMuteAction)}
|
||||
data-test-subj="alertInstancesList"
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const AlertInstancesWithApi = withBulkAlertOperations(AlertInstances);
|
||||
|
||||
interface AlertInstanceListItemStatus {
|
||||
label: string;
|
||||
healthColor: string;
|
||||
}
|
||||
export interface AlertInstanceListItem {
|
||||
instance: string;
|
||||
status: AlertInstanceListItemStatus;
|
||||
start?: Date;
|
||||
duration: number;
|
||||
isMuted: boolean;
|
||||
}
|
||||
|
||||
const ACTIVE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.active',
|
||||
{ defaultMessage: 'Active' }
|
||||
);
|
||||
|
||||
const INACTIVE_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.status.inactive',
|
||||
{ defaultMessage: 'Inactive' }
|
||||
);
|
||||
|
||||
const durationSince = (start?: Date) => (start ? Date.now() - start.getTime() : 0);
|
||||
|
||||
export function alertInstanceToListItem(
|
||||
alert: Alert,
|
||||
instanceId: string,
|
||||
instance?: RawAlertInstance
|
||||
): AlertInstanceListItem {
|
||||
const isMuted = alert.mutedInstanceIds.findIndex(muted => muted === instanceId) >= 0;
|
||||
return {
|
||||
instance: instanceId,
|
||||
status: instance
|
||||
? { label: ACTIVE_LABEL, healthColor: 'primary' }
|
||||
: { label: INACTIVE_LABEL, healthColor: 'subdued' },
|
||||
start: instance?.meta?.lastScheduledActions?.date,
|
||||
duration: durationSince(instance?.meta?.lastScheduledActions?.date),
|
||||
isMuted,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ToastsApi } from 'kibana/public';
|
||||
import { AlertInstancesRoute, getAlertState } from './alert_instances_route';
|
||||
import { Alert } from '../../../../types';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
jest.mock('../../../app_context', () => {
|
||||
const toastNotifications = jest.fn();
|
||||
return {
|
||||
useAppDependencies: jest.fn(() => ({ toastNotifications })),
|
||||
};
|
||||
});
|
||||
describe('alert_state_route', () => {
|
||||
it('render a loader while fetching data', () => {
|
||||
const alert = mockAlert();
|
||||
|
||||
expect(
|
||||
shallow(<AlertInstancesRoute alert={alert} {...mockApis()} />).containsMatchingElement(
|
||||
<EuiLoadingSpinner size="l" />
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlertState useEffect handler', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches alert state', async () => {
|
||||
const alert = mockAlert();
|
||||
const alertState = mockAlertState();
|
||||
const { loadAlertState } = mockApis();
|
||||
const { setAlertState } = mockStateSetter();
|
||||
|
||||
loadAlertState.mockImplementationOnce(async () => alertState);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
|
||||
await getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications);
|
||||
|
||||
expect(loadAlertState).toHaveBeenCalledWith(alert.id);
|
||||
expect(setAlertState).toHaveBeenCalledWith(alertState);
|
||||
});
|
||||
|
||||
it('displays an error if the alert state isnt found', async () => {
|
||||
const actionType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: actionType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { loadAlertState } = mockApis();
|
||||
const { setAlertState } = mockStateSetter();
|
||||
|
||||
loadAlertState.mockImplementation(async () => {
|
||||
throw new Error('OMG');
|
||||
});
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledWith({
|
||||
title: 'Unable to load alert state: OMG',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockApis() {
|
||||
return {
|
||||
loadAlertState: jest.fn(),
|
||||
requestRefresh: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function mockStateSetter() {
|
||||
return {
|
||||
setAlertState: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `alert-${uuid.v4()}`,
|
||||
tags: [],
|
||||
alertTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
...overloads,
|
||||
};
|
||||
}
|
||||
|
||||
function mockAlertState(overloads: Partial<any> = {}): any {
|
||||
return {
|
||||
alertTypeState: {
|
||||
some: 'value',
|
||||
},
|
||||
alertInstances: {
|
||||
first_instance: {
|
||||
state: {},
|
||||
meta: {
|
||||
lastScheduledActions: {
|
||||
group: 'first_group',
|
||||
date: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
second_instance: {},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ToastsApi } from 'kibana/public';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { Alert, AlertTaskState } from '../../../../types';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
import {
|
||||
ComponentOpts as AlertApis,
|
||||
withBulkAlertOperations,
|
||||
} from '../../common/components/with_bulk_alert_api_operations';
|
||||
import { AlertInstancesWithApi as AlertInstances } from './alert_instances';
|
||||
|
||||
type WithAlertStateProps = {
|
||||
alert: Alert;
|
||||
requestRefresh: () => Promise<void>;
|
||||
} & Pick<AlertApis, 'loadAlertState'>;
|
||||
|
||||
export const AlertInstancesRoute: React.FunctionComponent<WithAlertStateProps> = ({
|
||||
alert,
|
||||
requestRefresh,
|
||||
loadAlertState,
|
||||
}) => {
|
||||
const { http, toastNotifications } = useAppDependencies();
|
||||
|
||||
const [alertState, setAlertState] = useState<AlertTaskState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications);
|
||||
}, [alert, http, loadAlertState, toastNotifications]);
|
||||
|
||||
return alertState ? (
|
||||
<AlertInstances requestRefresh={requestRefresh} alert={alert} alertState={alertState} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
margin: '4em 0em',
|
||||
}}
|
||||
>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getAlertState(
|
||||
alertId: string,
|
||||
loadAlertState: AlertApis['loadAlertState'],
|
||||
setAlertState: React.Dispatch<React.SetStateAction<AlertTaskState | null>>,
|
||||
toastNotifications: Pick<ToastsApi, 'addDanger'>
|
||||
) {
|
||||
try {
|
||||
const loadedState = await loadAlertState(alertId);
|
||||
setAlertState(loadedState);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertStateMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to load alert state: {message}',
|
||||
values: {
|
||||
message: e.message,
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const AlertInstancesRouteWithApi = withBulkAlertOperations(AlertInstancesRoute);
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, AlertType } from '../../../../types';
|
||||
import { Alert, AlertType, AlertTaskState } from '../../../../types';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
import {
|
||||
deleteAlerts,
|
||||
|
@ -19,7 +19,10 @@ import {
|
|||
enableAlert,
|
||||
muteAlert,
|
||||
unmuteAlert,
|
||||
muteAlertInstance,
|
||||
unmuteAlertInstance,
|
||||
loadAlert,
|
||||
loadAlertState,
|
||||
loadAlertTypes,
|
||||
} from '../../../lib/alert_api';
|
||||
|
||||
|
@ -31,10 +34,13 @@ export interface ComponentOpts {
|
|||
deleteAlerts: (alerts: Alert[]) => Promise<void>;
|
||||
muteAlert: (alert: Alert) => Promise<void>;
|
||||
unmuteAlert: (alert: Alert) => Promise<void>;
|
||||
muteAlertInstance: (alert: Alert, alertInstanceId: string) => Promise<void>;
|
||||
unmuteAlertInstance: (alert: Alert, alertInstanceId: string) => Promise<void>;
|
||||
enableAlert: (alert: Alert) => Promise<void>;
|
||||
disableAlert: (alert: Alert) => Promise<void>;
|
||||
deleteAlert: (alert: Alert) => Promise<void>;
|
||||
loadAlert: (id: Alert['id']) => Promise<Alert>;
|
||||
loadAlertState: (id: Alert['id']) => Promise<AlertTaskState>;
|
||||
loadAlertTypes: () => Promise<AlertType[]>;
|
||||
}
|
||||
|
||||
|
@ -76,6 +82,16 @@ export function withBulkAlertOperations<T>(
|
|||
return unmuteAlert({ http, id: alert.id });
|
||||
}
|
||||
}}
|
||||
muteAlertInstance={async (alert: Alert, instanceId: string) => {
|
||||
if (!isAlertInstanceMuted(alert, instanceId)) {
|
||||
return muteAlertInstance({ http, id: alert.id, instanceId });
|
||||
}
|
||||
}}
|
||||
unmuteAlertInstance={async (alert: Alert, instanceId: string) => {
|
||||
if (isAlertInstanceMuted(alert, instanceId)) {
|
||||
return unmuteAlertInstance({ http, id: alert.id, instanceId });
|
||||
}
|
||||
}}
|
||||
enableAlert={async (alert: Alert) => {
|
||||
if (isAlertDisabled(alert)) {
|
||||
return enableAlert({ http, id: alert.id });
|
||||
|
@ -88,6 +104,7 @@ export function withBulkAlertOperations<T>(
|
|||
}}
|
||||
deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })}
|
||||
loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })}
|
||||
loadAlertState={async (alertId: Alert['id']) => loadAlertState({ http, alertId })}
|
||||
loadAlertTypes={async () => loadAlertTypes({ http })}
|
||||
/>
|
||||
);
|
||||
|
@ -101,3 +118,7 @@ function isAlertDisabled(alert: Alert) {
|
|||
function isAlertMuted(alert: Alert) {
|
||||
return alert.muteAll === true;
|
||||
}
|
||||
|
||||
function isAlertInstanceMuted(alert: Alert, instanceId: string) {
|
||||
return alert.mutedInstanceIds.findIndex(muted => muted === instanceId) >= 0;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,13 @@
|
|||
*/
|
||||
import { ActionType } from '../../actions/common';
|
||||
import { TypeRegistry } from './application/type_registry';
|
||||
import { SanitizedAlert as Alert, AlertAction } from '../../../legacy/plugins/alerting/common';
|
||||
export { Alert, AlertAction };
|
||||
import {
|
||||
SanitizedAlert as Alert,
|
||||
AlertAction,
|
||||
AlertTaskState,
|
||||
RawAlertInstance,
|
||||
} from '../../../legacy/plugins/alerting/common';
|
||||
export { Alert, AlertAction, AlertTaskState, RawAlertInstance };
|
||||
export { ActionType };
|
||||
|
||||
export type ActionTypeIndex = Record<string, ActionType>;
|
||||
|
|
|
@ -6,141 +6,328 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import uuid from 'uuid';
|
||||
import { omit } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']);
|
||||
const browser = getService('browser');
|
||||
const log = getService('log');
|
||||
const alerting = getService('alerting');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('Alert Details', function() {
|
||||
const testRunUuid = uuid.v4();
|
||||
describe('Header', function() {
|
||||
const testRunUuid = uuid.v4();
|
||||
before(async () => {
|
||||
await pageObjects.common.navigateToApp('triggersActions');
|
||||
|
||||
before(async () => {
|
||||
await pageObjects.common.navigateToApp('triggersActions');
|
||||
const actions = await Promise.all([
|
||||
alerting.actions.createAction({
|
||||
name: `server-log-${testRunUuid}-${0}`,
|
||||
actionTypeId: '.server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
}),
|
||||
alerting.actions.createAction({
|
||||
name: `server-log-${testRunUuid}-${1}`,
|
||||
actionTypeId: '.server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
}),
|
||||
]);
|
||||
|
||||
const actions = await Promise.all([
|
||||
alerting.actions.createAction({
|
||||
name: `server-log-${testRunUuid}-${0}`,
|
||||
actionTypeId: '.server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
}),
|
||||
alerting.actions.createAction({
|
||||
name: `server-log-${testRunUuid}-${1}`,
|
||||
actionTypeId: '.server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
}),
|
||||
]);
|
||||
const alert = await alerting.alerts.createAlwaysFiringWithActions(
|
||||
`test-alert-${testRunUuid}`,
|
||||
actions.map(action => ({
|
||||
id: action.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'from alert 1s',
|
||||
level: 'warn',
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
const alert = await alerting.alerts.createAlwaysFiringWithActions(
|
||||
`test-alert-${testRunUuid}`,
|
||||
actions.map(action => ({
|
||||
id: action.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'from alert 1s',
|
||||
level: 'warn',
|
||||
// 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);
|
||||
});
|
||||
|
||||
it('renders the alert details', async () => {
|
||||
const headingText = await pageObjects.alertDetailsUI.getHeadingText();
|
||||
expect(headingText).to.be(`test-alert-${testRunUuid}`);
|
||||
|
||||
const alertType = await pageObjects.alertDetailsUI.getAlertType();
|
||||
expect(alertType).to.be(`Always Firing`);
|
||||
|
||||
const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels();
|
||||
expect(actionType).to.be(`Server log`);
|
||||
expect(actionCount).to.be(`+1`);
|
||||
});
|
||||
|
||||
it('should disable the alert', async () => {
|
||||
const enableSwitch = await testSubjects.find('enableSwitch');
|
||||
|
||||
const isChecked = await enableSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('true');
|
||||
|
||||
await enableSwitch.click();
|
||||
|
||||
const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch');
|
||||
const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute(
|
||||
'aria-checked'
|
||||
);
|
||||
expect(isCheckedAfterDisabling).to.eql('false');
|
||||
});
|
||||
|
||||
it('shouldnt allow you to mute a disabled alert', async () => {
|
||||
const disabledEnableSwitch = await testSubjects.find('enableSwitch');
|
||||
expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false');
|
||||
|
||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
||||
expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false');
|
||||
|
||||
await muteSwitch.click();
|
||||
|
||||
const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch');
|
||||
const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute(
|
||||
'aria-checked'
|
||||
);
|
||||
expect(isDisabledMuteAfterDisabling).to.eql('false');
|
||||
});
|
||||
|
||||
it('should reenable a disabled the alert', async () => {
|
||||
const enableSwitch = await testSubjects.find('enableSwitch');
|
||||
|
||||
const isChecked = await enableSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('false');
|
||||
|
||||
await enableSwitch.click();
|
||||
|
||||
const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch');
|
||||
const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute(
|
||||
'aria-checked'
|
||||
);
|
||||
expect(isCheckedAfterDisabling).to.eql('true');
|
||||
});
|
||||
|
||||
it('should mute the alert', async () => {
|
||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
||||
|
||||
const isChecked = await muteSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('false');
|
||||
|
||||
await muteSwitch.click();
|
||||
|
||||
const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch');
|
||||
const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked');
|
||||
expect(isCheckedAfterDisabling).to.eql('true');
|
||||
});
|
||||
|
||||
it('should unmute the alert', async () => {
|
||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
||||
|
||||
const isChecked = await muteSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('true');
|
||||
|
||||
await muteSwitch.click();
|
||||
|
||||
const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch');
|
||||
const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked');
|
||||
expect(isCheckedAfterDisabling).to.eql('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alert Instances', function() {
|
||||
const testRunUuid = uuid.v4();
|
||||
let alert: any;
|
||||
|
||||
before(async () => {
|
||||
await pageObjects.common.navigateToApp('triggersActions');
|
||||
|
||||
const actions = await Promise.all([
|
||||
alerting.actions.createAction({
|
||||
name: `server-log-${testRunUuid}-${0}`,
|
||||
actionTypeId: '.server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
}),
|
||||
alerting.actions.createAction({
|
||||
name: `server-log-${testRunUuid}-${1}`,
|
||||
actionTypeId: '.server-log',
|
||||
config: {},
|
||||
secrets: {},
|
||||
}),
|
||||
]);
|
||||
|
||||
const instances = [{ id: 'us-central' }, { id: 'us-east' }, { id: 'us-west' }];
|
||||
alert = await alerting.alerts.createAlwaysFiringWithActions(
|
||||
`test-alert-${testRunUuid}`,
|
||||
actions.map(action => ({
|
||||
id: action.id,
|
||||
group: 'default',
|
||||
params: {
|
||||
message: 'from alert 1s',
|
||||
level: 'warn',
|
||||
},
|
||||
})),
|
||||
{
|
||||
instances,
|
||||
}
|
||||
);
|
||||
|
||||
// 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);
|
||||
|
||||
// await first run to complete so we have an initial state
|
||||
await retry.try(async () => {
|
||||
const { alertInstances } = await alerting.alerts.getAlertState(alert.id);
|
||||
expect(Object.keys(alertInstances).length).to.eql(instances.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the active alert instances', async () => {
|
||||
const testBeganAt = moment().utc();
|
||||
|
||||
// Verify content
|
||||
await testSubjects.existOrFail('alertInstancesList');
|
||||
|
||||
const {
|
||||
alertInstances: {
|
||||
['us-central']: {
|
||||
meta: {
|
||||
lastScheduledActions: { date },
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
);
|
||||
} = await alerting.alerts.getAlertState(alert.id);
|
||||
|
||||
// refresh to see alert
|
||||
await browser.refresh();
|
||||
const dateOnAllInstances = moment(date)
|
||||
.utc()
|
||||
.format('D MMM YYYY @ HH:mm:ss');
|
||||
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList();
|
||||
expect(instancesList.map(instance => omit(instance, 'duration'))).to.eql([
|
||||
{
|
||||
instance: 'us-central',
|
||||
status: 'Active',
|
||||
start: dateOnAllInstances,
|
||||
},
|
||||
{
|
||||
instance: 'us-east',
|
||||
status: 'Active',
|
||||
start: dateOnAllInstances,
|
||||
},
|
||||
{
|
||||
instance: 'us-west',
|
||||
status: 'Active',
|
||||
start: dateOnAllInstances,
|
||||
},
|
||||
]);
|
||||
|
||||
// Verify content
|
||||
await testSubjects.existOrFail('alertsList');
|
||||
const durationFromInstanceTillPageLoad = moment.duration(
|
||||
testBeganAt.diff(moment(date).utc())
|
||||
);
|
||||
instancesList
|
||||
.map(alertInstance => alertInstance.duration.split(':').map(part => parseInt(part, 10)))
|
||||
.map(([hours, minutes, seconds]) =>
|
||||
moment.duration({
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
})
|
||||
)
|
||||
.forEach(alertInstanceDuration => {
|
||||
// make sure the duration is within a 2 second range
|
||||
expect(alertInstanceDuration.as('milliseconds')).to.greaterThan(
|
||||
durationFromInstanceTillPageLoad.subtract(1000 * 2).as('milliseconds')
|
||||
);
|
||||
expect(alertInstanceDuration.as('milliseconds')).to.lessThan(
|
||||
durationFromInstanceTillPageLoad.add(1000 * 2).as('milliseconds')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// click on first alert
|
||||
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
|
||||
});
|
||||
it('renders the muted inactive alert instances', async () => {
|
||||
// mute an alert instance that doesn't exist
|
||||
await alerting.alerts.muteAlertInstance(alert.id, 'eu-east');
|
||||
|
||||
it('renders the alert details', async () => {
|
||||
const headingText = await pageObjects.alertDetailsUI.getHeadingText();
|
||||
expect(headingText).to.be(`test-alert-${testRunUuid}`);
|
||||
// refresh to see alert
|
||||
await browser.refresh();
|
||||
|
||||
const alertType = await pageObjects.alertDetailsUI.getAlertType();
|
||||
expect(alertType).to.be(`Always Firing`);
|
||||
const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList();
|
||||
expect(instancesList.filter(alertInstance => alertInstance.instance === 'eu-east')).to.eql([
|
||||
{
|
||||
instance: 'eu-east',
|
||||
status: 'Inactive',
|
||||
start: '',
|
||||
duration: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels();
|
||||
expect(actionType).to.be(`Server log`);
|
||||
expect(actionCount).to.be(`+1`);
|
||||
});
|
||||
it('allows the user to mute a specific instance', async () => {
|
||||
// Verify content
|
||||
await testSubjects.existOrFail('alertInstancesList');
|
||||
|
||||
it('should disable the alert', async () => {
|
||||
const enableSwitch = await testSubjects.find('enableSwitch');
|
||||
log.debug(`Ensuring us-central is not muted`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-central', false);
|
||||
|
||||
const isChecked = await enableSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('true');
|
||||
log.debug(`Muting us-central`);
|
||||
await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-central');
|
||||
|
||||
await enableSwitch.click();
|
||||
log.debug(`Ensuring us-central is muted`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-central', true);
|
||||
});
|
||||
|
||||
const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch');
|
||||
const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute(
|
||||
'aria-checked'
|
||||
);
|
||||
expect(isCheckedAfterDisabling).to.eql('false');
|
||||
});
|
||||
it('allows the user to unmute a specific instance', async () => {
|
||||
// Verify content
|
||||
await testSubjects.existOrFail('alertInstancesList');
|
||||
|
||||
it('shouldnt allow you to mute a disabled alert', async () => {
|
||||
const disabledEnableSwitch = await testSubjects.find('enableSwitch');
|
||||
expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false');
|
||||
log.debug(`Ensuring us-east is not muted`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', false);
|
||||
|
||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
||||
expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false');
|
||||
log.debug(`Muting us-east`);
|
||||
await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-east');
|
||||
|
||||
await muteSwitch.click();
|
||||
log.debug(`Ensuring us-east is muted`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', true);
|
||||
|
||||
const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch');
|
||||
const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute(
|
||||
'aria-checked'
|
||||
);
|
||||
expect(isDisabledMuteAfterDisabling).to.eql('false');
|
||||
});
|
||||
log.debug(`Unmuting us-east`);
|
||||
await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-east');
|
||||
|
||||
it('should reenable a disabled the alert', async () => {
|
||||
const enableSwitch = await testSubjects.find('enableSwitch');
|
||||
log.debug(`Ensuring us-east is not muted`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', false);
|
||||
});
|
||||
|
||||
const isChecked = await enableSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('false');
|
||||
it('allows the user unmute an inactive instance', async () => {
|
||||
log.debug(`Ensuring eu-east is muted`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('eu-east', true);
|
||||
|
||||
await enableSwitch.click();
|
||||
log.debug(`Unmuting eu-east`);
|
||||
await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('eu-east');
|
||||
|
||||
const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch');
|
||||
const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute(
|
||||
'aria-checked'
|
||||
);
|
||||
expect(isCheckedAfterDisabling).to.eql('true');
|
||||
});
|
||||
|
||||
it('should mute the alert', async () => {
|
||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
||||
|
||||
const isChecked = await muteSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('false');
|
||||
|
||||
await muteSwitch.click();
|
||||
|
||||
const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch');
|
||||
const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked');
|
||||
expect(isCheckedAfterDisabling).to.eql('true');
|
||||
});
|
||||
|
||||
it('should unmute the alert', async () => {
|
||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
||||
|
||||
const isChecked = await muteSwitch.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql('true');
|
||||
|
||||
await muteSwitch.click();
|
||||
|
||||
const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch');
|
||||
const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked');
|
||||
expect(isCheckedAfterDisabling).to.eql('false');
|
||||
log.debug(`Ensuring eu-east is removed from list`);
|
||||
await pageObjects.alertDetailsUI.ensureAlertInstanceExistance('eu-east', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -35,14 +35,15 @@ function createAlwaysFiringAlertType(setupContract: any) {
|
|||
name: 'Always Firing',
|
||||
actionGroups: ['default', 'other'],
|
||||
async executor(alertExecutorOptions: any) {
|
||||
const { services, state } = alertExecutorOptions;
|
||||
const { services, state, params } = alertExecutorOptions;
|
||||
|
||||
(params.instances || []).forEach((instance: { id: string; state: any }) => {
|
||||
services
|
||||
.alertInstanceFactory(instance.id)
|
||||
.replaceState({ instanceStateValue: true, ...(instance.state || {}) })
|
||||
.scheduleActions('default');
|
||||
});
|
||||
|
||||
services
|
||||
.alertInstanceFactory('1')
|
||||
.replaceState({ instanceStateValue: true })
|
||||
.scheduleActions('default', {
|
||||
instanceContextValue: true,
|
||||
});
|
||||
return {
|
||||
globalStateValue: true,
|
||||
groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1,
|
||||
|
|
|
@ -4,10 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
|
||||
return {
|
||||
async getHeadingText() {
|
||||
|
@ -22,5 +26,71 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
|
|||
actionCount: await testSubjects.getVisibleText('actionCountLabel'),
|
||||
};
|
||||
},
|
||||
async getAlertInstancesList() {
|
||||
const table = await find.byCssSelector(
|
||||
'.euiBasicTable[data-test-subj="alertInstancesList"]:not(.euiBasicTable-loading)'
|
||||
);
|
||||
const $ = await table.parseDomContent();
|
||||
return $.findTestSubjects('alert-instance-row')
|
||||
.toArray()
|
||||
.map(row => {
|
||||
return {
|
||||
instance: $(row)
|
||||
.findTestSubject('alertInstancesTableCell-instance')
|
||||
.find('.euiTableCellContent')
|
||||
.text(),
|
||||
status: $(row)
|
||||
.findTestSubject('alertInstancesTableCell-status')
|
||||
.find('.euiTableCellContent')
|
||||
.text(),
|
||||
start: $(row)
|
||||
.findTestSubject('alertInstancesTableCell-start')
|
||||
.find('.euiTableCellContent')
|
||||
.text(),
|
||||
duration: $(row)
|
||||
.findTestSubject('alertInstancesTableCell-duration')
|
||||
.find('.euiTableCellContent')
|
||||
.text(),
|
||||
};
|
||||
});
|
||||
},
|
||||
async clickAlertInstanceMuteButton(instance: string) {
|
||||
const muteAlertInstanceButton = await testSubjects.find(
|
||||
`muteAlertInstanceButton_${instance}`
|
||||
);
|
||||
await muteAlertInstanceButton.click();
|
||||
},
|
||||
async ensureAlertInstanceMute(instance: string, isMuted: boolean) {
|
||||
await retry.try(async () => {
|
||||
const muteAlertInstanceButton = await testSubjects.find(
|
||||
`muteAlertInstanceButton_${instance}`
|
||||
);
|
||||
log.debug(`checked:${await muteAlertInstanceButton.getAttribute('checked')}`);
|
||||
expect(await muteAlertInstanceButton.getAttribute('checked')).to.eql(
|
||||
isMuted ? 'true' : null
|
||||
);
|
||||
|
||||
expect(await testSubjects.exists(`mutedAlertInstanceLabel_${instance}`)).to.eql(isMuted);
|
||||
});
|
||||
},
|
||||
async ensureAlertInstanceExistance(instance: string, shouldExist: boolean) {
|
||||
await retry.try(async () => {
|
||||
const table = await find.byCssSelector(
|
||||
'.euiBasicTable[data-test-subj="alertInstancesList"]:not(.euiBasicTable-loading)'
|
||||
);
|
||||
const $ = await table.parseDomContent();
|
||||
expect(
|
||||
$.findTestSubjects('alert-instance-row')
|
||||
.toArray()
|
||||
.filter(
|
||||
row =>
|
||||
$(row)
|
||||
.findTestSubject('alertInstancesTableCell-instance')
|
||||
.find('.euiTableCellContent')
|
||||
.text() === instance
|
||||
)
|
||||
).to.eql(shouldExist ? 1 : 0);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@ export class Alerts {
|
|||
id: string;
|
||||
group: string;
|
||||
params: Record<string, any>;
|
||||
}>
|
||||
}>,
|
||||
params: Record<string, any> = {}
|
||||
) {
|
||||
this.log.debug(`creating alert ${name}`);
|
||||
|
||||
|
@ -41,7 +42,7 @@ export class Alerts {
|
|||
schedule: { interval: '1m' },
|
||||
throttle: '1m',
|
||||
actions,
|
||||
params: {},
|
||||
params,
|
||||
});
|
||||
if (status !== 200) {
|
||||
throw new Error(
|
||||
|
@ -76,4 +77,25 @@ export class Alerts {
|
|||
}
|
||||
this.log.debug(`deleted alert ${alert.id}`);
|
||||
}
|
||||
|
||||
public async getAlertState(id: string) {
|
||||
this.log.debug(`getting alert ${id} state`);
|
||||
|
||||
const { data } = await this.axios.get(`/api/alert/${id}/state`);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async muteAlertInstance(id: string, instanceId: string) {
|
||||
this.log.debug(`muting instance ${instanceId} under alert ${id}`);
|
||||
|
||||
const { data: alert, status, statusText } = await this.axios.post(
|
||||
`/api/alert/${id}/alert_instance/${instanceId}/_mute`
|
||||
);
|
||||
if (status !== 204) {
|
||||
throw new Error(
|
||||
`Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}`
|
||||
);
|
||||
}
|
||||
this.log.debug(`muted alert instance ${instanceId}`);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue