mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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 { SavedObjectAttributes } from 'kibana/server';
|
||||||
import { AlertActionParams } from '../server/types';
|
|
||||||
|
|
||||||
export interface IntervalSchedule extends SavedObjectAttributes {
|
export interface IntervalSchedule extends SavedObjectAttributes {
|
||||||
interval: string;
|
interval: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AlertActionParams = SavedObjectAttributes;
|
||||||
|
|
||||||
export interface AlertAction {
|
export interface AlertAction {
|
||||||
group: string;
|
group: string;
|
||||||
id: 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.
|
* 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;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import * as t from 'io-ts';
|
import {
|
||||||
|
AlertInstanceMeta,
|
||||||
|
AlertInstanceState,
|
||||||
|
RawAlertInstance,
|
||||||
|
rawAlertInstance,
|
||||||
|
} from '../../common';
|
||||||
|
|
||||||
import { State, Context } from '../types';
|
import { State, Context } from '../types';
|
||||||
import { DateFromString } from '../lib/types';
|
|
||||||
import { parseDuration } from '../lib';
|
import { parseDuration } from '../lib';
|
||||||
|
|
||||||
interface ScheduledExecutionOptions {
|
interface ScheduledExecutionOptions {
|
||||||
|
@ -14,24 +18,7 @@ interface ScheduledExecutionOptions {
|
||||||
context: Context;
|
context: Context;
|
||||||
state: State;
|
state: State;
|
||||||
}
|
}
|
||||||
|
export type AlertInstances = Record<string, AlertInstance>;
|
||||||
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 class AlertInstance {
|
export class AlertInstance {
|
||||||
private scheduledExecutionOptions?: ScheduledExecutionOptions;
|
private scheduledExecutionOptions?: ScheduledExecutionOptions;
|
||||||
private meta: AlertInstanceMeta;
|
private meta: AlertInstanceMeta;
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { AlertInstance, RawAlertInstance, rawAlertInstance } from './alert_instance';
|
export { AlertInstance } from './alert_instance';
|
||||||
export { createAlertInstanceFactory } from './create_alert_instance_factory';
|
export { createAlertInstanceFactory } from './create_alert_instance_factory';
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
AlertType,
|
AlertType,
|
||||||
IntervalSchedule,
|
IntervalSchedule,
|
||||||
SanitizedAlert,
|
SanitizedAlert,
|
||||||
|
AlertTaskState,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { validateAlertTypeParams } from './lib';
|
import { validateAlertTypeParams } from './lib';
|
||||||
import {
|
import {
|
||||||
|
@ -31,7 +32,7 @@ import {
|
||||||
} from '../../../../plugins/security/server';
|
} from '../../../../plugins/security/server';
|
||||||
import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server';
|
import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server';
|
||||||
import { TaskManagerStartContract } from '../../../../plugins/task_manager/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'>;
|
type NormalizedAlertAction = Omit<AlertAction, 'actionTypeId'>;
|
||||||
export type CreateAPIKeyResult =
|
export type CreateAPIKeyResult =
|
||||||
|
|
|
@ -7,32 +7,12 @@ import * as t from 'io-ts';
|
||||||
import { pipe } from 'fp-ts/lib/pipeable';
|
import { pipe } from 'fp-ts/lib/pipeable';
|
||||||
import { fold } from 'fp-ts/lib/Either';
|
import { fold } from 'fp-ts/lib/Either';
|
||||||
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
|
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
|
||||||
import { SanitizedAlert } from '../types';
|
import { SanitizedAlert, AlertTaskState, alertParamsSchema, alertStateSchema } from '../../common';
|
||||||
import { DateFromString } from '../lib/types';
|
|
||||||
import { AlertInstance, rawAlertInstance } from '../alert_instance';
|
|
||||||
|
|
||||||
export interface AlertTaskInstance extends ConcreteTaskInstance {
|
export interface AlertTaskInstance extends ConcreteTaskInstance {
|
||||||
state: AlertTaskState;
|
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) =>
|
const enumerateErrorFields = (e: t.Errors) =>
|
||||||
`${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`;
|
`${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 { TaskRunnerContext } from './task_runner_factory';
|
||||||
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
|
import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server';
|
||||||
import { createExecutionHandler } from './create_execution_handler';
|
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 { getNextRunAt } from './get_next_run_at';
|
||||||
import { validateAlertTypeParams } from '../lib';
|
import { validateAlertTypeParams } from '../lib';
|
||||||
import { AlertType, RawAlert, IntervalSchedule, Services, AlertInfoParams } from '../types';
|
|
||||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
|
||||||
import {
|
import {
|
||||||
|
AlertType,
|
||||||
|
RawAlert,
|
||||||
|
IntervalSchedule,
|
||||||
|
Services,
|
||||||
|
AlertInfoParams,
|
||||||
|
RawAlertInstance,
|
||||||
AlertTaskState,
|
AlertTaskState,
|
||||||
AlertInstances,
|
} from '../types';
|
||||||
taskInstanceToAlertTaskInstance,
|
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||||
} from './alert_task_instance';
|
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
|
||||||
|
import { AlertInstances } from '../alert_instance/alert_instance';
|
||||||
|
|
||||||
const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
|
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 { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
|
||||||
import { PluginSetupContract, PluginStartContract } from './plugin';
|
import { PluginSetupContract, PluginStartContract } from './plugin';
|
||||||
import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
|
import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
|
||||||
import { Alert } from '../common';
|
import { Alert, AlertActionParams } from '../common';
|
||||||
|
|
||||||
export * from '../common';
|
export * from '../common';
|
||||||
|
|
||||||
export type State = Record<string, any>;
|
export type State = Record<string, any>;
|
||||||
|
@ -53,8 +52,6 @@ export interface AlertType {
|
||||||
executor: ({ services, params, state }: AlertExecutorOptions) => Promise<State | void>;
|
executor: ({ services, params, state }: AlertExecutorOptions) => Promise<State | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AlertActionParams = SavedObjectAttributes;
|
|
||||||
|
|
||||||
export interface RawAlertAction extends SavedObjectAttributes {
|
export interface RawAlertAction extends SavedObjectAttributes {
|
||||||
group: string;
|
group: string;
|
||||||
actionRef: string;
|
actionRef: string;
|
||||||
|
|
|
@ -16,12 +16,15 @@ import {
|
||||||
enableAlert,
|
enableAlert,
|
||||||
loadAlert,
|
loadAlert,
|
||||||
loadAlerts,
|
loadAlerts,
|
||||||
|
loadAlertState,
|
||||||
loadAlertTypes,
|
loadAlertTypes,
|
||||||
muteAlerts,
|
muteAlerts,
|
||||||
unmuteAlerts,
|
unmuteAlerts,
|
||||||
muteAlert,
|
muteAlert,
|
||||||
unmuteAlert,
|
unmuteAlert,
|
||||||
updateAlert,
|
updateAlert,
|
||||||
|
muteAlertInstance,
|
||||||
|
unmuteAlertInstance,
|
||||||
} from './alert_api';
|
} from './alert_api';
|
||||||
import uuid from 'uuid';
|
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', () => {
|
describe('loadAlerts', () => {
|
||||||
test('should call find API with base parameters', async () => {
|
test('should call find API with base parameters', async () => {
|
||||||
const resolvedValue = {
|
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', () => {
|
describe('muteAlert', () => {
|
||||||
test('should call mute alert API', async () => {
|
test('should call mute alert API', async () => {
|
||||||
const result = await muteAlert({ http, id: '1' });
|
const result = await muteAlert({ http, id: '1' });
|
||||||
|
|
|
@ -5,8 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpSetup } from 'kibana/public';
|
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 { 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[]> {
|
export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<AlertType[]> {
|
||||||
return await http.get(`${BASE_ALERT_API_PATH}/types`);
|
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}`);
|
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({
|
export async function loadAlerts({
|
||||||
http,
|
http,
|
||||||
page,
|
page,
|
||||||
|
@ -133,6 +158,30 @@ export async function disableAlerts({
|
||||||
await Promise.all(ids.map(id => disableAlert({ id, http })));
|
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> {
|
export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
|
||||||
await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`);
|
await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ const mockAlertApis = {
|
||||||
unmuteAlert: jest.fn(),
|
unmuteAlert: jest.fn(),
|
||||||
enableAlert: jest.fn(),
|
enableAlert: jest.fn(),
|
||||||
disableAlert: jest.fn(),
|
disableAlert: jest.fn(),
|
||||||
|
requestRefresh: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// const AlertDetails = withBulkAlertOperations(RawAlertDetails);
|
// const AlertDetails = withBulkAlertOperations(RawAlertDetails);
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {
|
||||||
EuiPageContentBody,
|
EuiPageContentBody,
|
||||||
EuiButtonEmpty,
|
EuiButtonEmpty,
|
||||||
EuiSwitch,
|
EuiSwitch,
|
||||||
|
EuiCallOut,
|
||||||
|
EuiSpacer,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { useAppDependencies } from '../../../app_context';
|
import { useAppDependencies } from '../../../app_context';
|
||||||
|
@ -28,11 +30,13 @@ import {
|
||||||
ComponentOpts as BulkOperationsComponentOpts,
|
ComponentOpts as BulkOperationsComponentOpts,
|
||||||
withBulkAlertOperations,
|
withBulkAlertOperations,
|
||||||
} from '../../common/components/with_bulk_alert_api_operations';
|
} from '../../common/components/with_bulk_alert_api_operations';
|
||||||
|
import { AlertInstancesRouteWithApi } from './alert_instances_route';
|
||||||
|
|
||||||
type AlertDetailsProps = {
|
type AlertDetailsProps = {
|
||||||
alert: Alert;
|
alert: Alert;
|
||||||
alertType: AlertType;
|
alertType: AlertType;
|
||||||
actionTypes: ActionType[];
|
actionTypes: ActionType[];
|
||||||
|
requestRefresh: () => Promise<void>;
|
||||||
} & Pick<BulkOperationsComponentOpts, 'disableAlert' | 'enableAlert' | 'unmuteAlert' | 'muteAlert'>;
|
} & Pick<BulkOperationsComponentOpts, 'disableAlert' | 'enableAlert' | 'unmuteAlert' | 'muteAlert'>;
|
||||||
|
|
||||||
export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
|
@ -43,6 +47,7 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
enableAlert,
|
enableAlert,
|
||||||
unmuteAlert,
|
unmuteAlert,
|
||||||
muteAlert,
|
muteAlert,
|
||||||
|
requestRefresh,
|
||||||
}) => {
|
}) => {
|
||||||
const { capabilities } = useAppDependencies();
|
const { capabilities } = useAppDependencies();
|
||||||
|
|
||||||
|
@ -131,10 +136,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
setIsEnabled(true);
|
setIsEnabled(true);
|
||||||
await enableAlert(alert);
|
await enableAlert(alert);
|
||||||
}
|
}
|
||||||
|
requestRefresh();
|
||||||
}}
|
}}
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle"
|
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.enableTitle"
|
||||||
defaultMessage="Enable"
|
defaultMessage="Enable"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -154,10 +160,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
setIsMuted(true);
|
setIsMuted(true);
|
||||||
await muteAlert(alert);
|
await muteAlert(alert);
|
||||||
}
|
}
|
||||||
|
requestRefresh();
|
||||||
}}
|
}}
|
||||||
label={
|
label={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle"
|
id="xpack.triggersActionsUI.sections.alertDetails.collapsedItemActons.muteTitle"
|
||||||
defaultMessage="Mute"
|
defaultMessage="Mute"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -166,6 +173,23 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</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>
|
</EuiPageContentBody>
|
||||||
</EuiPageContent>
|
</EuiPageContent>
|
||||||
</EuiPageBody>
|
</EuiPageBody>
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps>
|
||||||
const [alert, setAlert] = useState<Alert | null>(null);
|
const [alert, setAlert] = useState<Alert | null>(null);
|
||||||
const [alertType, setAlertType] = useState<AlertType | null>(null);
|
const [alertType, setAlertType] = useState<AlertType | null>(null);
|
||||||
const [actionTypes, setActionTypes] = useState<ActionType[] | null>(null);
|
const [actionTypes, setActionTypes] = useState<ActionType[] | null>(null);
|
||||||
|
const [refreshToken, requestRefresh] = React.useState<number>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAlertData(
|
getAlertData(
|
||||||
alertId,
|
alertId,
|
||||||
|
@ -53,10 +53,15 @@ export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps>
|
||||||
setActionTypes,
|
setActionTypes,
|
||||||
toastNotifications
|
toastNotifications
|
||||||
);
|
);
|
||||||
}, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]);
|
}, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications, refreshToken]);
|
||||||
|
|
||||||
return alert && alertType && actionTypes ? (
|
return alert && alertType && actionTypes ? (
|
||||||
<AlertDetails alert={alert} alertType={alertType} actionTypes={actionTypes} />
|
<AlertDetails
|
||||||
|
alert={alert}
|
||||||
|
alertType={alertType}
|
||||||
|
actionTypes={actionTypes}
|
||||||
|
requestRefresh={async () => requestRefresh(Date.now())}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
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 React from 'react';
|
||||||
|
|
||||||
import { Alert, AlertType } from '../../../../types';
|
import { Alert, AlertType, AlertTaskState } from '../../../../types';
|
||||||
import { useAppDependencies } from '../../../app_context';
|
import { useAppDependencies } from '../../../app_context';
|
||||||
import {
|
import {
|
||||||
deleteAlerts,
|
deleteAlerts,
|
||||||
|
@ -19,7 +19,10 @@ import {
|
||||||
enableAlert,
|
enableAlert,
|
||||||
muteAlert,
|
muteAlert,
|
||||||
unmuteAlert,
|
unmuteAlert,
|
||||||
|
muteAlertInstance,
|
||||||
|
unmuteAlertInstance,
|
||||||
loadAlert,
|
loadAlert,
|
||||||
|
loadAlertState,
|
||||||
loadAlertTypes,
|
loadAlertTypes,
|
||||||
} from '../../../lib/alert_api';
|
} from '../../../lib/alert_api';
|
||||||
|
|
||||||
|
@ -31,10 +34,13 @@ export interface ComponentOpts {
|
||||||
deleteAlerts: (alerts: Alert[]) => Promise<void>;
|
deleteAlerts: (alerts: Alert[]) => Promise<void>;
|
||||||
muteAlert: (alert: Alert) => Promise<void>;
|
muteAlert: (alert: Alert) => Promise<void>;
|
||||||
unmuteAlert: (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>;
|
enableAlert: (alert: Alert) => Promise<void>;
|
||||||
disableAlert: (alert: Alert) => Promise<void>;
|
disableAlert: (alert: Alert) => Promise<void>;
|
||||||
deleteAlert: (alert: Alert) => Promise<void>;
|
deleteAlert: (alert: Alert) => Promise<void>;
|
||||||
loadAlert: (id: Alert['id']) => Promise<Alert>;
|
loadAlert: (id: Alert['id']) => Promise<Alert>;
|
||||||
|
loadAlertState: (id: Alert['id']) => Promise<AlertTaskState>;
|
||||||
loadAlertTypes: () => Promise<AlertType[]>;
|
loadAlertTypes: () => Promise<AlertType[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +82,16 @@ export function withBulkAlertOperations<T>(
|
||||||
return unmuteAlert({ http, id: alert.id });
|
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) => {
|
enableAlert={async (alert: Alert) => {
|
||||||
if (isAlertDisabled(alert)) {
|
if (isAlertDisabled(alert)) {
|
||||||
return enableAlert({ http, id: alert.id });
|
return enableAlert({ http, id: alert.id });
|
||||||
|
@ -88,6 +104,7 @@ export function withBulkAlertOperations<T>(
|
||||||
}}
|
}}
|
||||||
deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })}
|
deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })}
|
||||||
loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })}
|
loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })}
|
||||||
|
loadAlertState={async (alertId: Alert['id']) => loadAlertState({ http, alertId })}
|
||||||
loadAlertTypes={async () => loadAlertTypes({ http })}
|
loadAlertTypes={async () => loadAlertTypes({ http })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -101,3 +118,7 @@ function isAlertDisabled(alert: Alert) {
|
||||||
function isAlertMuted(alert: Alert) {
|
function isAlertMuted(alert: Alert) {
|
||||||
return alert.muteAll === true;
|
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 { ActionType } from '../../actions/common';
|
||||||
import { TypeRegistry } from './application/type_registry';
|
import { TypeRegistry } from './application/type_registry';
|
||||||
import { SanitizedAlert as Alert, AlertAction } from '../../../legacy/plugins/alerting/common';
|
import {
|
||||||
export { Alert, AlertAction };
|
SanitizedAlert as Alert,
|
||||||
|
AlertAction,
|
||||||
|
AlertTaskState,
|
||||||
|
RawAlertInstance,
|
||||||
|
} from '../../../legacy/plugins/alerting/common';
|
||||||
|
export { Alert, AlertAction, AlertTaskState, RawAlertInstance };
|
||||||
export { ActionType };
|
export { ActionType };
|
||||||
|
|
||||||
export type ActionTypeIndex = Record<string, ActionType>;
|
export type ActionTypeIndex = Record<string, ActionType>;
|
||||||
|
|
|
@ -6,141 +6,328 @@
|
||||||
|
|
||||||
import expect from '@kbn/expect';
|
import expect from '@kbn/expect';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||||
|
|
||||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
const testSubjects = getService('testSubjects');
|
const testSubjects = getService('testSubjects');
|
||||||
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']);
|
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']);
|
||||||
const browser = getService('browser');
|
const browser = getService('browser');
|
||||||
|
const log = getService('log');
|
||||||
const alerting = getService('alerting');
|
const alerting = getService('alerting');
|
||||||
|
const retry = getService('retry');
|
||||||
|
|
||||||
describe('Alert Details', function() {
|
describe('Alert Details', function() {
|
||||||
const testRunUuid = uuid.v4();
|
describe('Header', function() {
|
||||||
|
const testRunUuid = uuid.v4();
|
||||||
|
before(async () => {
|
||||||
|
await pageObjects.common.navigateToApp('triggersActions');
|
||||||
|
|
||||||
before(async () => {
|
const actions = await Promise.all([
|
||||||
await pageObjects.common.navigateToApp('triggersActions');
|
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([
|
const alert = await alerting.alerts.createAlwaysFiringWithActions(
|
||||||
alerting.actions.createAction({
|
`test-alert-${testRunUuid}`,
|
||||||
name: `server-log-${testRunUuid}-${0}`,
|
actions.map(action => ({
|
||||||
actionTypeId: '.server-log',
|
id: action.id,
|
||||||
config: {},
|
group: 'default',
|
||||||
secrets: {},
|
params: {
|
||||||
}),
|
message: 'from alert 1s',
|
||||||
alerting.actions.createAction({
|
level: 'warn',
|
||||||
name: `server-log-${testRunUuid}-${1}`,
|
},
|
||||||
actionTypeId: '.server-log',
|
}))
|
||||||
config: {},
|
);
|
||||||
secrets: {},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const alert = await alerting.alerts.createAlwaysFiringWithActions(
|
// refresh to see alert
|
||||||
`test-alert-${testRunUuid}`,
|
await browser.refresh();
|
||||||
actions.map(action => ({
|
|
||||||
id: action.id,
|
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||||
group: 'default',
|
|
||||||
params: {
|
// Verify content
|
||||||
message: 'from alert 1s',
|
await testSubjects.existOrFail('alertsList');
|
||||||
level: 'warn',
|
|
||||||
|
// 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
|
const dateOnAllInstances = moment(date)
|
||||||
await browser.refresh();
|
.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
|
const durationFromInstanceTillPageLoad = moment.duration(
|
||||||
await testSubjects.existOrFail('alertsList');
|
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
|
it('renders the muted inactive alert instances', async () => {
|
||||||
await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name);
|
// mute an alert instance that doesn't exist
|
||||||
});
|
await alerting.alerts.muteAlertInstance(alert.id, 'eu-east');
|
||||||
|
|
||||||
it('renders the alert details', async () => {
|
// refresh to see alert
|
||||||
const headingText = await pageObjects.alertDetailsUI.getHeadingText();
|
await browser.refresh();
|
||||||
expect(headingText).to.be(`test-alert-${testRunUuid}`);
|
|
||||||
|
|
||||||
const alertType = await pageObjects.alertDetailsUI.getAlertType();
|
const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList();
|
||||||
expect(alertType).to.be(`Always Firing`);
|
expect(instancesList.filter(alertInstance => alertInstance.instance === 'eu-east')).to.eql([
|
||||||
|
{
|
||||||
|
instance: 'eu-east',
|
||||||
|
status: 'Inactive',
|
||||||
|
start: '',
|
||||||
|
duration: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels();
|
it('allows the user to mute a specific instance', async () => {
|
||||||
expect(actionType).to.be(`Server log`);
|
// Verify content
|
||||||
expect(actionCount).to.be(`+1`);
|
await testSubjects.existOrFail('alertInstancesList');
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable the alert', async () => {
|
log.debug(`Ensuring us-central is not muted`);
|
||||||
const enableSwitch = await testSubjects.find('enableSwitch');
|
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-central', false);
|
||||||
|
|
||||||
const isChecked = await enableSwitch.getAttribute('aria-checked');
|
log.debug(`Muting us-central`);
|
||||||
expect(isChecked).to.eql('true');
|
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');
|
it('allows the user to unmute a specific instance', async () => {
|
||||||
const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute(
|
// Verify content
|
||||||
'aria-checked'
|
await testSubjects.existOrFail('alertInstancesList');
|
||||||
);
|
|
||||||
expect(isCheckedAfterDisabling).to.eql('false');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shouldnt allow you to mute a disabled alert', async () => {
|
log.debug(`Ensuring us-east is not muted`);
|
||||||
const disabledEnableSwitch = await testSubjects.find('enableSwitch');
|
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', false);
|
||||||
expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false');
|
|
||||||
|
|
||||||
const muteSwitch = await testSubjects.find('muteSwitch');
|
log.debug(`Muting us-east`);
|
||||||
expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false');
|
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');
|
log.debug(`Unmuting us-east`);
|
||||||
const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute(
|
await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('us-east');
|
||||||
'aria-checked'
|
|
||||||
);
|
|
||||||
expect(isDisabledMuteAfterDisabling).to.eql('false');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reenable a disabled the alert', async () => {
|
log.debug(`Ensuring us-east is not muted`);
|
||||||
const enableSwitch = await testSubjects.find('enableSwitch');
|
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('us-east', false);
|
||||||
|
});
|
||||||
|
|
||||||
const isChecked = await enableSwitch.getAttribute('aria-checked');
|
it('allows the user unmute an inactive instance', async () => {
|
||||||
expect(isChecked).to.eql('false');
|
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');
|
log.debug(`Ensuring eu-east is removed from list`);
|
||||||
const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute(
|
await pageObjects.alertDetailsUI.ensureAlertInstanceExistance('eu-east', false);
|
||||||
'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');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,14 +35,15 @@ function createAlwaysFiringAlertType(setupContract: any) {
|
||||||
name: 'Always Firing',
|
name: 'Always Firing',
|
||||||
actionGroups: ['default', 'other'],
|
actionGroups: ['default', 'other'],
|
||||||
async executor(alertExecutorOptions: any) {
|
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 {
|
return {
|
||||||
globalStateValue: true,
|
globalStateValue: true,
|
||||||
groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1,
|
groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1,
|
||||||
|
|
|
@ -4,10 +4,14 @@
|
||||||
* 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 expect from '@kbn/expect';
|
||||||
import { FtrProviderContext } from '../ftr_provider_context';
|
import { FtrProviderContext } from '../ftr_provider_context';
|
||||||
|
|
||||||
export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
|
export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
|
||||||
const testSubjects = getService('testSubjects');
|
const testSubjects = getService('testSubjects');
|
||||||
|
const find = getService('find');
|
||||||
|
const log = getService('log');
|
||||||
|
const retry = getService('retry');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async getHeadingText() {
|
async getHeadingText() {
|
||||||
|
@ -22,5 +26,71 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) {
|
||||||
actionCount: await testSubjects.getVisibleText('actionCountLabel'),
|
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;
|
id: string;
|
||||||
group: string;
|
group: string;
|
||||||
params: Record<string, any>;
|
params: Record<string, any>;
|
||||||
}>
|
}>,
|
||||||
|
params: Record<string, any> = {}
|
||||||
) {
|
) {
|
||||||
this.log.debug(`creating alert ${name}`);
|
this.log.debug(`creating alert ${name}`);
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ export class Alerts {
|
||||||
schedule: { interval: '1m' },
|
schedule: { interval: '1m' },
|
||||||
throttle: '1m',
|
throttle: '1m',
|
||||||
actions,
|
actions,
|
||||||
params: {},
|
params,
|
||||||
});
|
});
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -76,4 +77,25 @@ export class Alerts {
|
||||||
}
|
}
|
||||||
this.log.debug(`deleted alert ${alert.id}`);
|
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