mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAM] Add Snooze UI and Unsnooze API (#128214)
* Add Snooze UI and Unsnooze API * Add unsnooze writeoperation * Add unsnooze API tests * Add UI tests * Add tooltip and enable canceling snooze when clicking Enabled * Fix rulesClient mock Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
51e0845146
commit
d102213d1d
31 changed files with 1422 additions and 95 deletions
|
@ -155,6 +155,15 @@ Refer to the corresponding {es} logs for potential write errors.
|
|||
| `unknown` | User is updating an alert.
|
||||
| `failure` | User is not authorized to update an alert.
|
||||
|
||||
.2+| `rule_snooze`
|
||||
| `unknown` | User is snoozing a rule.
|
||||
| `failure` | User is not authorized to snooze a rule.
|
||||
|
||||
.2+| `rule_unsnooze`
|
||||
| `unknown` | User is unsnoozing a rule.
|
||||
| `failure` | User is not authorized to unsnooze a rule.
|
||||
|
||||
|
||||
3+a|
|
||||
====== Type: deletion
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ export enum WriteOperations {
|
|||
MuteAlert = 'muteAlert',
|
||||
UnmuteAlert = 'unmuteAlert',
|
||||
Snooze = 'snooze',
|
||||
Unsnooze = 'unsnooze',
|
||||
}
|
||||
|
||||
export interface EnsureAuthorizedOpts {
|
||||
|
|
|
@ -31,6 +31,7 @@ import { unmuteAllRuleRoute } from './unmute_all_rule';
|
|||
import { unmuteAlertRoute } from './unmute_alert';
|
||||
import { updateRuleApiKeyRoute } from './update_rule_api_key';
|
||||
import { snoozeRuleRoute } from './snooze_rule';
|
||||
import { unsnoozeRuleRoute } from './unsnooze_rule';
|
||||
|
||||
export interface RouteOptions {
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
|
@ -65,4 +66,5 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
unmuteAlertRoute(router, licenseState);
|
||||
updateRuleApiKeyRoute(router, licenseState);
|
||||
snoozeRuleRoute(router, licenseState);
|
||||
unsnoozeRuleRoute(router, licenseState);
|
||||
}
|
||||
|
|
|
@ -21,17 +21,10 @@ beforeEach(() => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const SNOOZE_END_TIME = '2025-03-07T00:00:00.000Z';
|
||||
// These tests don't test for future snooze time validation, so this date doesn't need to be in the future
|
||||
const SNOOZE_END_TIME = '2021-03-07T00:00:00.000Z';
|
||||
|
||||
describe('snoozeAlertRoute', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers('modern');
|
||||
jest.setSystemTime(new Date(2020, 3, 1));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it('snoozes an alert', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
|
80
x-pack/plugins/alerting/server/routes/unsnooze_rule.test.ts
Normal file
80
x-pack/plugins/alerting/server/routes/unsnooze_rule.test.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { unsnoozeRuleRoute } from './unsnooze_rule';
|
||||
import { httpServiceMock } from 'src/core/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('unsnoozeAlertRoute', () => {
|
||||
it('unsnoozes an alert', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
unsnoozeRuleRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_unsnooze"`);
|
||||
|
||||
rulesClient.unsnooze.mockResolvedValueOnce();
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
params: {
|
||||
id: '1',
|
||||
},
|
||||
},
|
||||
['noContent']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toEqual(undefined);
|
||||
|
||||
expect(rulesClient.unsnooze).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.unsnooze.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"id": "1",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(res.noContent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ensures the rule type gets validated for the license', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
unsnoozeRuleRoute(router, licenseState);
|
||||
|
||||
const [, handler] = router.post.mock.calls[0];
|
||||
|
||||
rulesClient.unsnooze.mockRejectedValue(new AlertTypeDisabledError('Fail', 'license_invalid'));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [
|
||||
'ok',
|
||||
'forbidden',
|
||||
]);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } });
|
||||
});
|
||||
});
|
45
x-pack/plugins/alerting/server/routes/unsnooze_rule.ts
Normal file
45
x-pack/plugins/alerting/server/routes/unsnooze_rule.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IRouter } from 'kibana/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ILicenseState, RuleMutedError } from '../lib';
|
||||
import { verifyAccessAndContext } from './lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
export const unsnoozeRuleRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_unsnooze`,
|
||||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesClient = context.alerting.getRulesClient();
|
||||
const params = req.params;
|
||||
try {
|
||||
await rulesClient.unsnooze({ ...params });
|
||||
return res.noContent();
|
||||
} catch (e) {
|
||||
if (e instanceof RuleMutedError) {
|
||||
return e.sendResponse(res);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -33,6 +33,7 @@ const createRulesClientMock = () => {
|
|||
getExecutionLogForRule: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
snooze: jest.fn(),
|
||||
unsnooze: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ export enum RuleAuditAction {
|
|||
AGGREGATE = 'rule_aggregate',
|
||||
GET_EXECUTION_LOG = 'rule_get_execution_log',
|
||||
SNOOZE = 'rule_snooze',
|
||||
UNSNOOZE = 'rule_unsnooze',
|
||||
}
|
||||
|
||||
type VerbsTuple = [string, string, string];
|
||||
|
@ -50,6 +51,7 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
|
|||
'accessed execution log for',
|
||||
],
|
||||
rule_snooze: ['snooze', 'snoozing', 'snoozed'],
|
||||
rule_unsnooze: ['unsnooze', 'unsnoozing', 'unsnoozed'],
|
||||
};
|
||||
|
||||
const eventTypes: Record<RuleAuditAction, EcsEventType> = {
|
||||
|
@ -69,6 +71,7 @@ const eventTypes: Record<RuleAuditAction, EcsEventType> = {
|
|||
rule_aggregate: 'access',
|
||||
rule_get_execution_log: 'access',
|
||||
rule_snooze: 'change',
|
||||
rule_unsnooze: 'change',
|
||||
};
|
||||
|
||||
export interface RuleAuditEventParams {
|
||||
|
|
|
@ -1682,6 +1682,68 @@ export class RulesClient {
|
|||
);
|
||||
}
|
||||
|
||||
public async unsnooze({ id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
this.logger,
|
||||
`rulesClient.unsnooze('${id}')`,
|
||||
async () => await this.unsnoozeWithOCC({ id })
|
||||
);
|
||||
}
|
||||
|
||||
private async unsnoozeWithOCC({ id }: { id: string }) {
|
||||
const { attributes, version } = await this.unsecuredSavedObjectsClient.get<RawRule>(
|
||||
'alert',
|
||||
id
|
||||
);
|
||||
|
||||
try {
|
||||
await this.authorization.ensureAuthorized({
|
||||
ruleTypeId: attributes.alertTypeId,
|
||||
consumer: attributes.consumer,
|
||||
operation: WriteOperations.Unsnooze,
|
||||
entity: AlertingAuthorizationEntity.Rule,
|
||||
});
|
||||
|
||||
if (attributes.actions.length) {
|
||||
await this.actionsAuthorization.ensureAuthorized('execute');
|
||||
}
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.UNSNOOZE,
|
||||
savedObject: { type: 'alert', id },
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.UNSNOOZE,
|
||||
outcome: 'unknown',
|
||||
savedObject: { type: 'alert', id },
|
||||
})
|
||||
);
|
||||
|
||||
this.ruleTypeRegistry.ensureRuleTypeEnabled(attributes.alertTypeId);
|
||||
|
||||
const updateAttributes = this.updateMeta({
|
||||
snoozeEndTime: null,
|
||||
muteAll: false,
|
||||
updatedBy: await this.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
const updateOptions = { version };
|
||||
|
||||
await partiallyUpdateAlert(
|
||||
this.unsecuredSavedObjectsClient,
|
||||
id,
|
||||
updateAttributes,
|
||||
updateOptions
|
||||
);
|
||||
}
|
||||
|
||||
public async muteAll({ id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
this.logger,
|
||||
|
|
|
@ -226,6 +226,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/snooze",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unsnooze",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -321,6 +322,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/snooze",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/get",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/find",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/alert/update",
|
||||
|
@ -376,6 +378,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/snooze",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary",
|
||||
|
@ -478,6 +481,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/snooze",
|
||||
"alerting:1.0.0-zeta1:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary",
|
||||
|
|
|
@ -33,6 +33,7 @@ const writeOperations: Record<AlertingEntity, string[]> = {
|
|||
'muteAlert',
|
||||
'unmuteAlert',
|
||||
'snooze',
|
||||
'unsnooze',
|
||||
],
|
||||
alert: ['update'],
|
||||
};
|
||||
|
|
|
@ -27874,9 +27874,7 @@
|
|||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.durationTitle": "ルールを実行するのにかかる時間。",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel": "編集",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editButtonTooltip": "編集",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.enabledTitle": "有効",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle": "前回の実行の開始時間。",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.mutedBadge": "ミュート",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle": "名前",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle": "間隔",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle": "ステータス",
|
||||
|
|
|
@ -27905,9 +27905,7 @@
|
|||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.durationTitle": "运行规则所需的时间长度。",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editAriaLabel": "编辑",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.editButtonTooltip": "编辑",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.enabledTitle": "已启用",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastExecutionDateTitle": "上次执行的开始时间。",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.mutedBadge": "已静音",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.nameTitle": "名称",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.scheduleTitle": "时间间隔",
|
||||
"xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle": "状态",
|
||||
|
|
|
@ -10,3 +10,5 @@
|
|||
|
||||
export * from './data';
|
||||
export const BASE_TRIGGERS_ACTIONS_UI_API_PATH = '/api/triggers_actions_ui';
|
||||
export * from './parse_interval';
|
||||
export * from './experimental_features';
|
||||
|
|
28
x-pack/plugins/triggers_actions_ui/common/parse_interval.ts
Normal file
28
x-pack/plugins/triggers_actions_ui/common/parse_interval.ts
Normal file
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
export const INTERVAL_STRING_RE = new RegExp(`^([\\d\\.]+)\\s*(${dateMath.units.join('|')})$`);
|
||||
|
||||
export const parseInterval = (intervalString: string) => {
|
||||
if (intervalString) {
|
||||
const matches = intervalString.match(INTERVAL_STRING_RE);
|
||||
if (matches) {
|
||||
const value = Number(matches[1]);
|
||||
const unit = matches[2];
|
||||
return { value, unit };
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
i18n.translate('xpack.triggersActionsUI.parseInterval.errorMessage', {
|
||||
defaultMessage: '{value} is not an interval string',
|
||||
values: {
|
||||
value: intervalString,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
|
@ -43,6 +43,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
|
|||
scheduled_task_id: scheduledTaskId,
|
||||
execution_status: executionStatus,
|
||||
actions: actions,
|
||||
snooze_end_time: snoozeEndTime,
|
||||
...rest
|
||||
}: any) => ({
|
||||
ruleTypeId,
|
||||
|
@ -54,6 +55,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
|
|||
notifyWhen,
|
||||
muteAll,
|
||||
mutedInstanceIds,
|
||||
snoozeEndTime,
|
||||
executionStatus: executionStatus ? transformExecutionStatus(executionStatus) : undefined,
|
||||
actions: actions
|
||||
? actions.map((action: AsApiContract<RuleAction>) => transformAction(action))
|
||||
|
|
|
@ -23,3 +23,5 @@ export { unmuteAlertInstance } from './unmute_alert';
|
|||
export { unmuteRule, unmuteRules } from './unmute';
|
||||
export { updateRule } from './update';
|
||||
export { resolveRule } from './resolve_rule';
|
||||
export { snoozeRule } from './snooze';
|
||||
export { unsnoozeRule } from './unsnooze';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { snoozeRule } from './snooze';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('snoozeRule', () => {
|
||||
test('should call mute alert API', async () => {
|
||||
const result = await snoozeRule({ http, id: '1/', snoozeEndTime: '9999-01-01T00:00:00.000Z' });
|
||||
expect(result).toEqual(undefined);
|
||||
expect(http.post.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/internal/alerting/rule/1%2F/_snooze",
|
||||
Object {
|
||||
"body": "{\\"snooze_end_time\\":\\"9999-01-01T00:00:00.000Z\\"}",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
|
||||
export async function snoozeRule({
|
||||
id,
|
||||
snoozeEndTime,
|
||||
http,
|
||||
}: {
|
||||
id: string;
|
||||
snoozeEndTime: string | -1;
|
||||
http: HttpSetup;
|
||||
}): Promise<void> {
|
||||
await http.post(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_snooze`, {
|
||||
body: JSON.stringify({
|
||||
snooze_end_time: snoozeEndTime,
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { unsnoozeRule } from './unsnooze';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('muteRule', () => {
|
||||
test('should call mute alert API', async () => {
|
||||
const result = await unsnoozeRule({ http, id: '1/' });
|
||||
expect(result).toEqual(undefined);
|
||||
expect(http.post.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/internal/alerting/rule/1%2F/_unsnooze",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { HttpSetup } from 'kibana/public';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
|
||||
export async function unsnoozeRule({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
|
||||
await http.post(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_unsnooze`);
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { RuleStatusDropdown, ComponentOpts } from './rule_status_dropdown';
|
||||
|
||||
const NOW_STRING = '2020-03-01T00:00:00.000Z';
|
||||
const SNOOZE_END_TIME = new Date('2020-03-04T00:00:00.000Z');
|
||||
|
||||
describe('RuleStatusDropdown', () => {
|
||||
const enableRule = jest.fn();
|
||||
const disableRule = jest.fn();
|
||||
const snoozeRule = jest.fn();
|
||||
const unsnoozeRule = jest.fn();
|
||||
const props: ComponentOpts = {
|
||||
disableRule,
|
||||
enableRule,
|
||||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
item: {
|
||||
id: '1',
|
||||
name: 'test rule',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'active',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
consumer: 'test',
|
||||
actionsCount: 0,
|
||||
ruleType: 'test_rule_type',
|
||||
createdAt: new Date('2020-08-20T19:23:38Z'),
|
||||
enabledInLicense: true,
|
||||
isEditable: true,
|
||||
notifyWhen: null,
|
||||
index: 0,
|
||||
updatedAt: new Date('2020-08-20T19:23:38Z'),
|
||||
snoozeEndTime: null,
|
||||
},
|
||||
onRuleChanged: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(NOW_STRING).valueOf());
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('renders status control', () => {
|
||||
const wrapper = mountWithIntl(<RuleStatusDropdown {...props} />);
|
||||
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe('Enabled');
|
||||
});
|
||||
|
||||
test('renders status control as disabled when rule is disabled', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusDropdown {...{ ...props, item: { ...props.item, enabled: false } }} />
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe(
|
||||
'Disabled'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders status control as snoozed when rule is snoozed', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(NOW_STRING).valueOf());
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusDropdown
|
||||
{...{ ...props, item: { ...props.item, snoozeEndTime: SNOOZE_END_TIME } }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe('Snoozed');
|
||||
expect(wrapper.find('[data-test-subj="remainingSnoozeTime"]').first().text()).toBe('3 days');
|
||||
});
|
||||
|
||||
test('renders status control as snoozed when rule has muteAll set to true', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => new Date(NOW_STRING).valueOf());
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusDropdown {...{ ...props, item: { ...props.item, muteAll: true } }} />
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe('Snoozed');
|
||||
expect(wrapper.find('[data-test-subj="remainingSnoozeTime"]').first().text()).toBe(
|
||||
'Indefinitely'
|
||||
);
|
||||
});
|
||||
|
||||
test('renders status control as disabled when rule is snoozed but also disabled', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<RuleStatusDropdown
|
||||
{...{ ...props, item: { ...props.item, enabled: false, snoozeEndTime: SNOOZE_END_TIME } }}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="statusDropdown"]').first().props().title).toBe(
|
||||
'Disabled'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
useGeneratedHtmlId,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiBadge,
|
||||
EuiPanel,
|
||||
EuiFieldNumber,
|
||||
EuiSelect,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiHorizontalRule,
|
||||
EuiTitle,
|
||||
EuiFlexGrid,
|
||||
EuiSpacer,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { parseInterval } from '../../../../../common';
|
||||
|
||||
import { RuleTableItem } from '../../../../types';
|
||||
|
||||
type SnoozeUnit = 'm' | 'h' | 'd' | 'w' | 'M';
|
||||
const SNOOZE_END_TIME_FORMAT = 'LL @ LT';
|
||||
|
||||
export interface ComponentOpts {
|
||||
item: RuleTableItem;
|
||||
onRuleChanged: () => void;
|
||||
enableRule: () => Promise<void>;
|
||||
disableRule: () => Promise<void>;
|
||||
snoozeRule: (snoozeEndTime: string | -1) => Promise<void>;
|
||||
unsnoozeRule: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const RuleStatusDropdown: React.FunctionComponent<ComponentOpts> = ({
|
||||
item,
|
||||
onRuleChanged,
|
||||
disableRule,
|
||||
enableRule,
|
||||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
}: ComponentOpts) => {
|
||||
const [isEnabled, setIsEnabled] = useState<boolean>(item.enabled);
|
||||
const [isSnoozed, setIsSnoozed] = useState<boolean>(isItemSnoozed(item));
|
||||
useEffect(() => {
|
||||
setIsEnabled(item.enabled);
|
||||
}, [item.enabled]);
|
||||
useEffect(() => {
|
||||
setIsSnoozed(isItemSnoozed(item));
|
||||
}, [item]);
|
||||
const [isUpdating, setIsUpdating] = useState<boolean>(false);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const onClickBadge = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), [setIsPopoverOpen]);
|
||||
const onClosePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]);
|
||||
|
||||
const onChangeEnabledStatus = useCallback(
|
||||
async (enable: boolean) => {
|
||||
setIsUpdating(true);
|
||||
if (enable) {
|
||||
await enableRule();
|
||||
} else {
|
||||
await disableRule();
|
||||
}
|
||||
setIsEnabled(!isEnabled);
|
||||
onRuleChanged();
|
||||
setIsUpdating(false);
|
||||
},
|
||||
[setIsUpdating, isEnabled, setIsEnabled, onRuleChanged, enableRule, disableRule]
|
||||
);
|
||||
const onChangeSnooze = useCallback(
|
||||
async (value: number, unit?: SnoozeUnit) => {
|
||||
setIsUpdating(true);
|
||||
if (value === -1) {
|
||||
await snoozeRule(-1);
|
||||
} else if (value !== 0) {
|
||||
const snoozeEndTime = moment().add(value, unit).toISOString();
|
||||
await snoozeRule(snoozeEndTime);
|
||||
} else await unsnoozeRule();
|
||||
setIsSnoozed(value !== 0);
|
||||
onRuleChanged();
|
||||
setIsUpdating(false);
|
||||
},
|
||||
[setIsUpdating, setIsSnoozed, onRuleChanged, snoozeRule, unsnoozeRule]
|
||||
);
|
||||
|
||||
const badgeColor = !isEnabled ? 'default' : isSnoozed ? 'warning' : 'primary';
|
||||
const badgeMessage = !isEnabled ? DISABLED : isSnoozed ? SNOOZED : ENABLED;
|
||||
|
||||
const remainingSnoozeTime =
|
||||
isEnabled && isSnoozed ? (
|
||||
<EuiToolTip content={moment(item.snoozeEndTime).format(SNOOZE_END_TIME_FORMAT)}>
|
||||
<EuiText color="subdued" size="xs">
|
||||
{item.muteAll ? INDEFINITELY : moment(item.snoozeEndTime).fromNow(true)}
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
) : null;
|
||||
|
||||
const badge = (
|
||||
<EuiBadge
|
||||
color={badgeColor}
|
||||
iconSide="right"
|
||||
iconType={!isUpdating ? 'arrowDown' : undefined}
|
||||
onClick={onClickBadge}
|
||||
iconOnClick={onClickBadge}
|
||||
onClickAriaLabel={OPEN_MENU_ARIA_LABEL}
|
||||
iconOnClickAriaLabel={OPEN_MENU_ARIA_LABEL}
|
||||
isDisabled={isUpdating}
|
||||
>
|
||||
{badgeMessage}
|
||||
{isUpdating && (
|
||||
<EuiLoadingSpinner style={{ marginLeft: '4px', marginRight: '4px' }} size="s" />
|
||||
)}
|
||||
</EuiBadge>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexStart" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={badge}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={onClosePopover}
|
||||
panelPaddingSize="s"
|
||||
data-test-subj="statusDropdown"
|
||||
title={badgeMessage}
|
||||
>
|
||||
<RuleStatusMenu
|
||||
onClosePopover={onClosePopover}
|
||||
onChangeEnabledStatus={onChangeEnabledStatus}
|
||||
onChangeSnooze={onChangeSnooze}
|
||||
isEnabled={isEnabled}
|
||||
isSnoozed={isSnoozed}
|
||||
snoozeEndTime={item.snoozeEndTime}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj="remainingSnoozeTime" grow={false}>
|
||||
{remainingSnoozeTime}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface RuleStatusMenuProps {
|
||||
onChangeEnabledStatus: (enabled: boolean) => void;
|
||||
onChangeSnooze: (value: number | -1, unit?: SnoozeUnit) => void;
|
||||
onClosePopover: () => void;
|
||||
isEnabled: boolean;
|
||||
isSnoozed: boolean;
|
||||
snoozeEndTime?: Date | null;
|
||||
}
|
||||
|
||||
const RuleStatusMenu: React.FunctionComponent<RuleStatusMenuProps> = ({
|
||||
onChangeEnabledStatus,
|
||||
onChangeSnooze,
|
||||
onClosePopover,
|
||||
isEnabled,
|
||||
isSnoozed,
|
||||
snoozeEndTime,
|
||||
}) => {
|
||||
const enableRule = useCallback(() => {
|
||||
if (isSnoozed) {
|
||||
// Unsnooze if the rule is snoozed and the user clicks Enabled
|
||||
onChangeSnooze(0, 'm');
|
||||
} else {
|
||||
onChangeEnabledStatus(true);
|
||||
}
|
||||
onClosePopover();
|
||||
}, [onChangeEnabledStatus, onClosePopover, onChangeSnooze, isSnoozed]);
|
||||
const disableRule = useCallback(() => {
|
||||
onChangeEnabledStatus(false);
|
||||
onClosePopover();
|
||||
}, [onChangeEnabledStatus, onClosePopover]);
|
||||
|
||||
const onApplySnooze = useCallback(
|
||||
(value: number, unit?: SnoozeUnit) => {
|
||||
onChangeSnooze(value, unit);
|
||||
onClosePopover();
|
||||
},
|
||||
[onClosePopover, onChangeSnooze]
|
||||
);
|
||||
|
||||
let snoozeButtonTitle = <EuiText size="s">{SNOOZE}</EuiText>;
|
||||
if (isSnoozed && snoozeEndTime) {
|
||||
snoozeButtonTitle = (
|
||||
<>
|
||||
<EuiText size="s">{SNOOZE}</EuiText>{' '}
|
||||
<EuiText size="xs" color="subdued">
|
||||
{moment(snoozeEndTime).format(SNOOZE_END_TIME_FORMAT)}
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const panels = [
|
||||
{
|
||||
id: 0,
|
||||
width: 360,
|
||||
items: [
|
||||
{
|
||||
name: ENABLED,
|
||||
icon: isEnabled && !isSnoozed ? 'check' : 'empty',
|
||||
onClick: enableRule,
|
||||
},
|
||||
{
|
||||
name: DISABLED,
|
||||
icon: !isEnabled ? 'check' : 'empty',
|
||||
onClick: disableRule,
|
||||
},
|
||||
{
|
||||
name: snoozeButtonTitle,
|
||||
icon: isEnabled && isSnoozed ? 'check' : 'empty',
|
||||
panel: 1,
|
||||
disabled: !isEnabled,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
width: 360,
|
||||
title: SNOOZE,
|
||||
content: (
|
||||
<SnoozePanel
|
||||
applySnooze={onApplySnooze}
|
||||
interval={futureTimeToInterval(snoozeEndTime)}
|
||||
showCancel={isSnoozed}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return <EuiContextMenu initialPanelId={0} panels={panels} />;
|
||||
};
|
||||
|
||||
interface SnoozePanelProps {
|
||||
interval?: string;
|
||||
applySnooze: (value: number | -1, unit?: SnoozeUnit) => void;
|
||||
showCancel: boolean;
|
||||
}
|
||||
|
||||
const SnoozePanel: React.FunctionComponent<SnoozePanelProps> = ({
|
||||
interval = '3d',
|
||||
applySnooze,
|
||||
showCancel,
|
||||
}) => {
|
||||
const [intervalValue, setIntervalValue] = useState(parseInterval(interval).value);
|
||||
const [intervalUnit, setIntervalUnit] = useState(parseInterval(interval).unit);
|
||||
|
||||
const onChangeValue = useCallback(
|
||||
({ target }) => setIntervalValue(target.value),
|
||||
[setIntervalValue]
|
||||
);
|
||||
const onChangeUnit = useCallback(
|
||||
({ target }) => setIntervalUnit(target.value),
|
||||
[setIntervalUnit]
|
||||
);
|
||||
|
||||
const onApply1h = useCallback(() => applySnooze(1, 'h'), [applySnooze]);
|
||||
const onApply3h = useCallback(() => applySnooze(3, 'h'), [applySnooze]);
|
||||
const onApply8h = useCallback(() => applySnooze(8, 'h'), [applySnooze]);
|
||||
const onApply1d = useCallback(() => applySnooze(1, 'd'), [applySnooze]);
|
||||
const onApplyIndefinite = useCallback(() => applySnooze(-1), [applySnooze]);
|
||||
const onClickApplyButton = useCallback(
|
||||
() => applySnooze(intervalValue, intervalUnit as SnoozeUnit),
|
||||
[applySnooze, intervalValue, intervalUnit]
|
||||
);
|
||||
const onCancelSnooze = useCallback(() => applySnooze(0, 'm'), [applySnooze]);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="none">
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldNumber
|
||||
value={intervalValue}
|
||||
onChange={onChangeValue}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.snoozePanelIntervalValueLabel',
|
||||
{ defaultMessage: 'Snooze interval value' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiSelect
|
||||
id={useGeneratedHtmlId({ prefix: 'snoozeUnit' })}
|
||||
value={intervalUnit}
|
||||
onChange={onChangeUnit}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.snoozePanelIntervalUnitLabel',
|
||||
{ defaultMessage: 'Snooze interval unit' }
|
||||
)}
|
||||
options={[
|
||||
{ value: 'm', text: MINUTES },
|
||||
{ value: 'h', text: HOURS },
|
||||
{ value: 'd', text: DAYS },
|
||||
{ value: 'w', text: WEEKS },
|
||||
{ value: 'M', text: MONTHS },
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={onClickApplyButton}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.applySnooze', {
|
||||
defaultMessage: 'Apply',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeCommonlyUsed', {
|
||||
defaultMessage: 'Commonly used',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
<EuiFlexItem>
|
||||
<EuiLink onClick={onApply1h}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneHour', {
|
||||
defaultMessage: '1 hour',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink onClick={onApply3h}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeThreeHours', {
|
||||
defaultMessage: '3 hours',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink onClick={onApply8h}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeEightHours', {
|
||||
defaultMessage: '8 hours',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink onClick={onApply1d}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeOneDay', {
|
||||
defaultMessage: '1 day',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiLink onClick={onApplyIndefinite}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeIndefinitely', {
|
||||
defaultMessage: 'Snooze indefinitely',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{showCancel && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow>
|
||||
<EuiButton color="danger" onClick={onCancelSnooze}>
|
||||
Cancel snooze
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const isItemSnoozed = (item: { snoozeEndTime?: Date | null; muteAll: boolean }) => {
|
||||
const { snoozeEndTime, muteAll } = item;
|
||||
if (muteAll) return true;
|
||||
if (!snoozeEndTime) {
|
||||
return false;
|
||||
}
|
||||
return moment(Date.now()).isBefore(snoozeEndTime);
|
||||
};
|
||||
|
||||
const futureTimeToInterval = (time?: Date | null) => {
|
||||
if (!time) return;
|
||||
const relativeTime = moment(time).locale('en').fromNow(true);
|
||||
const [valueStr, unitStr] = relativeTime.split(' ');
|
||||
let value = valueStr === 'a' || valueStr === 'an' ? 1 : parseInt(valueStr, 10);
|
||||
let unit;
|
||||
switch (unitStr) {
|
||||
case 'year':
|
||||
case 'years':
|
||||
unit = 'M';
|
||||
value = value * 12;
|
||||
break;
|
||||
case 'month':
|
||||
case 'months':
|
||||
unit = 'M';
|
||||
break;
|
||||
case 'day':
|
||||
case 'days':
|
||||
unit = 'd';
|
||||
break;
|
||||
case 'hour':
|
||||
case 'hours':
|
||||
unit = 'h';
|
||||
break;
|
||||
case 'minute':
|
||||
case 'minutes':
|
||||
unit = 'm';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!unit) return;
|
||||
return `${value}${unit}`;
|
||||
};
|
||||
|
||||
const ENABLED = i18n.translate('xpack.triggersActionsUI.sections.rulesList.enabledRuleStatus', {
|
||||
defaultMessage: 'Enabled',
|
||||
});
|
||||
|
||||
const DISABLED = i18n.translate('xpack.triggersActionsUI.sections.rulesList.disabledRuleStatus', {
|
||||
defaultMessage: 'Disabled',
|
||||
});
|
||||
|
||||
const SNOOZED = i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozedRuleStatus', {
|
||||
defaultMessage: 'Snoozed',
|
||||
});
|
||||
|
||||
const SNOOZE = i18n.translate('xpack.triggersActionsUI.sections.rulesList.snoozeMenuTitle', {
|
||||
defaultMessage: 'Snooze',
|
||||
});
|
||||
|
||||
const OPEN_MENU_ARIA_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel',
|
||||
{
|
||||
defaultMessage: 'Change rule status or snooze',
|
||||
}
|
||||
);
|
||||
|
||||
const MINUTES = i18n.translate('xpack.triggersActionsUI.sections.rulesList.minutesLabel', {
|
||||
defaultMessage: 'minutes',
|
||||
});
|
||||
const HOURS = i18n.translate('xpack.triggersActionsUI.sections.rulesList.hoursLabel', {
|
||||
defaultMessage: 'hours',
|
||||
});
|
||||
const DAYS = i18n.translate('xpack.triggersActionsUI.sections.rulesList.daysLabel', {
|
||||
defaultMessage: 'days',
|
||||
});
|
||||
const WEEKS = i18n.translate('xpack.triggersActionsUI.sections.rulesList.weeksLabel', {
|
||||
defaultMessage: 'weeks',
|
||||
});
|
||||
const MONTHS = i18n.translate('xpack.triggersActionsUI.sections.rulesList.monthsLabel', {
|
||||
defaultMessage: 'months',
|
||||
});
|
||||
|
||||
const INDEFINITELY = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.remainingSnoozeIndefinite',
|
||||
{ defaultMessage: 'Indefinitely' }
|
||||
);
|
|
@ -59,7 +59,7 @@ export const RuleStatusFilter: React.FunctionComponent<RuleStatusFilterProps> =
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel"
|
||||
defaultMessage="Status"
|
||||
defaultMessage="Last response"
|
||||
/>
|
||||
</EuiFilterButton>
|
||||
}
|
||||
|
|
|
@ -427,11 +427,6 @@ describe('rules_list component with items', () => {
|
|||
expect(wrapper.find('EuiBasicTable')).toHaveLength(1);
|
||||
expect(wrapper.find('EuiTableRow')).toHaveLength(mockedRulesData.length);
|
||||
|
||||
// Enabled switch column
|
||||
expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-enabled"]').length).toEqual(
|
||||
mockedRulesData.length
|
||||
);
|
||||
|
||||
// Name and rule type column
|
||||
const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-name"]');
|
||||
expect(ruleNameColumns.length).toEqual(mockedRulesData.length);
|
||||
|
@ -512,10 +507,10 @@ describe('rules_list component with items', () => {
|
|||
'The length of time it took for the rule to run (mm:ss).'
|
||||
);
|
||||
|
||||
// Status column
|
||||
expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length).toEqual(
|
||||
mockedRulesData.length
|
||||
);
|
||||
// Last response column
|
||||
expect(
|
||||
wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastResponse"]').length
|
||||
).toEqual(mockedRulesData.length);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1);
|
||||
expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1);
|
||||
|
@ -536,6 +531,11 @@ describe('rules_list component with items', () => {
|
|||
'License Error'
|
||||
);
|
||||
|
||||
// Status control column
|
||||
expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length).toEqual(
|
||||
mockedRulesData.length
|
||||
);
|
||||
|
||||
// Monitoring column
|
||||
expect(
|
||||
wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"]').length
|
||||
|
@ -727,7 +727,7 @@ describe('rules_list component with items', () => {
|
|||
it('sorts rules when clicking the name column', async () => {
|
||||
await setup();
|
||||
wrapper
|
||||
.find('[data-test-subj="tableHeaderCell_name_1"] .euiTableHeaderButton')
|
||||
.find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
|
@ -746,10 +746,10 @@ describe('rules_list component with items', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('sorts rules when clicking the enabled column', async () => {
|
||||
it('sorts rules when clicking the status control column', async () => {
|
||||
await setup();
|
||||
wrapper
|
||||
.find('[data-test-subj="tableHeaderCell_enabled_0"] .euiTableHeaderButton')
|
||||
.find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
|
|
|
@ -63,6 +63,8 @@ import {
|
|||
loadRuleTypes,
|
||||
disableRule,
|
||||
enableRule,
|
||||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
deleteRules,
|
||||
} from '../../../lib/rule_api';
|
||||
import { loadActionTypes } from '../../../lib/action_connector_api';
|
||||
|
@ -86,7 +88,7 @@ import './rules_list.scss';
|
|||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { ManageLicenseModal } from './manage_license_modal';
|
||||
import { checkRuleTypeEnabled } from '../../../lib/check_rule_type_enabled';
|
||||
import { RuleEnabledSwitch } from './rule_enabled_switch';
|
||||
import { RuleStatusDropdown } from './rule_status_dropdown';
|
||||
import { PercentileSelectablePopover } from './percentile_selectable_popover';
|
||||
import { RuleDurationFormat } from './rule_duration_format';
|
||||
import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils';
|
||||
|
@ -336,6 +338,21 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => {
|
||||
return (
|
||||
<RuleStatusDropdown
|
||||
disableRule={async () => await disableRule({ http, id: item.id })}
|
||||
enableRule={async () => await enableRule({ http, id: item.id })}
|
||||
snoozeRule={async (snoozeEndTime: string | -1) =>
|
||||
await snoozeRule({ http, id: item.id, snoozeEndTime })
|
||||
}
|
||||
unsnoozeRule={async () => await unsnoozeRule({ http, id: item.id })}
|
||||
item={item}
|
||||
onRuleChanged={() => loadRulesData()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAlertExecutionStatus = (
|
||||
executionStatus: AlertExecutionStatus,
|
||||
item: RuleTableItem
|
||||
|
@ -440,26 +457,6 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
|
||||
const getRulesTableColumns = () => {
|
||||
return [
|
||||
{
|
||||
field: 'enabled',
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.enabledTitle',
|
||||
{ defaultMessage: 'Enabled' }
|
||||
),
|
||||
width: '50px',
|
||||
render(_enabled: boolean | undefined, item: RuleTableItem) {
|
||||
return (
|
||||
<RuleEnabledSwitch
|
||||
disableRule={async () => await disableRule({ http, id: item.id })}
|
||||
enableRule={async () => await enableRule({ http, id: item.id })}
|
||||
item={item}
|
||||
onRuleChanged={() => loadRulesData()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
'data-test-subj': 'rulesTableCell-enabled',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate(
|
||||
|
@ -509,19 +506,7 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{link}
|
||||
{rule.enabled && rule.muteAll && (
|
||||
<EuiBadge data-test-subj="mutedActionsBadge" color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.mutedBadge"
|
||||
defaultMessage="Muted"
|
||||
/>
|
||||
</EuiBadge>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <>{link}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -756,16 +741,30 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
},
|
||||
{
|
||||
field: 'executionStatus.status',
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.lastResponseTitle',
|
||||
{ defaultMessage: 'Last response' }
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '120px',
|
||||
'data-test-subj': 'rulesTableCell-lastResponse',
|
||||
render: (_executionStatus: AlertExecutionStatus, item: RuleTableItem) => {
|
||||
return renderAlertExecutionStatus(item.executionStatus, item);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListTable.columns.statusTitle',
|
||||
{ defaultMessage: 'Status' }
|
||||
),
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
width: '120px',
|
||||
width: '200px',
|
||||
'data-test-subj': 'rulesTableCell-status',
|
||||
render: (_executionStatus: AlertExecutionStatus, item: RuleTableItem) => {
|
||||
return renderAlertExecutionStatus(item.executionStatus, item);
|
||||
render: (_enabled: boolean | undefined, item: RuleTableItem) => {
|
||||
return renderRuleStatusDropdown(item.enabled, item);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -97,6 +97,17 @@ export class AlertUtils {
|
|||
return request;
|
||||
}
|
||||
|
||||
public getUnsnoozeRequest(alertId: string) {
|
||||
const request = this.supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(this.space.id)}/internal/alerting/rule/${alertId}/_unsnooze`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('content-type', 'application/json');
|
||||
if (this.user) {
|
||||
return request.auth(this.user.username, this.user.password);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public getMuteAllRequest(alertId: string) {
|
||||
const request = this.supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/_mute_all`)
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../scenarios';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import {
|
||||
AlertUtils,
|
||||
checkAAD,
|
||||
getUrlPrefix,
|
||||
getTestRuleData,
|
||||
ObjectRemover,
|
||||
getConsumerUnauthorizedErrorMessage,
|
||||
getProducerUnauthorizedErrorMessage,
|
||||
} from '../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createUnsnoozeRuleTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('unsnooze', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth });
|
||||
|
||||
describe(scenario.id, () => {
|
||||
it('should handle unsnooze rule request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
connector_type_id: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getUnsnoozeRequest(createdAlert.id);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getConsumerUnauthorizedErrorMessage(
|
||||
'unsnooze',
|
||||
'test.noop',
|
||||
'alertsFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: `Unauthorized to execute actions`,
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(204);
|
||||
expect(response.body).to.eql('');
|
||||
const { body: updatedAlert } = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.expect(200);
|
||||
expect(updatedAlert.snooze_end_time).to.eql(null);
|
||||
expect(updatedAlert.mute_all).to.eql(false);
|
||||
// Ensure AAD isn't broken
|
||||
await checkAAD({
|
||||
supertest,
|
||||
spaceId: space.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle unsnooze rule request appropriately when consumer is the same as producer', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
rule_type_id: 'test.restricted-noop',
|
||||
consumer: 'alertsRestrictedFixture',
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getUnsnoozeRequest(createdAlert.id);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getConsumerUnauthorizedErrorMessage(
|
||||
'unsnooze',
|
||||
'test.restricted-noop',
|
||||
'alertsRestrictedFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(204);
|
||||
expect(response.body).to.eql('');
|
||||
const { body: updatedAlert } = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.expect(200);
|
||||
expect(updatedAlert.snooze_end_time).to.eql(null);
|
||||
expect(updatedAlert.mute_all).to.eql(false);
|
||||
// Ensure AAD isn't broken
|
||||
await checkAAD({
|
||||
supertest,
|
||||
spaceId: space.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle unsnooze rule request appropriately when consumer is not the producer', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
rule_type_id: 'test.unrestricted-noop',
|
||||
consumer: 'alertsFixture',
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getUnsnoozeRequest(createdAlert.id);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getConsumerUnauthorizedErrorMessage(
|
||||
'unsnooze',
|
||||
'test.unrestricted-noop',
|
||||
'alertsFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getProducerUnauthorizedErrorMessage(
|
||||
'unsnooze',
|
||||
'test.unrestricted-noop',
|
||||
'alertsRestrictedFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(204);
|
||||
expect(response.body).to.eql('');
|
||||
const { body: updatedAlert } = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.expect(200);
|
||||
expect(updatedAlert.snooze_end_time).to.eql(null);
|
||||
expect(updatedAlert.mute_all).to.eql(false);
|
||||
// Ensure AAD isn't broken
|
||||
await checkAAD({
|
||||
supertest,
|
||||
spaceId: space.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle unsnooze rule request appropriately when consumer is "alerts"', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
rule_type_id: 'test.restricted-noop',
|
||||
consumer: 'alerts',
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getUnsnoozeRequest(createdAlert.id);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getConsumerUnauthorizedErrorMessage(
|
||||
'unsnooze',
|
||||
'test.restricted-noop',
|
||||
'alerts'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
expect(response.statusCode).to.eql(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getProducerUnauthorizedErrorMessage(
|
||||
'unsnooze',
|
||||
'test.restricted-noop',
|
||||
'alertsRestrictedFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(204);
|
||||
expect(response.body).to.eql('');
|
||||
const { body: updatedAlert } = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(space.id)}/internal/alerting/rule/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.expect(200);
|
||||
expect(updatedAlert.snooze_end_time).to.eql(null);
|
||||
expect(updatedAlert.mute_all).to.eql(false);
|
||||
// Ensure AAD isn't broken
|
||||
await checkAAD({
|
||||
supertest,
|
||||
spaceId: space.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import {
|
||||
AlertUtils,
|
||||
checkAAD,
|
||||
getUrlPrefix,
|
||||
getTestRuleData,
|
||||
ObjectRemover,
|
||||
} from '../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createSnoozeRuleTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('unsnooze', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth });
|
||||
|
||||
it('should handle unsnooze rule request appropriately', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
connector_type_id: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
enabled: false,
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting');
|
||||
|
||||
const response = await alertUtils.getSnoozeRequest(createdAlert.id);
|
||||
|
||||
expect(response.statusCode).to.eql(204);
|
||||
expect(response.body).to.eql('');
|
||||
const { body: updatedAlert } = await supertestWithoutAuth
|
||||
.get(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(200);
|
||||
expect(updatedAlert.snooze_end_time).to.eql(null);
|
||||
expect(updatedAlert.mute_all).to.eql(false);
|
||||
// Ensure AAD isn't broken
|
||||
await checkAAD({
|
||||
supertest,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -162,10 +162,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
|
||||
await testSubjects.click('disableButton');
|
||||
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied(
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'enableSwitch',
|
||||
'true'
|
||||
'statusDropdown',
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -181,10 +181,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('collapsedItemActions');
|
||||
|
||||
await testSubjects.click('disableButton');
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied(
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'enableSwitch',
|
||||
'false'
|
||||
'statusDropdown',
|
||||
'enabled'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -201,9 +201,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('muteButton');
|
||||
|
||||
await retry.tryForTime(30000, async () => {
|
||||
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
|
||||
const muteBadge = await testSubjects.find('mutedActionsBadge');
|
||||
expect(await muteBadge.isDisplayed()).to.eql(true);
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'statusDropdown',
|
||||
'snoozed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -221,9 +223,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('muteButton');
|
||||
|
||||
await retry.tryForTime(30000, async () => {
|
||||
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
|
||||
const muteBadge = await testSubjects.find('mutedActionsBadge');
|
||||
expect(await muteBadge.isDisplayed()).to.eql(true);
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'statusDropdown',
|
||||
'snoozed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -241,8 +245,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
|
||||
await testSubjects.click('muteButton');
|
||||
await retry.tryForTime(30000, async () => {
|
||||
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
|
||||
await testSubjects.missingOrFail('mutedActionsBadge');
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'statusDropdown',
|
||||
'enabled'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -289,9 +296,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
|
||||
|
||||
await retry.tryForTime(30000, async () => {
|
||||
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
|
||||
const muteBadge = await testSubjects.find('mutedActionsBadge');
|
||||
expect(await muteBadge.isDisplayed()).to.eql(true);
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'statusDropdown',
|
||||
'snoozed'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -312,8 +321,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
await testSubjects.existOrFail('muteAll');
|
||||
|
||||
await retry.tryForTime(30000, async () => {
|
||||
await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
|
||||
await testSubjects.missingOrFail('mutedActionsBadge');
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'statusDropdown',
|
||||
'enabled'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -331,10 +343,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
// Enable all button shows after clicking disable all
|
||||
await testSubjects.existOrFail('enableAll');
|
||||
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied(
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'enableSwitch',
|
||||
'false'
|
||||
'statusDropdown',
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -354,10 +366,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
// Disable all button shows after clicking enable all
|
||||
await testSubjects.existOrFail('disableAll');
|
||||
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied(
|
||||
await pageObjects.triggersActionsUI.ensureRuleActionStatusApplied(
|
||||
createdAlert.name,
|
||||
'enableSwitch',
|
||||
'true'
|
||||
'statusDropdown',
|
||||
'enabled'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
|
|||
return {
|
||||
...rowItem,
|
||||
status: $(row)
|
||||
.findTestSubject('rulesTableCell-status')
|
||||
.findTestSubject('rulesTableCell-lastResponse')
|
||||
.find('.euiTableCellContent')
|
||||
.text(),
|
||||
};
|
||||
|
@ -183,16 +183,16 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext)
|
|||
expect(isConfirmationModalVisible).to.eql(true, 'Expect confirmation modal to be visible');
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
},
|
||||
async ensureRuleActionToggleApplied(
|
||||
async ensureRuleActionStatusApplied(
|
||||
ruleName: string,
|
||||
switchName: string,
|
||||
shouldBeCheckedAsString: string
|
||||
controlName: string,
|
||||
expectedStatus: string
|
||||
) {
|
||||
await retry.tryForTime(30000, async () => {
|
||||
await this.searchAlerts(ruleName);
|
||||
const switchControl = await testSubjects.find(switchName);
|
||||
const isChecked = await switchControl.getAttribute('aria-checked');
|
||||
expect(isChecked).to.eql(shouldBeCheckedAsString);
|
||||
const statusControl = await testSubjects.find(controlName);
|
||||
const title = await statusControl.getAttribute('title');
|
||||
expect(title.toLowerCase()).to.eql(expectedStatus.toLowerCase());
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue