mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Alerting] Update rules detail page to resolve SO IDs if necessary (#108091)
* Adding internal resolve API to resolve rules given an ID * Updating after merge * Updating after merge * Adding resolveRule api to client and adding spacesOss plugin dependency * Handling 404 errors by calling resolve. Updating unit tests * Handling aliasMatch and conflict results * Fixing types * Unit tests for spaces oss components * Adding audit event for resolve Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
09f122b478
commit
48ce73db15
22 changed files with 1296 additions and 121 deletions
|
@ -5,7 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';
|
||||
import {
|
||||
SavedObjectAttribute,
|
||||
SavedObjectAttributes,
|
||||
SavedObjectsResolveResponse,
|
||||
} from 'kibana/server';
|
||||
import { AlertNotifyWhenType } from './alert_notify_when_type';
|
||||
|
||||
export type AlertTypeState = Record<string, unknown>;
|
||||
|
@ -76,6 +80,8 @@ export interface Alert<Params extends AlertTypeParams = never> {
|
|||
}
|
||||
|
||||
export type SanitizedAlert<Params extends AlertTypeParams = never> = Omit<Alert<Params>, 'apiKey'>;
|
||||
export type ResolvedSanitizedRule<Params extends AlertTypeParams = never> = SanitizedAlert<Params> &
|
||||
Omit<SavedObjectsResolveResponse, 'saved_object'>;
|
||||
|
||||
export type SanitizedRuleConfig = Pick<
|
||||
SanitizedAlert,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { findRulesRoute } from './find_rules';
|
|||
import { getRuleAlertSummaryRoute } from './get_rule_alert_summary';
|
||||
import { getRuleStateRoute } from './get_rule_state';
|
||||
import { healthRoute } from './health';
|
||||
import { resolveRuleRoute } from './resolve_rule';
|
||||
import { ruleTypesRoute } from './rule_types';
|
||||
import { muteAllRuleRoute } from './mute_all_rule';
|
||||
import { muteAlertRoute } from './mute_alert';
|
||||
|
@ -42,6 +43,7 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
defineLegacyRoutes(opts);
|
||||
createRuleRoute(opts);
|
||||
getRuleRoute(router, licenseState);
|
||||
resolveRuleRoute(router, licenseState);
|
||||
updateRuleRoute(router, licenseState);
|
||||
deleteRuleRoute(router, licenseState);
|
||||
aggregateRulesRoute(router, licenseState);
|
||||
|
|
182
x-pack/plugins/alerting/server/routes/resolve_rule.test.ts
Normal file
182
x-pack/plugins/alerting/server/routes/resolve_rule.test.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 { pick } from 'lodash';
|
||||
import { resolveRuleRoute } from './resolve_rule';
|
||||
import { httpServiceMock } from 'src/core/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { ResolvedSanitizedRule } from '../types';
|
||||
import { AsApiContract } from './lib';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('resolveRuleRoute', () => {
|
||||
const mockedRule: ResolvedSanitizedRule<{
|
||||
bar: boolean;
|
||||
}> = {
|
||||
id: '1',
|
||||
alertTypeId: '1',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '2',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
consumer: 'bar',
|
||||
name: 'abc',
|
||||
tags: ['foo'],
|
||||
enabled: true,
|
||||
muteAll: false,
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
createdBy: '',
|
||||
updatedBy: '',
|
||||
apiKeyOwner: '',
|
||||
throttle: '30s',
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
};
|
||||
|
||||
const resolveResult: AsApiContract<ResolvedSanitizedRule<{ bar: boolean }>> = {
|
||||
...pick(
|
||||
mockedRule,
|
||||
'consumer',
|
||||
'name',
|
||||
'schedule',
|
||||
'tags',
|
||||
'params',
|
||||
'throttle',
|
||||
'enabled',
|
||||
'alias_target_id'
|
||||
),
|
||||
rule_type_id: mockedRule.alertTypeId,
|
||||
notify_when: mockedRule.notifyWhen,
|
||||
mute_all: mockedRule.muteAll,
|
||||
created_by: mockedRule.createdBy,
|
||||
updated_by: mockedRule.updatedBy,
|
||||
api_key_owner: mockedRule.apiKeyOwner,
|
||||
muted_alert_ids: mockedRule.mutedInstanceIds,
|
||||
created_at: mockedRule.createdAt,
|
||||
updated_at: mockedRule.updatedAt,
|
||||
id: mockedRule.id,
|
||||
execution_status: {
|
||||
status: mockedRule.executionStatus.status,
|
||||
last_execution_date: mockedRule.executionStatus.lastExecutionDate,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: mockedRule.actions[0].group,
|
||||
id: mockedRule.actions[0].id,
|
||||
params: mockedRule.actions[0].params,
|
||||
connector_type_id: mockedRule.actions[0].actionTypeId,
|
||||
},
|
||||
],
|
||||
outcome: 'aliasMatch',
|
||||
};
|
||||
|
||||
it('resolves a rule with proper parameters', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
resolveRuleRoute(router, licenseState);
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_resolve"`);
|
||||
|
||||
rulesClient.resolve.mockResolvedValueOnce(mockedRule);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(rulesClient.resolve).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.resolve.mock.calls[0][0].id).toEqual('1');
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: resolveResult,
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures the license allows resolving rules', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
resolveRuleRoute(router, licenseState);
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
rulesClient.resolve.mockResolvedValueOnce(mockedRule);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
it('ensures the license check prevents getting rules', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('OMG');
|
||||
});
|
||||
|
||||
resolveRuleRoute(router, licenseState);
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
rulesClient.resolve.mockResolvedValueOnce(mockedRule);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
params: { id: '1' },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
|
||||
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
});
|
84
x-pack/plugins/alerting/server/routes/resolve_rule.ts
Normal file
84
x-pack/plugins/alerting/server/routes/resolve_rule.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { omit } from 'lodash';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from 'kibana/server';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { verifyAccessAndContext, RewriteResponseCase } from './lib';
|
||||
import {
|
||||
AlertTypeParams,
|
||||
AlertingRequestHandlerContext,
|
||||
INTERNAL_BASE_ALERTING_API_PATH,
|
||||
ResolvedSanitizedRule,
|
||||
} from '../types';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteResponseCase<ResolvedSanitizedRule<AlertTypeParams>> = ({
|
||||
alertTypeId,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
apiKeyOwner,
|
||||
notifyWhen,
|
||||
muteAll,
|
||||
mutedInstanceIds,
|
||||
executionStatus,
|
||||
actions,
|
||||
scheduledTaskId,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rule_type_id: alertTypeId,
|
||||
created_by: createdBy,
|
||||
updated_by: updatedBy,
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
api_key_owner: apiKeyOwner,
|
||||
notify_when: notifyWhen,
|
||||
mute_all: muteAll,
|
||||
muted_alert_ids: mutedInstanceIds,
|
||||
scheduled_task_id: scheduledTaskId,
|
||||
execution_status: executionStatus && {
|
||||
...omit(executionStatus, 'lastExecutionDate'),
|
||||
last_execution_date: executionStatus.lastExecutionDate,
|
||||
},
|
||||
actions: actions.map(({ group, id, actionTypeId, params }) => ({
|
||||
group,
|
||||
id,
|
||||
params,
|
||||
connector_type_id: actionTypeId,
|
||||
})),
|
||||
});
|
||||
|
||||
export const resolveRuleRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_resolve`,
|
||||
validate: {
|
||||
params: paramSchema,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesClient = context.alerting.getRulesClient();
|
||||
const { id } = req.params;
|
||||
const rule = await rulesClient.resolve({ id });
|
||||
return res.ok({
|
||||
body: rewriteBodyRes(rule),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -16,6 +16,7 @@ const createRulesClientMock = () => {
|
|||
aggregate: jest.fn(),
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
resolve: jest.fn(),
|
||||
getAlertState: jest.fn(),
|
||||
find: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
|
|
|
@ -11,6 +11,7 @@ import { AuditEvent } from '../../../security/server';
|
|||
export enum RuleAuditAction {
|
||||
CREATE = 'rule_create',
|
||||
GET = 'rule_get',
|
||||
RESOLVE = 'rule_resolve',
|
||||
UPDATE = 'rule_update',
|
||||
UPDATE_API_KEY = 'rule_update_api_key',
|
||||
ENABLE = 'rule_enable',
|
||||
|
@ -28,6 +29,7 @@ type VerbsTuple = [string, string, string];
|
|||
const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
|
||||
rule_create: ['create', 'creating', 'created'],
|
||||
rule_get: ['access', 'accessing', 'accessed'],
|
||||
rule_resolve: ['access', 'accessing', 'accessed'],
|
||||
rule_update: ['update', 'updating', 'updated'],
|
||||
rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'],
|
||||
rule_enable: ['enable', 'enabling', 'enabled'],
|
||||
|
@ -43,6 +45,7 @@ const eventVerbs: Record<RuleAuditAction, VerbsTuple> = {
|
|||
const eventTypes: Record<RuleAuditAction, EcsEventType> = {
|
||||
rule_create: 'creation',
|
||||
rule_get: 'access',
|
||||
rule_resolve: 'access',
|
||||
rule_update: 'change',
|
||||
rule_update_api_key: 'change',
|
||||
rule_enable: 'change',
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
AlertExecutionStatusValues,
|
||||
AlertNotifyWhenType,
|
||||
AlertTypeParams,
|
||||
ResolvedSanitizedRule,
|
||||
} from '../types';
|
||||
import {
|
||||
validateAlertTypeParams,
|
||||
|
@ -411,6 +412,52 @@ export class RulesClient {
|
|||
);
|
||||
}
|
||||
|
||||
public async resolve<Params extends AlertTypeParams = never>({
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
}): Promise<ResolvedSanitizedRule<Params>> {
|
||||
const {
|
||||
saved_object: result,
|
||||
...resolveResponse
|
||||
} = await this.unsecuredSavedObjectsClient.resolve<RawAlert>('alert', id);
|
||||
try {
|
||||
await this.authorization.ensureAuthorized({
|
||||
ruleTypeId: result.attributes.alertTypeId,
|
||||
consumer: result.attributes.consumer,
|
||||
operation: ReadOperations.Get,
|
||||
entity: AlertingAuthorizationEntity.Rule,
|
||||
});
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.RESOLVE,
|
||||
savedObject: { type: 'alert', id },
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
this.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.RESOLVE,
|
||||
savedObject: { type: 'alert', id },
|
||||
})
|
||||
);
|
||||
|
||||
const rule = this.getAlertFromRaw<Params>(
|
||||
result.id,
|
||||
result.attributes.alertTypeId,
|
||||
result.attributes,
|
||||
result.references
|
||||
);
|
||||
|
||||
return {
|
||||
...rule,
|
||||
...resolveResponse,
|
||||
};
|
||||
}
|
||||
|
||||
public async getAlertState({ id }: { id: string }): Promise<AlertTaskState | void> {
|
||||
const alert = await this.get({ id });
|
||||
await this.authorization.ensureAuthorized({
|
||||
|
|
|
@ -0,0 +1,451 @@
|
|||
/*
|
||||
* 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 { RulesClient, ConstructorOptions } from '../rules_client';
|
||||
import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
import { taskManagerMock } from '../../../../task_manager/server/mocks';
|
||||
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
|
||||
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
|
||||
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
|
||||
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
|
||||
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
|
||||
import { ActionsAuthorization } from '../../../../actions/server';
|
||||
import { httpServerMock } from '../../../../../../src/core/server/mocks';
|
||||
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { RecoveredActionGroup } from '../../../common';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
|
||||
const authorization = alertingAuthorizationMock.create();
|
||||
const actionsAuthorization = actionsAuthorizationMock.create();
|
||||
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
|
||||
|
||||
const kibanaVersion = 'v7.10.0';
|
||||
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
||||
taskManager,
|
||||
ruleTypeRegistry,
|
||||
unsecuredSavedObjectsClient,
|
||||
authorization: (authorization as unknown) as AlertingAuthorization,
|
||||
actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
getEventLogClient: jest.fn(),
|
||||
kibanaVersion,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
|
||||
(auditLogger.log as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
setGlobalDate();
|
||||
|
||||
describe('resolve()', () => {
|
||||
test('calls saved objects client with given params', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
const result = await rulesClient.resolve({ id: '1' });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alias_target_id": "2",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"outcome": "aliasMatch",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
},
|
||||
"schedule": Object {
|
||||
"interval": "10s",
|
||||
},
|
||||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
}
|
||||
`);
|
||||
expect(unsecuredSavedObjectsClient.resolve).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.resolve.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alert",
|
||||
"1",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
|
||||
const injectReferencesFn = jest.fn().mockReturnValue({
|
||||
bar: true,
|
||||
parameterThatIsSavedObjectId: '9',
|
||||
});
|
||||
ruleTypeRegistry.get.mockImplementation(() => ({
|
||||
id: '123',
|
||||
name: 'Test',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
useSavedObjectReferences: {
|
||||
extractReferences: jest.fn(),
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
}));
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
parameterThatIsSavedObjectRef: 'soRef_0',
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
name: 'param:soRef_0',
|
||||
type: 'someSavedObjectType',
|
||||
id: '9',
|
||||
},
|
||||
],
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
const result = await rulesClient.resolve({ id: '1' });
|
||||
|
||||
expect(injectReferencesFn).toHaveBeenCalledWith(
|
||||
{
|
||||
bar: true,
|
||||
parameterThatIsSavedObjectRef: 'soRef_0',
|
||||
},
|
||||
[{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }]
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"group": "default",
|
||||
"id": "1",
|
||||
"params": Object {
|
||||
"foo": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"alertTypeId": "123",
|
||||
"alias_target_id": "2",
|
||||
"createdAt": 2019-02-12T21:01:22.479Z,
|
||||
"id": "1",
|
||||
"notifyWhen": "onActiveAlert",
|
||||
"outcome": "aliasMatch",
|
||||
"params": Object {
|
||||
"bar": true,
|
||||
"parameterThatIsSavedObjectId": "9",
|
||||
},
|
||||
"schedule": Object {
|
||||
"interval": "10s",
|
||||
},
|
||||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test(`throws an error when references aren't found`, async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
await expect(rulesClient.resolve({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Action reference \\"action_0\\" not found in alert id: 1"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws an error if useSavedObjectReferences.injectReferences throws an error', async () => {
|
||||
const injectReferencesFn = jest.fn().mockImplementation(() => {
|
||||
throw new Error('something went wrong!');
|
||||
});
|
||||
ruleTypeRegistry.get.mockImplementation(() => ({
|
||||
id: '123',
|
||||
name: 'Test',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
async executor() {},
|
||||
producer: 'alerts',
|
||||
useSavedObjectReferences: {
|
||||
extractReferences: jest.fn(),
|
||||
injectReferences: injectReferencesFn,
|
||||
},
|
||||
}));
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
parameterThatIsSavedObjectRef: 'soRef_0',
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
notifyWhen: 'onActiveAlert',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
name: 'soRef_0',
|
||||
type: 'someSavedObjectType',
|
||||
id: '9',
|
||||
},
|
||||
],
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
await expect(rulesClient.resolve({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error injecting reference into rule params for rule id 1 - something went wrong!"`
|
||||
);
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: 'myType',
|
||||
consumer: 'myApp',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
});
|
||||
|
||||
test('ensures user is authorised to resolve this type of rule under the consumer', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.resolve({ id: '1' });
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'rule',
|
||||
consumer: 'myApp',
|
||||
operation: 'get',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when user is not authorised to get this type of alert', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
authorization.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "myType" alert for "myApp"`)
|
||||
);
|
||||
|
||||
await expect(rulesClient.resolve({ id: '1' })).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "myType" alert for "myApp"]`
|
||||
);
|
||||
|
||||
expect(authorization.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'rule',
|
||||
consumer: 'myApp',
|
||||
operation: 'get',
|
||||
ruleTypeId: 'myType',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
beforeEach(() => {
|
||||
unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [],
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
});
|
||||
|
||||
test('logs audit event when getting a rule', async () => {
|
||||
const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger });
|
||||
await rulesClient.resolve({ id: '1' });
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'rule_resolve',
|
||||
outcome: 'success',
|
||||
}),
|
||||
kibana: { saved_object: { id: '1', type: 'alert' } },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('logs audit event when not authorised to get a rule', async () => {
|
||||
const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger });
|
||||
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
|
||||
|
||||
await expect(rulesClient.resolve({ id: '1' })).rejects.toThrow();
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
action: 'rule_resolve',
|
||||
outcome: 'failure',
|
||||
}),
|
||||
kibana: {
|
||||
saved_object: {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,7 +8,7 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"optionalPlugins": ["alerting", "features", "home", "spaces"],
|
||||
"requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects"],
|
||||
"requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "spacesOss"],
|
||||
"configPath": ["xpack", "trigger_actions_ui"],
|
||||
"extraPublicDirs": ["public/common", "public/common/constants"],
|
||||
"requiredBundles": ["home", "alerting", "esUiShared", "kibanaReact", "kibanaUtils"]
|
||||
|
|
|
@ -18,6 +18,7 @@ import { ChartsPluginStart } from '../../../../../src/plugins/charts/public';
|
|||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import { PluginStartContract as AlertingStart } from '../../../alerting/public';
|
||||
import type { SpacesPluginStart } from '../../../spaces/public';
|
||||
import type { SpacesOssPluginStart } from '../../../../../src/plugins/spaces_oss/public';
|
||||
|
||||
import { suspendedComponentWithProps } from './lib/suspended_component_with_props';
|
||||
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
|
||||
|
@ -36,6 +37,7 @@ export interface TriggersAndActionsUiServices extends CoreStart {
|
|||
charts: ChartsPluginStart;
|
||||
alerting?: AlertingStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
spacesOss: SpacesOssPluginStart;
|
||||
storage?: Storage;
|
||||
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { AlertExecutionStatus } from '../../../../../alerting/common';
|
||||
import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common';
|
||||
import { Alert, AlertAction } from '../../../types';
|
||||
import { Alert, AlertAction, ResolvedRule } from '../../../types';
|
||||
|
||||
const transformAction: RewriteRequestCase<AlertAction> = ({
|
||||
group,
|
||||
|
@ -59,3 +59,16 @@ export const transformAlert: RewriteRequestCase<Alert> = ({
|
|||
scheduledTaskId,
|
||||
...rest,
|
||||
});
|
||||
|
||||
export const transformResolvedRule: RewriteRequestCase<ResolvedRule> = ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
alias_target_id,
|
||||
outcome,
|
||||
...rest
|
||||
}: any) => {
|
||||
return {
|
||||
...transformAlert(rest),
|
||||
alias_target_id,
|
||||
outcome,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,3 +22,4 @@ export { loadAlertState } from './state';
|
|||
export { unmuteAlertInstance } from './unmute_alert';
|
||||
export { unmuteAlert, unmuteAlerts } from './unmute';
|
||||
export { updateAlert } from './update';
|
||||
export { resolveRule } from './resolve_rule';
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { resolveRule } from './resolve_rule';
|
||||
import uuid from 'uuid';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
describe('resolveRule', () => {
|
||||
test('should call get API with base parameters', async () => {
|
||||
const ruleId = `${uuid.v4()}/`;
|
||||
const ruleIdEncoded = encodeURIComponent(ruleId);
|
||||
const resolvedValue = {
|
||||
id: '1/',
|
||||
params: {
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
groupBy: 'all',
|
||||
threshold: [1000],
|
||||
index: ['.kibana'],
|
||||
timeField: 'canvas-element.@created',
|
||||
},
|
||||
consumer: 'alerts',
|
||||
schedule: { interval: '1m' },
|
||||
tags: ['sdfsdf'],
|
||||
name: 'dfsdfdsf',
|
||||
enabled: true,
|
||||
throttle: '1h',
|
||||
rule_type_id: '.index-threshold',
|
||||
created_by: 'elastic',
|
||||
updated_by: 'elastic',
|
||||
created_at: '2021-04-01T20:29:18.652Z',
|
||||
updated_at: '2021-04-01T20:33:38.260Z',
|
||||
api_key_owner: 'elastic',
|
||||
notify_when: 'onThrottleInterval',
|
||||
mute_all: false,
|
||||
muted_alert_ids: [],
|
||||
scheduled_task_id: '1',
|
||||
execution_status: { status: 'ok', last_execution_date: '2021-04-01T21:16:46.709Z' },
|
||||
actions: [
|
||||
{
|
||||
group: 'threshold met',
|
||||
id: '1',
|
||||
params: { documents: [{ dsfsdf: 1212 }] },
|
||||
connector_type_id: '.index',
|
||||
},
|
||||
],
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
};
|
||||
http.get.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
expect(await resolveRule({ http, ruleId })).toEqual({
|
||||
id: '1/',
|
||||
params: {
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
groupBy: 'all',
|
||||
threshold: [1000],
|
||||
index: ['.kibana'],
|
||||
timeField: 'canvas-element.@created',
|
||||
},
|
||||
consumer: 'alerts',
|
||||
schedule: { interval: '1m' },
|
||||
tags: ['sdfsdf'],
|
||||
name: 'dfsdfdsf',
|
||||
enabled: true,
|
||||
throttle: '1h',
|
||||
alertTypeId: '.index-threshold',
|
||||
createdBy: 'elastic',
|
||||
updatedBy: 'elastic',
|
||||
createdAt: '2021-04-01T20:29:18.652Z',
|
||||
updatedAt: '2021-04-01T20:33:38.260Z',
|
||||
apiKeyOwner: 'elastic',
|
||||
notifyWhen: 'onThrottleInterval',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
scheduledTaskId: '1',
|
||||
executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' },
|
||||
actions: [
|
||||
{
|
||||
group: 'threshold met',
|
||||
id: '1',
|
||||
params: { documents: [{ dsfsdf: 1212 }] },
|
||||
actionTypeId: '.index',
|
||||
},
|
||||
],
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: '2',
|
||||
});
|
||||
expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${ruleIdEncoded}/_resolve`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { ResolvedRule } from '../../../types';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
import { transformResolvedRule } from './common_transformations';
|
||||
|
||||
export async function resolveRule({
|
||||
http,
|
||||
ruleId,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
ruleId: string;
|
||||
}): Promise<ResolvedRule> {
|
||||
const res = await http.get(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}/_resolve`
|
||||
);
|
||||
return transformResolvedRule(res);
|
||||
}
|
|
@ -8,45 +8,150 @@
|
|||
import * as React from 'react';
|
||||
import uuid from 'uuid';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test/jest';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory, createLocation } from 'history';
|
||||
import { ToastsApi } from 'kibana/public';
|
||||
import { AlertDetailsRoute, getAlertData } from './alert_details_route';
|
||||
import { AlertDetailsRoute, getRuleData } from './alert_details_route';
|
||||
import { Alert } from '../../../../types';
|
||||
import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner';
|
||||
import { spacesOssPluginMock } from 'src/plugins/spaces_oss/public/mocks';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
class NotFoundError extends Error {
|
||||
public readonly body: {
|
||||
statusCode: number;
|
||||
name: string;
|
||||
} = {
|
||||
statusCode: 404,
|
||||
name: 'Not found',
|
||||
};
|
||||
|
||||
constructor(message: string | undefined) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
describe('alert_details_route', () => {
|
||||
it('render a loader while fetching data', () => {
|
||||
const alert = mockAlert();
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<AlertDetailsRoute {...mockRouterProps(alert)} {...mockApis()} />
|
||||
).containsMatchingElement(<CenterJustifiedSpinner />)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlertData useEffect handler', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches alert', async () => {
|
||||
const alert = mockAlert();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const spacesOssMock = spacesOssPluginMock.createStartContract();
|
||||
async function setup() {
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useKibanaMock().services.spacesOss = spacesOssMock;
|
||||
}
|
||||
|
||||
it('render a loader while fetching data', () => {
|
||||
const rule = mockRule();
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<AlertDetailsRoute {...mockRouterProps(rule)} {...mockApis()} />
|
||||
).containsMatchingElement(<CenterJustifiedSpinner />)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('redirects to another page if fetched rule is an aliasMatch', async () => {
|
||||
await setup();
|
||||
const rule = mockRule();
|
||||
const { loadAlert, resolveRule } = mockApis();
|
||||
|
||||
loadAlert.mockImplementationOnce(async () => {
|
||||
throw new NotFoundError('OMG');
|
||||
});
|
||||
resolveRule.mockImplementationOnce(async () => ({
|
||||
...rule,
|
||||
id: 'new_id',
|
||||
outcome: 'aliasMatch',
|
||||
alias_target_id: rule.id,
|
||||
}));
|
||||
const wrapper = mountWithIntl(
|
||||
<AlertDetailsRoute
|
||||
{...mockRouterProps(rule)}
|
||||
{...{ ...mockApis(), loadAlert, resolveRule }}
|
||||
/>
|
||||
);
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(loadAlert).toHaveBeenCalledWith(rule.id);
|
||||
expect(resolveRule).toHaveBeenCalledWith(rule.id);
|
||||
expect((spacesOssMock as any).ui.redirectLegacyUrl).toHaveBeenCalledWith(
|
||||
`insightsAndAlerting/triggersActions/rule/new_id`,
|
||||
`rule`
|
||||
);
|
||||
});
|
||||
|
||||
it('shows warning callout if fetched rule is a conflict', async () => {
|
||||
await setup();
|
||||
const rule = mockRule();
|
||||
const ruleType = {
|
||||
id: rule.alertTypeId,
|
||||
name: 'type name',
|
||||
authorizedConsumers: ['consumer'],
|
||||
};
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
|
||||
loadAlert.mockImplementationOnce(async () => {
|
||||
throw new NotFoundError('OMG');
|
||||
});
|
||||
loadAlertTypes.mockImplementationOnce(async () => [ruleType]);
|
||||
loadActionTypes.mockImplementation(async () => []);
|
||||
resolveRule.mockImplementationOnce(async () => ({
|
||||
...rule,
|
||||
id: 'new_id',
|
||||
outcome: 'conflict',
|
||||
alias_target_id: rule.id,
|
||||
}));
|
||||
const wrapper = mountWithIntl(
|
||||
<AlertDetailsRoute
|
||||
{...mockRouterProps(rule)}
|
||||
{...{ ...mockApis(), loadAlert, loadAlertTypes, loadActionTypes, resolveRule }}
|
||||
/>
|
||||
);
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(loadAlert).toHaveBeenCalledWith(rule.id);
|
||||
expect(resolveRule).toHaveBeenCalledWith(rule.id);
|
||||
expect((spacesOssMock as any).ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({
|
||||
currentObjectId: 'new_id',
|
||||
objectNoun: 'rule',
|
||||
otherObjectId: rule.id,
|
||||
otherObjectPath: `insightsAndAlerting/triggersActions/rule/${rule.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuleData useEffect handler', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches rule', async () => {
|
||||
const rule = mockRule();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementationOnce(async () => alert);
|
||||
loadAlert.mockImplementationOnce(async () => rule);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -54,45 +159,47 @@ describe('getAlertData useEffect handler', () => {
|
|||
toastNotifications
|
||||
);
|
||||
|
||||
expect(loadAlert).toHaveBeenCalledWith(alert.id);
|
||||
expect(setAlert).toHaveBeenCalledWith(alert);
|
||||
expect(loadAlert).toHaveBeenCalledWith(rule.id);
|
||||
expect(resolveRule).not.toHaveBeenCalled();
|
||||
expect(setAlert).toHaveBeenCalledWith(rule);
|
||||
});
|
||||
|
||||
it('fetches alert and action types', async () => {
|
||||
const actionType = {
|
||||
it('fetches rule and connector types', async () => {
|
||||
const connectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: actionType.id,
|
||||
actionTypeId: connectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const alertType = {
|
||||
id: alert.alertTypeId,
|
||||
const ruleType = {
|
||||
id: rule.alertTypeId,
|
||||
name: 'type name',
|
||||
};
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementation(async () => alert);
|
||||
loadAlertTypes.mockImplementation(async () => [alertType]);
|
||||
loadActionTypes.mockImplementation(async () => [actionType]);
|
||||
loadAlert.mockImplementation(async () => rule);
|
||||
loadAlertTypes.mockImplementation(async () => [ruleType]);
|
||||
loadActionTypes.mockImplementation(async () => [connectorType]);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -102,29 +209,76 @@ describe('getAlertData useEffect handler', () => {
|
|||
|
||||
expect(loadAlertTypes).toHaveBeenCalledTimes(1);
|
||||
expect(loadActionTypes).toHaveBeenCalledTimes(1);
|
||||
expect(resolveRule).not.toHaveBeenCalled();
|
||||
|
||||
expect(setAlertType).toHaveBeenCalledWith(alertType);
|
||||
expect(setActionTypes).toHaveBeenCalledWith([actionType]);
|
||||
expect(setAlert).toHaveBeenCalledWith(rule);
|
||||
expect(setAlertType).toHaveBeenCalledWith(ruleType);
|
||||
expect(setActionTypes).toHaveBeenCalledWith([connectorType]);
|
||||
});
|
||||
|
||||
it('displays an error if the alert isnt found', async () => {
|
||||
const actionType = {
|
||||
it('fetches rule using resolve if initial GET results in a 404 error', async () => {
|
||||
const connectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: actionType.id,
|
||||
actionTypeId: connectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementationOnce(async () => {
|
||||
throw new NotFoundError('OMG');
|
||||
});
|
||||
resolveRule.mockImplementationOnce(async () => rule);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
setActionTypes,
|
||||
toastNotifications
|
||||
);
|
||||
|
||||
expect(loadAlert).toHaveBeenCalledWith(rule.id);
|
||||
expect(resolveRule).toHaveBeenCalledWith(rule.id);
|
||||
expect(setAlert).toHaveBeenCalledWith(rule);
|
||||
});
|
||||
|
||||
it('displays an error if fetching the rule results in a non-404 error', async () => {
|
||||
const connectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: connectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementation(async () => {
|
||||
|
@ -134,10 +288,11 @@ describe('getAlertData useEffect handler', () => {
|
|||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -150,40 +305,41 @@ describe('getAlertData useEffect handler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('displays an error if the alert type isnt loaded', async () => {
|
||||
const actionType = {
|
||||
it('displays an error if the rule type isnt loaded', async () => {
|
||||
const connectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: actionType.id,
|
||||
actionTypeId: connectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementation(async () => alert);
|
||||
loadAlert.mockImplementation(async () => rule);
|
||||
|
||||
loadAlertTypes.mockImplementation(async () => {
|
||||
throw new Error('OMG no alert type');
|
||||
throw new Error('OMG no rule type');
|
||||
});
|
||||
loadActionTypes.mockImplementation(async () => [actionType]);
|
||||
loadActionTypes.mockImplementation(async () => [connectorType]);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -192,48 +348,49 @@ describe('getAlertData useEffect handler', () => {
|
|||
);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledWith({
|
||||
title: 'Unable to load rule: OMG no alert type',
|
||||
title: 'Unable to load rule: OMG no rule type',
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an error if the action type isnt loaded', async () => {
|
||||
const actionType = {
|
||||
it('displays an error if the connector type isnt loaded', async () => {
|
||||
const connectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: actionType.id,
|
||||
actionTypeId: connectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
const alertType = {
|
||||
id: alert.alertTypeId,
|
||||
const ruleType = {
|
||||
id: rule.alertTypeId,
|
||||
name: 'type name',
|
||||
};
|
||||
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementation(async () => alert);
|
||||
loadAlert.mockImplementation(async () => rule);
|
||||
|
||||
loadAlertTypes.mockImplementation(async () => [alertType]);
|
||||
loadAlertTypes.mockImplementation(async () => [ruleType]);
|
||||
loadActionTypes.mockImplementation(async () => {
|
||||
throw new Error('OMG no action type');
|
||||
throw new Error('OMG no connector type');
|
||||
});
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -242,46 +399,47 @@ describe('getAlertData useEffect handler', () => {
|
|||
);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledWith({
|
||||
title: 'Unable to load rule: OMG no action type',
|
||||
title: 'Unable to load rule: OMG no connector type',
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an error if the alert type isnt found', async () => {
|
||||
const actionType = {
|
||||
it('displays an error if the rule type isnt found', async () => {
|
||||
const connectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: actionType.id,
|
||||
actionTypeId: connectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertType = {
|
||||
const ruleType = {
|
||||
id: uuid.v4(),
|
||||
name: 'type name',
|
||||
};
|
||||
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementation(async () => alert);
|
||||
loadAlertTypes.mockImplementation(async () => [alertType]);
|
||||
loadActionTypes.mockImplementation(async () => [actionType]);
|
||||
loadAlert.mockImplementation(async () => rule);
|
||||
loadAlertTypes.mockImplementation(async () => [ruleType]);
|
||||
loadActionTypes.mockImplementation(async () => [connectorType]);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -290,57 +448,58 @@ describe('getAlertData useEffect handler', () => {
|
|||
);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledWith({
|
||||
title: `Unable to load rule: Invalid Alert Type: ${alert.alertTypeId}`,
|
||||
title: `Unable to load rule: Invalid Rule Type: ${rule.alertTypeId}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('displays an error if an action type isnt found', async () => {
|
||||
const availableActionType = {
|
||||
const availableConnectorType = {
|
||||
id: '.server-log',
|
||||
name: 'Server log',
|
||||
enabled: true,
|
||||
};
|
||||
const missingActionType = {
|
||||
const missingConnectorType = {
|
||||
id: '.noop',
|
||||
name: 'No Op',
|
||||
enabled: true,
|
||||
};
|
||||
const alert = mockAlert({
|
||||
const rule = mockRule({
|
||||
actions: [
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: availableActionType.id,
|
||||
actionTypeId: availableConnectorType.id,
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
group: '',
|
||||
id: uuid.v4(),
|
||||
actionTypeId: missingActionType.id,
|
||||
actionTypeId: missingConnectorType.id,
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertType = {
|
||||
const ruleType = {
|
||||
id: uuid.v4(),
|
||||
name: 'type name',
|
||||
};
|
||||
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis();
|
||||
const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis();
|
||||
const { setAlert, setAlertType, setActionTypes } = mockStateSetter();
|
||||
|
||||
loadAlert.mockImplementation(async () => alert);
|
||||
loadAlertTypes.mockImplementation(async () => [alertType]);
|
||||
loadActionTypes.mockImplementation(async () => [availableActionType]);
|
||||
loadAlert.mockImplementation(async () => rule);
|
||||
loadAlertTypes.mockImplementation(async () => [ruleType]);
|
||||
loadActionTypes.mockImplementation(async () => [availableConnectorType]);
|
||||
|
||||
const toastNotifications = ({
|
||||
addDanger: jest.fn(),
|
||||
} as unknown) as ToastsApi;
|
||||
await getAlertData(
|
||||
alert.id,
|
||||
await getRuleData(
|
||||
rule.id,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
|
@ -349,7 +508,7 @@ describe('getAlertData useEffect handler', () => {
|
|||
);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1);
|
||||
expect(toastNotifications.addDanger).toHaveBeenCalledWith({
|
||||
title: `Unable to load rule: Invalid Action Type: ${missingActionType.id}`,
|
||||
title: `Unable to load rule: Invalid Connector Type: ${missingConnectorType.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -359,6 +518,7 @@ function mockApis() {
|
|||
loadAlert: jest.fn(),
|
||||
loadAlertTypes: jest.fn(),
|
||||
loadActionTypes: jest.fn(),
|
||||
resolveRule: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -370,23 +530,23 @@ function mockStateSetter() {
|
|||
};
|
||||
}
|
||||
|
||||
function mockRouterProps(alert: Alert) {
|
||||
function mockRouterProps(rule: Alert) {
|
||||
return {
|
||||
match: {
|
||||
isExact: false,
|
||||
path: `/rule/${alert.id}`,
|
||||
path: `/rule/${rule.id}`,
|
||||
url: '',
|
||||
params: { ruleId: alert.id },
|
||||
params: { ruleId: rule.id },
|
||||
},
|
||||
history: createMemoryHistory(),
|
||||
location: createLocation(`/rule/${alert.id}`),
|
||||
location: createLocation(`/rule/${rule.id}`),
|
||||
};
|
||||
}
|
||||
function mockAlert(overloads: Partial<Alert> = {}): Alert {
|
||||
function mockRule(overloads: Partial<Alert> = {}): Alert {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
enabled: true,
|
||||
name: `alert-${uuid.v4()}`,
|
||||
name: `rule-${uuid.v4()}`,
|
||||
tags: [],
|
||||
alertTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
|
|
|
@ -9,7 +9,8 @@ import { i18n } from '@kbn/i18n';
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { ToastsApi } from 'kibana/public';
|
||||
import { Alert, AlertType, ActionType } from '../../../../types';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { Alert, AlertType, ActionType, ResolvedRule } from '../../../../types';
|
||||
import { AlertDetailsWithApi as AlertDetails } from './alert_details';
|
||||
import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators';
|
||||
import {
|
||||
|
@ -27,7 +28,7 @@ type AlertDetailsRouteProps = RouteComponentProps<{
|
|||
ruleId: string;
|
||||
}> &
|
||||
Pick<ActionApis, 'loadActionTypes'> &
|
||||
Pick<AlertApis, 'loadAlert' | 'loadAlertTypes'>;
|
||||
Pick<AlertApis, 'loadAlert' | 'loadAlertTypes' | 'resolveRule'>;
|
||||
|
||||
export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps> = ({
|
||||
match: {
|
||||
|
@ -36,63 +37,127 @@ export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps>
|
|||
loadAlert,
|
||||
loadAlertTypes,
|
||||
loadActionTypes,
|
||||
resolveRule,
|
||||
}) => {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
spacesOss,
|
||||
} = useKibana().services;
|
||||
|
||||
const [alert, setAlert] = useState<Alert | null>(null);
|
||||
const { basePath } = http;
|
||||
|
||||
const [alert, setAlert] = useState<Alert | ResolvedRule | null>(null);
|
||||
const [alertType, setAlertType] = useState<AlertType | null>(null);
|
||||
const [actionTypes, setActionTypes] = useState<ActionType[] | null>(null);
|
||||
const [refreshToken, requestRefresh] = React.useState<number>();
|
||||
useEffect(() => {
|
||||
getAlertData(
|
||||
getRuleData(
|
||||
ruleId,
|
||||
loadAlert,
|
||||
loadAlertTypes,
|
||||
resolveRule,
|
||||
loadActionTypes,
|
||||
setAlert,
|
||||
setAlertType,
|
||||
setActionTypes,
|
||||
toasts
|
||||
);
|
||||
}, [ruleId, http, loadActionTypes, loadAlert, loadAlertTypes, toasts, refreshToken]);
|
||||
}, [ruleId, http, loadActionTypes, loadAlert, loadAlertTypes, resolveRule, toasts, refreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (alert) {
|
||||
const outcome = (alert as ResolvedRule).outcome;
|
||||
if (outcome === 'aliasMatch' && spacesOss.isSpacesAvailable) {
|
||||
// This rule has been resolved from a legacy URL - redirect the user to the new URL and display a toast.
|
||||
const path = basePath.prepend(`insightsAndAlerting/triggersActions/rule/${alert.id}`);
|
||||
spacesOss.ui.redirectLegacyUrl(
|
||||
path,
|
||||
i18n.translate('xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', {
|
||||
defaultMessage: 'rule',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [alert]);
|
||||
|
||||
const getLegacyUrlConflictCallout = () => {
|
||||
const outcome = (alert as ResolvedRule).outcome;
|
||||
const aliasTargetId = (alert as ResolvedRule).alias_target_id;
|
||||
if (outcome === 'conflict' && aliasTargetId && spacesOss.isSpacesAvailable) {
|
||||
// We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a
|
||||
// callout with a warning for the user, and provide a way for them to navigate to the other rule.
|
||||
const otherRulePath = basePath.prepend(
|
||||
`insightsAndAlerting/triggersActions/rule/${aliasTargetId}`
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{spacesOss.ui.components.getLegacyUrlConflict({
|
||||
objectNoun: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun',
|
||||
{
|
||||
defaultMessage: 'rule',
|
||||
}
|
||||
),
|
||||
currentObjectId: alert?.id!,
|
||||
otherObjectId: aliasTargetId,
|
||||
otherObjectPath: otherRulePath,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return alert && alertType && actionTypes ? (
|
||||
<AlertDetails
|
||||
alert={alert}
|
||||
alertType={alertType}
|
||||
actionTypes={actionTypes}
|
||||
requestRefresh={async () => requestRefresh(Date.now())}
|
||||
/>
|
||||
<>
|
||||
{getLegacyUrlConflictCallout()}
|
||||
<AlertDetails
|
||||
alert={alert}
|
||||
alertType={alertType}
|
||||
actionTypes={actionTypes}
|
||||
requestRefresh={async () => requestRefresh(Date.now())}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<CenterJustifiedSpinner />
|
||||
);
|
||||
};
|
||||
|
||||
export async function getAlertData(
|
||||
alertId: string,
|
||||
export async function getRuleData(
|
||||
ruleId: string,
|
||||
loadAlert: AlertApis['loadAlert'],
|
||||
loadAlertTypes: AlertApis['loadAlertTypes'],
|
||||
resolveRule: AlertApis['resolveRule'],
|
||||
loadActionTypes: ActionApis['loadActionTypes'],
|
||||
setAlert: React.Dispatch<React.SetStateAction<Alert | null>>,
|
||||
setAlert: React.Dispatch<React.SetStateAction<Alert | ResolvedRule | null>>,
|
||||
setAlertType: React.Dispatch<React.SetStateAction<AlertType | null>>,
|
||||
setActionTypes: React.Dispatch<React.SetStateAction<ActionType[] | null>>,
|
||||
toasts: Pick<ToastsApi, 'addDanger'>
|
||||
) {
|
||||
try {
|
||||
const loadedAlert = await loadAlert(alertId);
|
||||
setAlert(loadedAlert);
|
||||
let loadedRule: Alert | ResolvedRule;
|
||||
try {
|
||||
loadedRule = await loadAlert(ruleId);
|
||||
} catch (err) {
|
||||
// Try resolving this rule id if the error is a 404, otherwise re-throw
|
||||
if (err?.body?.statusCode !== 404) {
|
||||
throw err;
|
||||
}
|
||||
loadedRule = await resolveRule(ruleId);
|
||||
}
|
||||
setAlert(loadedRule);
|
||||
|
||||
const [loadedAlertType, loadedActionTypes] = await Promise.all<AlertType, ActionType[]>([
|
||||
loadAlertTypes()
|
||||
.then((types) => types.find((type) => type.id === loadedAlert.alertTypeId))
|
||||
.then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)),
|
||||
.then((types) => types.find((type) => type.id === loadedRule.alertTypeId))
|
||||
.then(throwIfAbsent(`Invalid Rule Type: ${loadedRule.alertTypeId}`)),
|
||||
loadActionTypes().then(
|
||||
throwIfIsntContained(
|
||||
new Set(loadedAlert.actions.map((action) => action.actionTypeId)),
|
||||
(requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`,
|
||||
new Set(loadedRule.actions.map((action) => action.actionTypeId)),
|
||||
(requiredActionType: string) => `Invalid Connector Type: ${requiredActionType}`,
|
||||
(action: ActionType) => action.id
|
||||
)
|
||||
),
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('with_bulk_alert_api_operations', () => {
|
|||
expect(typeof props.deleteAlert).toEqual('function');
|
||||
expect(typeof props.loadAlert).toEqual('function');
|
||||
expect(typeof props.loadAlertTypes).toEqual('function');
|
||||
expect(typeof props.resolveRule).toEqual('function');
|
||||
return <div />;
|
||||
};
|
||||
|
||||
|
@ -220,6 +221,24 @@ describe('with_bulk_alert_api_operations', () => {
|
|||
expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http });
|
||||
});
|
||||
|
||||
it('resolveRule calls the resolveRule api', () => {
|
||||
const { http } = useKibanaMock().services;
|
||||
const ComponentToExtend = ({
|
||||
resolveRule,
|
||||
ruleId,
|
||||
}: ComponentOpts & { ruleId: Alert['id'] }) => {
|
||||
return <button onClick={() => resolveRule(ruleId)}>{'call api'}</button>;
|
||||
};
|
||||
|
||||
const ExtendedComponent = withBulkAlertOperations(ComponentToExtend);
|
||||
const ruleId = uuid.v4();
|
||||
const component = mount(<ExtendedComponent ruleId={ruleId} />);
|
||||
component.find('button').simulate('click');
|
||||
|
||||
expect(alertApi.resolveRule).toHaveBeenCalledTimes(1);
|
||||
expect(alertApi.resolveRule).toHaveBeenCalledWith({ ruleId, http });
|
||||
});
|
||||
|
||||
it('loadAlertTypes calls the loadAlertTypes api', () => {
|
||||
const { http } = useKibanaMock().services;
|
||||
const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
AlertTaskState,
|
||||
AlertInstanceSummary,
|
||||
AlertingFrameworkHealth,
|
||||
ResolvedRule,
|
||||
} from '../../../../types';
|
||||
import {
|
||||
deleteAlerts,
|
||||
|
@ -31,6 +32,7 @@ import {
|
|||
loadAlertInstanceSummary,
|
||||
loadAlertTypes,
|
||||
alertingFrameworkHealth,
|
||||
resolveRule,
|
||||
} from '../../../lib/alert_api';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
|
@ -62,6 +64,7 @@ export interface ComponentOpts {
|
|||
loadAlertInstanceSummary: (id: Alert['id']) => Promise<AlertInstanceSummary>;
|
||||
loadAlertTypes: () => Promise<AlertType[]>;
|
||||
getHealth: () => Promise<AlertingFrameworkHealth>;
|
||||
resolveRule: (id: Alert['id']) => Promise<ResolvedRule>;
|
||||
}
|
||||
|
||||
export type PropsWithOptionalApiHandlers<T> = Omit<T, keyof ComponentOpts> & Partial<ComponentOpts>;
|
||||
|
@ -132,6 +135,7 @@ export function withBulkAlertOperations<T>(
|
|||
loadAlertInstanceSummary({ http, alertId })
|
||||
}
|
||||
loadAlertTypes={async () => loadAlertTypes({ http })}
|
||||
resolveRule={async (ruleId: Alert['id']) => resolveRule({ http, ruleId })}
|
||||
getHealth={async () => alertingFrameworkHealth({ http })}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React from 'react';
|
||||
import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
import { spacesOssPluginMock } from '../../../../../../../src/plugins/spaces_oss/public/mocks';
|
||||
import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { TriggersAndActionsUiServices } from '../../../application/app';
|
||||
|
@ -45,6 +45,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => {
|
|||
element: ({
|
||||
style: { cursor: 'pointer' },
|
||||
} as unknown) as HTMLElement,
|
||||
spacesOss: spacesOssPluginMock.createStartContract(),
|
||||
} as TriggersAndActionsUiServices;
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import { PluginStartContract as AlertingStart } from '../../alerting/public';
|
|||
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
|
||||
import { Storage } from '../../../../src/plugins/kibana_utils/public';
|
||||
import type { SpacesPluginStart } from '../../spaces/public';
|
||||
import type { SpacesOssPluginStart } from '../../../../src/plugins/spaces_oss/public';
|
||||
|
||||
import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout';
|
||||
import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout';
|
||||
|
@ -73,6 +74,7 @@ interface PluginsStart {
|
|||
charts: ChartsPluginStart;
|
||||
alerting?: AlertingStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
spacesOss: SpacesOssPluginStart;
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
features: FeaturesPluginStart;
|
||||
}
|
||||
|
@ -148,6 +150,7 @@ export class Plugin
|
|||
charts: pluginsStart.charts,
|
||||
alerting: pluginsStart.alerting,
|
||||
spaces: pluginsStart.spaces,
|
||||
spacesOss: pluginsStart.spacesOss,
|
||||
element: params.element,
|
||||
storage: new Storage(window.localStorage),
|
||||
setBreadcrumbs: params.setBreadcrumbs,
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
ActionGroup,
|
||||
AlertActionParam,
|
||||
SanitizedAlert,
|
||||
ResolvedSanitizedRule,
|
||||
AlertAction,
|
||||
AlertAggregations,
|
||||
AlertTaskState,
|
||||
|
@ -40,6 +41,7 @@ import {
|
|||
// In Triggers and Actions we treat all `Alert`s as `SanitizedAlert<AlertTypeParams>`
|
||||
// so the `Params` is a black-box of Record<string, unknown>
|
||||
type Alert = SanitizedAlert<AlertTypeParams>;
|
||||
type ResolvedRule = ResolvedSanitizedRule<AlertTypeParams>;
|
||||
|
||||
export {
|
||||
Alert,
|
||||
|
@ -52,6 +54,7 @@ export {
|
|||
AlertingFrameworkHealth,
|
||||
AlertNotifyWhenType,
|
||||
AlertTypeParams,
|
||||
ResolvedRule,
|
||||
};
|
||||
export {
|
||||
ActionType,
|
||||
|
|
|
@ -24,5 +24,6 @@
|
|||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/management/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/spaces_oss/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue