mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
This PR: 1. Adds a callout on the Alerting UI when security is enabled but TLS is not 2. Cleans up displayed error message when creation fails due to TLS being switched off
This commit is contained in:
parent
c8590d95b8
commit
b5e271f126
33 changed files with 809 additions and 103 deletions
|
@ -25,6 +25,7 @@ import {
|
|||
AppMountParameters,
|
||||
CoreStart,
|
||||
IUiSettingsClient,
|
||||
DocLinksStart,
|
||||
ToastsSetup,
|
||||
} from '../../../src/core/public';
|
||||
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
|
||||
|
@ -45,6 +46,7 @@ export interface AlertingExampleComponentParams {
|
|||
data: DataPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
docLinks: DocLinksStart;
|
||||
toastNotifications: ToastsSetup;
|
||||
}
|
||||
|
||||
|
@ -88,7 +90,7 @@ const AlertingExampleApp = (deps: AlertingExampleComponentParams) => {
|
|||
};
|
||||
|
||||
export const renderApp = (
|
||||
{ application, notifications, http, uiSettings }: CoreStart,
|
||||
{ application, notifications, http, uiSettings, docLinks }: CoreStart,
|
||||
deps: AlertingExamplePublicStartDeps,
|
||||
{ appBasePath, element }: AppMountParameters
|
||||
) => {
|
||||
|
@ -99,6 +101,7 @@ export const renderApp = (
|
|||
toastNotifications={notifications.toasts}
|
||||
http={http}
|
||||
uiSettings={uiSettings}
|
||||
docLinks={docLinks}
|
||||
{...deps}
|
||||
/>,
|
||||
element
|
||||
|
|
|
@ -33,6 +33,7 @@ export const CreateAlert = ({
|
|||
triggers_actions_ui,
|
||||
charts,
|
||||
uiSettings,
|
||||
docLinks,
|
||||
data,
|
||||
toastNotifications,
|
||||
}: AlertingExampleComponentParams) => {
|
||||
|
@ -56,6 +57,7 @@ export const CreateAlert = ({
|
|||
alertTypeRegistry: triggers_actions_ui.alertTypeRegistry,
|
||||
toastNotifications,
|
||||
uiSettings,
|
||||
docLinks,
|
||||
charts,
|
||||
dataFieldsFormats: data.fieldFormats,
|
||||
}}
|
||||
|
|
|
@ -172,6 +172,7 @@ export class ApmPlugin
|
|||
<AlertsContextProvider
|
||||
value={{
|
||||
http: core.http,
|
||||
docLinks: core.docLinks,
|
||||
toastNotifications: core.notifications.toasts,
|
||||
actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry,
|
||||
alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry
|
||||
|
|
|
@ -17,6 +17,7 @@ export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
|
|||
notifications,
|
||||
triggers_actions_ui: { actionTypeRegistry, alertTypeRegistry },
|
||||
uiSettings,
|
||||
docLinks,
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
|
@ -26,6 +27,7 @@ export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
|
|||
actionTypeRegistry,
|
||||
alertTypeRegistry,
|
||||
charts,
|
||||
docLinks,
|
||||
dataFieldsFormats: fieldFormats,
|
||||
http,
|
||||
toastNotifications: notifications?.toasts,
|
||||
|
|
|
@ -10,4 +10,13 @@ export * from './alert_instance';
|
|||
export * from './alert_task_instance';
|
||||
export * from './alert_navigation';
|
||||
|
||||
export interface ActionGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AlertingFrameworkHealth {
|
||||
isSufficientlySecure: boolean;
|
||||
}
|
||||
|
||||
export const BASE_ALERT_API_PATH = '/api/alert';
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
unmuteAllAlertRoute,
|
||||
muteAlertInstanceRoute,
|
||||
unmuteAlertInstanceRoute,
|
||||
healthRoute,
|
||||
} from './routes';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import {
|
||||
|
@ -173,6 +174,7 @@ export class AlertingPlugin {
|
|||
unmuteAllAlertRoute(router, this.licenseState);
|
||||
muteAlertInstanceRoute(router, this.licenseState);
|
||||
unmuteAlertInstanceRoute(router, this.licenseState);
|
||||
healthRoute(router, this.licenseState);
|
||||
|
||||
return {
|
||||
registerType: alertTypeRegistry.register.bind(alertTypeRegistry),
|
||||
|
|
|
@ -10,13 +10,14 @@ import { httpServerMock } from '../../../../../src/core/server/mocks';
|
|||
import { alertsClientMock } from '../alerts_client.mock';
|
||||
|
||||
export function mockHandlerArguments(
|
||||
{ alertsClient, listTypes: listTypesRes = [] }: any,
|
||||
{ alertsClient, listTypes: listTypesRes = [], elasticsearch }: any,
|
||||
req: any,
|
||||
res?: Array<MethodKeysOf<KibanaResponseFactory>>
|
||||
): [RequestHandlerContext, KibanaRequest<any, any, any, any>, KibanaResponseFactory] {
|
||||
const listTypes = jest.fn(() => listTypesRes);
|
||||
return [
|
||||
({
|
||||
core: { elasticsearch },
|
||||
alerting: {
|
||||
listTypes,
|
||||
getAlertsClient() {
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { LicenseState } from '../lib/license_state';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { validateDurationSchema } from '../lib';
|
||||
import { handleDisabledApiKeysError } from './lib/error_handler';
|
||||
import { Alert, BASE_ALERT_API_PATH } from '../types';
|
||||
|
||||
export const bodySchema = schema.object({
|
||||
|
@ -50,22 +51,24 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, TypeOf<typeof bodySchema>, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, TypeOf<typeof bodySchema>, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const alert = req.body;
|
||||
const alertRes: Alert = await alertsClient.create({ data: alert });
|
||||
return res.ok({
|
||||
body: alertRes,
|
||||
});
|
||||
})
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const alert = req.body;
|
||||
const alertRes: Alert = await alertsClient.create({ data: alert });
|
||||
return res.ok({
|
||||
body: alertRes,
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { LicenseState } from '../lib/license_state';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { BASE_ALERT_API_PATH } from '../../common';
|
||||
import { handleDisabledApiKeysError } from './lib/error_handler';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
|
@ -31,19 +32,21 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
await alertsClient.enable({ id });
|
||||
return res.noContent();
|
||||
})
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
await alertsClient.enable({ id });
|
||||
return res.noContent();
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
171
x-pack/plugins/alerting/server/routes/health.test.ts
Normal file
171
x-pack/plugins/alerting/server/routes/health.test.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { healthRoute } from './health';
|
||||
import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockLicenseState } from '../lib/license_state.mock';
|
||||
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('healthRoute', () => {
|
||||
it('registers the route', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
|
||||
const [config] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/alert/_health"`);
|
||||
});
|
||||
|
||||
it('queries the usage api', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
|
||||
expect(elasticsearch.adminClient.callAsInternalUser.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"transport.request",
|
||||
Object {
|
||||
"method": "GET",
|
||||
"path": "/_xpack/usage",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: {} }));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(
|
||||
Promise.resolve({ security: { enabled: true } })
|
||||
);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"isSufficientlySecure": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(
|
||||
Promise.resolve({ security: { enabled: true, ssl: {} } })
|
||||
);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"isSufficientlySecure": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('evaluates security and tls enabled to mean that the user can generate keys', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(
|
||||
Promise.resolve({ security: { enabled: true, ssl: { http: { enabled: true } } } })
|
||||
);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
67
x-pack/plugins/alerting/server/routes/health.ts
Normal file
67
x-pack/plugins/alerting/server/routes/health.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IRouter,
|
||||
RequestHandlerContext,
|
||||
KibanaRequest,
|
||||
IKibanaResponse,
|
||||
KibanaResponseFactory,
|
||||
} from 'kibana/server';
|
||||
import { LicenseState } from '../lib/license_state';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { AlertingFrameworkHealth } from '../types';
|
||||
|
||||
interface XPackUsageSecurity {
|
||||
security?: {
|
||||
enabled?: boolean;
|
||||
ssl?: {
|
||||
http?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function healthRoute(router: IRouter, licenseState: LicenseState) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/alert/_health',
|
||||
validate: false,
|
||||
},
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
try {
|
||||
const {
|
||||
security: {
|
||||
enabled: isSecurityEnabled = false,
|
||||
ssl: { http: { enabled: isTLSEnabled = false } = {} } = {},
|
||||
} = {},
|
||||
}: XPackUsageSecurity = await context.core.elasticsearch.adminClient
|
||||
// `transport.request` is potentially unsafe when combined with untrusted user input.
|
||||
// Do not augment with such input.
|
||||
.callAsInternalUser('transport.request', {
|
||||
method: 'GET',
|
||||
path: '/_xpack/usage',
|
||||
});
|
||||
|
||||
const frameworkHealth: AlertingFrameworkHealth = {
|
||||
isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
|
||||
};
|
||||
|
||||
return res.ok({
|
||||
body: frameworkHealth,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.badRequest({ body: error });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
|
@ -18,3 +18,4 @@ export { muteAlertInstanceRoute } from './mute_instance';
|
|||
export { unmuteAlertInstanceRoute } from './unmute_instance';
|
||||
export { muteAllAlertRoute } from './mute_all';
|
||||
export { unmuteAllAlertRoute } from './unmute_all';
|
||||
export { healthRoute } from './health';
|
||||
|
|
47
x-pack/plugins/alerting/server/routes/lib/error_handler.ts
Normal file
47
x-pack/plugins/alerting/server/routes/lib/error_handler.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
RequestHandler,
|
||||
KibanaRequest,
|
||||
KibanaResponseFactory,
|
||||
RequestHandlerContext,
|
||||
RouteMethod,
|
||||
} from 'kibana/server';
|
||||
|
||||
export function handleDisabledApiKeysError<P, Q, B>(
|
||||
handler: RequestHandler<P, Q, B>
|
||||
): RequestHandler<P, Q, B> {
|
||||
return async (
|
||||
context: RequestHandlerContext,
|
||||
request: KibanaRequest<P, Q, B, RouteMethod>,
|
||||
response: KibanaResponseFactory
|
||||
) => {
|
||||
try {
|
||||
return await handler(context, request, response);
|
||||
} catch (e) {
|
||||
if (isApiKeyDisabledError(e)) {
|
||||
return response.badRequest({
|
||||
body: new Error(
|
||||
i18n.translate('xpack.alerting.api.error.disabledApiKeys', {
|
||||
defaultMessage: 'Alerting relies upon API keys which appear to be disabled',
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isApiKeyDisabledError(e: Error) {
|
||||
return e?.message?.includes('api keys are not enabled') ?? false;
|
||||
}
|
||||
|
||||
export function isSecurityPluginDisabledError(e: Error) {
|
||||
return e?.message?.includes('no handler found') ?? false;
|
||||
}
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { LicenseState } from '../lib/license_state';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { validateDurationSchema } from '../lib';
|
||||
import { handleDisabledApiKeysError } from './lib/error_handler';
|
||||
import { BASE_ALERT_API_PATH } from '../../common';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
|
@ -52,24 +53,26 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) =>
|
|||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<TypeOf<typeof paramSchema>, any, TypeOf<typeof bodySchema>, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
const { name, actions, params, schedule, tags, throttle } = req.body;
|
||||
return res.ok({
|
||||
body: await alertsClient.update({
|
||||
id,
|
||||
data: { name, actions, params, schedule, tags, throttle },
|
||||
}),
|
||||
});
|
||||
})
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<TypeOf<typeof paramSchema>, any, TypeOf<typeof bodySchema>, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
const { name, actions, params, schedule, tags, throttle } = req.body;
|
||||
return res.ok({
|
||||
body: await alertsClient.update({
|
||||
id,
|
||||
data: { name, actions, params, schedule, tags, throttle },
|
||||
}),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { LicenseState } from '../lib/license_state';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { BASE_ALERT_API_PATH } from '../../common';
|
||||
import { handleDisabledApiKeysError } from './lib/error_handler';
|
||||
|
||||
const paramSchema = schema.object({
|
||||
id: schema.string(),
|
||||
|
@ -31,19 +32,21 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) =
|
|||
tags: ['access:alerting-all'],
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
await alertsClient.updateApiKey({ id });
|
||||
return res.noContent();
|
||||
})
|
||||
handleDisabledApiKeysError(
|
||||
router.handleLegacyErrors(async function(
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
const alertsClient = context.alerting.getAlertsClient();
|
||||
const { id } = req.params;
|
||||
await alertsClient.updateApiKey({ id });
|
||||
return res.noContent();
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ export const AlertFlyout = (props: Props) => {
|
|||
},
|
||||
toastNotifications: services.notifications?.toasts,
|
||||
http: services.http,
|
||||
docLinks: services.docLinks,
|
||||
actionTypeRegistry: triggersActionsUI.actionTypeRegistry,
|
||||
alertTypeRegistry: triggersActionsUI.alertTypeRegistry,
|
||||
}}
|
||||
|
|
|
@ -660,6 +660,7 @@ const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState<boolean>(false);
|
|||
alertTypeRegistry: triggers_actions_ui.alertTypeRegistry,
|
||||
toastNotifications: toasts,
|
||||
uiSettings,
|
||||
docLinks,
|
||||
charts,
|
||||
dataFieldsFormats,
|
||||
metadata: { test: 'some value', fields: ['test'] },
|
||||
|
@ -697,6 +698,7 @@ export interface AlertsContextValue<MetaData = Record<string, any>> {
|
|||
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
|
||||
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
|
||||
uiSettings?: IUiSettingsClient;
|
||||
docLinks: DocLinksStart;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
|
@ -714,6 +716,7 @@ export interface AlertsContextValue<MetaData = Record<string, any>> {
|
|||
|alertTypeRegistry|Registry for alert types.|
|
||||
|actionTypeRegistry|Registry for action types.|
|
||||
|uiSettings|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|docLinks|Documentation Links, needed to link to the documentation from informational callouts.|
|
||||
|toastNotifications|Toast messages.|
|
||||
|charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|
@ -1322,6 +1325,7 @@ export interface AlertsContextValue {
|
|||
alertTypeRegistry: TypeRegistry<AlertTypeModel>;
|
||||
actionTypeRegistry: TypeRegistry<ActionTypeModel>;
|
||||
uiSettings?: IUiSettingsClient;
|
||||
docLinks: DocLinksStart;
|
||||
toastNotifications: Pick<
|
||||
ToastsApi,
|
||||
'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError'
|
||||
|
@ -1338,6 +1342,7 @@ export interface AlertsContextValue {
|
|||
|alertTypeRegistry|Registry for alert types.|
|
||||
|actionTypeRegistry|Registry for action types.|
|
||||
|uiSettings|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|docLinks|Documentation Links, needed to link to the documentation from informational callouts.|
|
||||
|toastNotifications|Toast messages.|
|
||||
|charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.|
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment } from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { AlertActionSecurityCallOut } from './alert_action_security_call_out';
|
||||
|
||||
import { EuiCallOut, EuiButton } from '@elastic/eui';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
|
||||
|
||||
const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' };
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
describe('alert action security call out', () => {
|
||||
let useEffect: any;
|
||||
|
||||
const mockUseEffect = () => {
|
||||
// make react execute useEffects despite shallow rendering
|
||||
useEffect.mockImplementationOnce((f: Function) => f());
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useEffect = jest.spyOn(React, 'useEffect');
|
||||
mockUseEffect();
|
||||
});
|
||||
|
||||
test('renders nothing while health is loading', async () => {
|
||||
http.get.mockImplementationOnce(() => new Promise(() => {}));
|
||||
|
||||
let component: ShallowWrapper | undefined;
|
||||
await act(async () => {
|
||||
component = shallow(
|
||||
<AlertActionSecurityCallOut action="created" http={http} docLinks={docLinks} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(component?.is(Fragment)).toBeTruthy();
|
||||
expect(component?.html()).toBe('');
|
||||
});
|
||||
|
||||
test('renders nothing if keys are enabled', async () => {
|
||||
http.get.mockResolvedValue({ isSufficientlySecure: true });
|
||||
|
||||
let component: ShallowWrapper | undefined;
|
||||
await act(async () => {
|
||||
component = shallow(
|
||||
<AlertActionSecurityCallOut action="created" http={http} docLinks={docLinks} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(component?.is(Fragment)).toBeTruthy();
|
||||
expect(component?.html()).toBe('');
|
||||
});
|
||||
|
||||
test('renders the callout if keys are disabled', async () => {
|
||||
http.get.mockResolvedValue({ isSufficientlySecure: false });
|
||||
|
||||
let component: ShallowWrapper | undefined;
|
||||
await act(async () => {
|
||||
component = shallow(
|
||||
<AlertActionSecurityCallOut action="creation" http={http} docLinks={docLinks} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(component?.find(EuiCallOut).prop('title')).toMatchInlineSnapshot(
|
||||
`"Alert creation requires TLS between Elasticsearch and Kibana."`
|
||||
);
|
||||
|
||||
expect(component?.find(EuiButton).prop('href')).toMatchInlineSnapshot(
|
||||
`"elastic.co/guide/en/kibana/current/configuring-tls.html"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { Option, none, some, fold, filter } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { DocLinksStart, HttpSetup } from 'kibana/public';
|
||||
import { AlertingFrameworkHealth } from '../../types';
|
||||
import { health } from '../lib/alert_api';
|
||||
|
||||
interface Props {
|
||||
docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>;
|
||||
action: string;
|
||||
http: HttpSetup;
|
||||
}
|
||||
|
||||
export const AlertActionSecurityCallOut: React.FunctionComponent<Props> = ({
|
||||
http,
|
||||
action,
|
||||
docLinks,
|
||||
}) => {
|
||||
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
|
||||
|
||||
const [alertingHealth, setAlertingHealth] = React.useState<Option<AlertingFrameworkHealth>>(none);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchSecurityConfigured() {
|
||||
setAlertingHealth(some(await health({ http })));
|
||||
}
|
||||
|
||||
fetchSecurityConfigured();
|
||||
}, [http]);
|
||||
|
||||
return pipe(
|
||||
alertingHealth,
|
||||
filter(healthCheck => !healthCheck.isSufficientlySecure),
|
||||
fold(
|
||||
() => <Fragment />,
|
||||
() => (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle',
|
||||
{
|
||||
defaultMessage: 'Alert {action} requires TLS between Elasticsearch and Kibana.',
|
||||
values: {
|
||||
action,
|
||||
},
|
||||
}
|
||||
)}
|
||||
color="warning"
|
||||
size="s"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<EuiButton
|
||||
color="warning"
|
||||
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta"
|
||||
defaultMessage="Enable TLS"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { Fragment } from 'react';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { SecurityEnabledCallOut } from './security_call_out';
|
||||
|
||||
import { EuiCallOut, EuiButton } from '@elastic/eui';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { httpServiceMock } from '../../../../../../src/core/public/mocks';
|
||||
|
||||
const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' };
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
describe('security call out', () => {
|
||||
let useEffect: any;
|
||||
|
||||
const mockUseEffect = () => {
|
||||
// make react execute useEffects despite shallow rendering
|
||||
useEffect.mockImplementationOnce((f: Function) => f());
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
useEffect = jest.spyOn(React, 'useEffect');
|
||||
mockUseEffect();
|
||||
});
|
||||
|
||||
test('renders nothing while health is loading', async () => {
|
||||
http.get.mockImplementationOnce(() => new Promise(() => {}));
|
||||
|
||||
let component: ShallowWrapper | undefined;
|
||||
await act(async () => {
|
||||
component = shallow(<SecurityEnabledCallOut http={http} docLinks={docLinks} />);
|
||||
});
|
||||
|
||||
expect(component?.is(Fragment)).toBeTruthy();
|
||||
expect(component?.html()).toBe('');
|
||||
});
|
||||
|
||||
test('renders nothing if keys are enabled', async () => {
|
||||
http.get.mockResolvedValue({ isSufficientlySecure: true });
|
||||
|
||||
let component: ShallowWrapper | undefined;
|
||||
await act(async () => {
|
||||
component = shallow(<SecurityEnabledCallOut http={http} docLinks={docLinks} />);
|
||||
});
|
||||
|
||||
expect(component?.is(Fragment)).toBeTruthy();
|
||||
expect(component?.html()).toBe('');
|
||||
});
|
||||
|
||||
test('renders the callout if keys are disabled', async () => {
|
||||
http.get.mockImplementationOnce(async () => ({ isSufficientlySecure: false }));
|
||||
|
||||
let component: ShallowWrapper | undefined;
|
||||
await act(async () => {
|
||||
component = shallow(<SecurityEnabledCallOut http={http} docLinks={docLinks} />);
|
||||
});
|
||||
|
||||
expect(component?.find(EuiCallOut).prop('title')).toMatchInlineSnapshot(
|
||||
`"Enable Transport Layer Security"`
|
||||
);
|
||||
|
||||
expect(component?.find(EuiButton).prop('href')).toMatchInlineSnapshot(
|
||||
`"elastic.co/guide/en/kibana/current/configuring-tls.html"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { Option, none, some, fold, filter } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
|
||||
import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { DocLinksStart, HttpSetup } from 'kibana/public';
|
||||
|
||||
import { AlertingFrameworkHealth } from '../../types';
|
||||
import { health } from '../lib/alert_api';
|
||||
|
||||
interface Props {
|
||||
docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>;
|
||||
http: HttpSetup;
|
||||
}
|
||||
|
||||
export const SecurityEnabledCallOut: React.FunctionComponent<Props> = ({ docLinks, http }) => {
|
||||
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
|
||||
|
||||
const [alertingHealth, setAlertingHealth] = React.useState<Option<AlertingFrameworkHealth>>(none);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchSecurityConfigured() {
|
||||
setAlertingHealth(some(await health({ http })));
|
||||
}
|
||||
|
||||
fetchSecurityConfigured();
|
||||
}, [http]);
|
||||
|
||||
return pipe(
|
||||
alertingHealth,
|
||||
filter(healthCheck => !healthCheck?.isSufficientlySecure),
|
||||
fold(
|
||||
() => <Fragment />,
|
||||
() => (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle',
|
||||
{
|
||||
defaultMessage: 'Enable Transport Layer Security',
|
||||
}
|
||||
)}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription"
|
||||
defaultMessage="Alerting relies on API keys, which require TLS between Elasticsearch and Kibana."
|
||||
/>
|
||||
</p>
|
||||
<EuiButton
|
||||
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.securityCallOut.enableTlsCta"
|
||||
defaultMessage="Enable TLS"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { useContext, createContext } from 'react';
|
||||
import { HttpSetup, IUiSettingsClient, ToastsApi } from 'kibana/public';
|
||||
import { HttpSetup, IUiSettingsClient, ToastsApi, DocLinksStart } from 'kibana/public';
|
||||
import { ChartsPluginSetup } from 'src/plugins/charts/public';
|
||||
import { FieldFormatsRegistry } from 'src/plugins/data/common/field_formats';
|
||||
import { TypeRegistry } from '../type_registry';
|
||||
|
@ -23,6 +23,7 @@ export interface AlertsContextValue<MetaData = Record<string, any>> {
|
|||
uiSettings?: IUiSettingsClient;
|
||||
charts?: ChartsPluginSetup;
|
||||
dataFieldsFormats?: Pick<FieldFormatsRegistry, 'register'>;
|
||||
docLinks: DocLinksStart;
|
||||
metadata?: MetaData;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import { hasShowActionsCapability, hasShowAlertsCapability } from './lib/capabil
|
|||
|
||||
import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list';
|
||||
import { AlertsList } from './sections/alerts_list/components/alerts_list';
|
||||
import { SecurityEnabledCallOut } from './components/security_call_out';
|
||||
import { PLUGIN } from './constants/plugin';
|
||||
|
||||
interface MatchParams {
|
||||
|
@ -41,7 +42,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
},
|
||||
history,
|
||||
}) => {
|
||||
const { chrome, capabilities, setBreadcrumbs } = useAppDependencies();
|
||||
const { chrome, capabilities, setBreadcrumbs, docLinks, http } = useAppDependencies();
|
||||
|
||||
const canShowActions = hasShowActionsCapability(capabilities);
|
||||
const canShowAlerts = hasShowAlertsCapability(capabilities);
|
||||
|
@ -87,6 +88,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<SecurityEnabledCallOut docLinks={docLinks} http={http} />
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle size="m">
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
updateAlert,
|
||||
muteAlertInstance,
|
||||
unmuteAlertInstance,
|
||||
health,
|
||||
} from './alert_api';
|
||||
import uuid from 'uuid';
|
||||
|
||||
|
@ -618,3 +619,17 @@ describe('unmuteAlerts', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('health', () => {
|
||||
test('should call health API', async () => {
|
||||
const result = await health({ http });
|
||||
expect(result).toEqual(undefined);
|
||||
expect(http.get.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/api/alert/_health",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import * as t from 'io-ts';
|
|||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { pick } from 'lodash';
|
||||
import { alertStateSchema } from '../../../../alerting/common';
|
||||
import { alertStateSchema, AlertingFrameworkHealth } from '../../../../alerting/common';
|
||||
import { BASE_ALERT_API_PATH } from '../constants';
|
||||
import { Alert, AlertType, AlertWithoutId, AlertTaskState } from '../../types';
|
||||
|
||||
|
@ -214,3 +214,7 @@ export async function unmuteAlerts({
|
|||
}): Promise<void> {
|
||||
await Promise.all(ids.map(id => unmuteAlert({ id, http })));
|
||||
}
|
||||
|
||||
export async function health({ http }: { http: HttpSetup }): Promise<AlertingFrameworkHealth> {
|
||||
return await http.get(`${BASE_ALERT_API_PATH}/_health`);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
|
|||
import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { AppContextProvider } from '../../app_context';
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const alertTypeRegistry = alertTypeRegistryMock.create();
|
||||
|
||||
|
@ -49,7 +50,11 @@ describe('alert_add', () => {
|
|||
charts: chartPluginMock.createStartContract(),
|
||||
actionTypeRegistry: actionTypeRegistry as any,
|
||||
alertTypeRegistry: alertTypeRegistry as any,
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||
};
|
||||
|
||||
mockes.http.get.mockResolvedValue({ isSufficientlySecure: true });
|
||||
|
||||
const alertType = {
|
||||
id: 'my-alert-type',
|
||||
iconClass: 'test',
|
||||
|
@ -83,22 +88,30 @@ describe('alert_add', () => {
|
|||
actionTypeRegistry.has.mockReturnValue(true);
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
reloadAlerts: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
http: deps.http,
|
||||
actionTypeRegistry: deps.actionTypeRegistry,
|
||||
alertTypeRegistry: deps.alertTypeRegistry,
|
||||
toastNotifications: deps.toastNotifications,
|
||||
uiSettings: deps.uiSettings,
|
||||
metadata: { test: 'some value', fields: ['test'] },
|
||||
}}
|
||||
>
|
||||
<AlertAdd consumer={'alerting'} addFlyoutVisible={true} setAddFlyoutVisibility={() => {}} />
|
||||
</AlertsContextProvider>
|
||||
<AppContextProvider appDeps={deps}>
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
reloadAlerts: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
http: deps.http,
|
||||
actionTypeRegistry: deps.actionTypeRegistry,
|
||||
alertTypeRegistry: deps.alertTypeRegistry,
|
||||
toastNotifications: deps.toastNotifications,
|
||||
uiSettings: deps.uiSettings,
|
||||
docLinks: deps.docLinks,
|
||||
metadata: { test: 'some value', fields: ['test'] },
|
||||
}}
|
||||
>
|
||||
<AlertAdd
|
||||
consumer={'alerting'}
|
||||
addFlyoutVisible={true}
|
||||
setAddFlyoutVisibility={() => {}}
|
||||
/>
|
||||
</AlertsContextProvider>
|
||||
</AppContextProvider>
|
||||
);
|
||||
|
||||
// Wait for active space to resolve before requesting the component to update
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
|
@ -108,12 +121,15 @@ describe('alert_add', () => {
|
|||
|
||||
it('renders alert add flyout', async () => {
|
||||
await setup();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="addAlertFlyoutTitle"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="saveAlertButton"]').exists()).toBeTruthy();
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="my-alert-type-SelectOption"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(wrapper.contains('Metadata: some value. Fields: test.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ import { Alert, AlertAction, IErrorObject } from '../../../types';
|
|||
import { AlertForm, validateBaseProperties } from './alert_form';
|
||||
import { alertReducer } from './alert_reducer';
|
||||
import { createAlert } from '../../lib/alert_api';
|
||||
import { AlertActionSecurityCallOut } from '../../components/alert_action_security_call_out';
|
||||
import { PLUGIN } from '../../constants/plugin';
|
||||
|
||||
interface AlertAddProps {
|
||||
|
@ -65,6 +66,7 @@ export const AlertAdd = ({
|
|||
toastNotifications,
|
||||
alertTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
docLinks,
|
||||
} = useAlertsContext();
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
|
@ -151,6 +153,16 @@ export const AlertAdd = ({
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<AlertActionSecurityCallOut
|
||||
docLinks={docLinks}
|
||||
action={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction',
|
||||
{
|
||||
defaultMessage: 'creation',
|
||||
}
|
||||
)}
|
||||
http={http}
|
||||
/>
|
||||
<EuiFlyoutBody>
|
||||
<AlertForm
|
||||
alert={alert}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { AlertsContextProvider } from '../../context/alerts_context';
|
|||
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { AlertEdit } from './alert_edit';
|
||||
import { AppContextProvider } from '../../app_context';
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const alertTypeRegistry = alertTypeRegistryMock.create();
|
||||
|
||||
|
@ -32,7 +33,11 @@ describe('alert_edit', () => {
|
|||
uiSettings: mockedCoreSetup.uiSettings,
|
||||
actionTypeRegistry: actionTypeRegistry as any,
|
||||
alertTypeRegistry: alertTypeRegistry as any,
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||
};
|
||||
|
||||
mockedCoreSetup.http.get.mockResolvedValue({ isSufficientlySecure: true });
|
||||
|
||||
const alertType = {
|
||||
id: 'my-alert-type',
|
||||
iconClass: 'test',
|
||||
|
@ -102,24 +107,27 @@ describe('alert_edit', () => {
|
|||
actionTypeRegistry.has.mockReturnValue(true);
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
reloadAlerts: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
http: deps!.http,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
alertTypeRegistry: deps!.alertTypeRegistry,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
uiSettings: deps!.uiSettings,
|
||||
}}
|
||||
>
|
||||
<AlertEdit
|
||||
editFlyoutVisible={true}
|
||||
setEditFlyoutVisibility={() => {}}
|
||||
initialAlert={alert}
|
||||
/>
|
||||
</AlertsContextProvider>
|
||||
<AppContextProvider appDeps={deps}>
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
reloadAlerts: () => {
|
||||
return new Promise<void>(() => {});
|
||||
},
|
||||
http: deps!.http,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
alertTypeRegistry: deps!.alertTypeRegistry,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
uiSettings: deps!.uiSettings,
|
||||
docLinks: deps.docLinks,
|
||||
}}
|
||||
>
|
||||
<AlertEdit
|
||||
editFlyoutVisible={true}
|
||||
setEditFlyoutVisibility={() => {}}
|
||||
initialAlert={alert}
|
||||
/>
|
||||
</AlertsContextProvider>
|
||||
</AppContextProvider>
|
||||
);
|
||||
// Wait for active space to resolve before requesting the component to update
|
||||
await act(async () => {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { Alert, AlertAction, IErrorObject } from '../../../types';
|
|||
import { AlertForm, validateBaseProperties } from './alert_form';
|
||||
import { alertReducer } from './alert_reducer';
|
||||
import { updateAlert } from '../../lib/alert_api';
|
||||
import { AlertActionSecurityCallOut } from '../../components/alert_action_security_call_out';
|
||||
import { PLUGIN } from '../../constants/plugin';
|
||||
|
||||
interface AlertEditProps {
|
||||
|
@ -49,6 +50,7 @@ export const AlertEdit = ({
|
|||
toastNotifications,
|
||||
alertTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
docLinks,
|
||||
} = useAlertsContext();
|
||||
|
||||
const closeFlyout = useCallback(() => {
|
||||
|
@ -135,6 +137,16 @@ export const AlertEdit = ({
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<AlertActionSecurityCallOut
|
||||
docLinks={docLinks}
|
||||
action={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction',
|
||||
{
|
||||
defaultMessage: 'editing',
|
||||
}
|
||||
)}
|
||||
http={http}
|
||||
/>
|
||||
<EuiFlyoutBody>
|
||||
{hasActionsDisabled && (
|
||||
<Fragment>
|
||||
|
|
|
@ -53,6 +53,7 @@ describe('alert_form', () => {
|
|||
uiSettings: mockes.uiSettings,
|
||||
actionTypeRegistry: actionTypeRegistry as any,
|
||||
alertTypeRegistry: alertTypeRegistry as any,
|
||||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||
};
|
||||
alertTypeRegistry.list.mockReturnValue([alertType]);
|
||||
alertTypeRegistry.has.mockReturnValue(true);
|
||||
|
@ -80,6 +81,7 @@ describe('alert_form', () => {
|
|||
return new Promise<void>(() => {});
|
||||
},
|
||||
http: deps!.http,
|
||||
docLinks: deps.docLinks,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
alertTypeRegistry: deps!.alertTypeRegistry,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
|
@ -159,6 +161,7 @@ describe('alert_form', () => {
|
|||
return new Promise<void>(() => {});
|
||||
},
|
||||
http: deps!.http,
|
||||
docLinks: deps.docLinks,
|
||||
actionTypeRegistry: deps!.actionTypeRegistry,
|
||||
alertTypeRegistry: deps!.alertTypeRegistry,
|
||||
toastNotifications: deps!.toastNotifications,
|
||||
|
|
|
@ -59,6 +59,7 @@ export const AlertsList: React.FunctionComponent = () => {
|
|||
alertTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
uiSettings,
|
||||
docLinks,
|
||||
charts,
|
||||
dataPlugin,
|
||||
} = useAppDependencies();
|
||||
|
@ -480,6 +481,7 @@ export const AlertsList: React.FunctionComponent = () => {
|
|||
alertTypeRegistry,
|
||||
toastNotifications,
|
||||
uiSettings,
|
||||
docLinks,
|
||||
charts,
|
||||
dataFieldsFormats: dataPlugin.fieldFormats,
|
||||
}}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, AlertType, AlertTaskState } from '../../../../types';
|
||||
import { Alert, AlertType, AlertTaskState, AlertingFrameworkHealth } from '../../../../types';
|
||||
import { useAppDependencies } from '../../../app_context';
|
||||
import {
|
||||
deleteAlerts,
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
loadAlert,
|
||||
loadAlertState,
|
||||
loadAlertTypes,
|
||||
health,
|
||||
} from '../../../lib/alert_api';
|
||||
|
||||
export interface ComponentOpts {
|
||||
|
@ -51,6 +52,7 @@ export interface ComponentOpts {
|
|||
loadAlert: (id: Alert['id']) => Promise<Alert>;
|
||||
loadAlertState: (id: Alert['id']) => Promise<AlertTaskState>;
|
||||
loadAlertTypes: () => Promise<AlertType[]>;
|
||||
getHealth: () => Promise<AlertingFrameworkHealth>;
|
||||
}
|
||||
|
||||
export type PropsWithOptionalApiHandlers<T> = Omit<T, keyof ComponentOpts> & Partial<ComponentOpts>;
|
||||
|
@ -115,6 +117,7 @@ export function withBulkAlertOperations<T>(
|
|||
loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })}
|
||||
loadAlertState={async (alertId: Alert['id']) => loadAlertState({ http, alertId })}
|
||||
loadAlertTypes={async () => loadAlertTypes({ http })}
|
||||
getHealth={async () => health({ http })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,8 +12,9 @@ import {
|
|||
AlertAction,
|
||||
AlertTaskState,
|
||||
RawAlertInstance,
|
||||
AlertingFrameworkHealth,
|
||||
} from '../../../plugins/alerting/common';
|
||||
export { Alert, AlertAction, AlertTaskState, RawAlertInstance };
|
||||
export { Alert, AlertAction, AlertTaskState, RawAlertInstance, AlertingFrameworkHealth };
|
||||
export { ActionType };
|
||||
|
||||
export type ActionTypeIndex = Record<string, ActionType>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue