[Alerting] Displays warning when a permanent encryption key is missing and hides alerting UI appropriately (#62772)
Removes the Security flyout and instead replaces the Alerting List, Connectors List and Alert Flyout with suitable messaging. Verifies that a permanent Encryption Key has been configured and if it hasn't displays a suitable warning in place, or along side the TLS warning, as needed.
This commit is contained in:
parent
578e443bdd
commit
e3bd04fcb0
|
@ -17,6 +17,7 @@ export interface ActionGroup {
|
|||
|
||||
export interface AlertingFrameworkHealth {
|
||||
isSufficientlySecure: boolean;
|
||||
hasPermanentEncryptionKey: boolean;
|
||||
}
|
||||
|
||||
export const BASE_ALERT_API_PATH = '/api/alert';
|
||||
|
|
|
@ -190,7 +190,7 @@ export class AlertingPlugin {
|
|||
unmuteAllAlertRoute(router, this.licenseState);
|
||||
muteAlertInstanceRoute(router, this.licenseState);
|
||||
unmuteAlertInstanceRoute(router, this.licenseState);
|
||||
healthRoute(router, this.licenseState);
|
||||
healthRoute(router, this.licenseState, plugins.encryptedSavedObjects);
|
||||
|
||||
return {
|
||||
registerType: alertTypeRegistry.register.bind(alertTypeRegistry),
|
||||
|
|
|
@ -10,6 +10,7 @@ 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';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
|
||||
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
|
@ -24,7 +25,9 @@ describe('healthRoute', () => {
|
|||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
|
||||
const [config] = router.get.mock.calls[0];
|
||||
|
||||
|
@ -35,7 +38,9 @@ describe('healthRoute', () => {
|
|||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
|
@ -58,11 +63,13 @@ describe('healthRoute', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => {
|
||||
it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => {
|
||||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = true;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
|
@ -73,6 +80,31 @@ describe('healthRoute', () => {
|
|||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": false,
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
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();
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
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 {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
|
@ -83,7 +115,9 @@ describe('healthRoute', () => {
|
|||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
|
@ -94,6 +128,7 @@ describe('healthRoute', () => {
|
|||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
|
@ -104,7 +139,9 @@ describe('healthRoute', () => {
|
|||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
|
@ -117,6 +154,7 @@ describe('healthRoute', () => {
|
|||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": false,
|
||||
},
|
||||
}
|
||||
|
@ -127,7 +165,9 @@ describe('healthRoute', () => {
|
|||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
|
@ -140,6 +180,7 @@ describe('healthRoute', () => {
|
|||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": false,
|
||||
},
|
||||
}
|
||||
|
@ -150,7 +191,9 @@ describe('healthRoute', () => {
|
|||
const router: RouterMock = mockRouter.create();
|
||||
|
||||
const licenseState = mockLicenseState();
|
||||
healthRoute(router, licenseState);
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
|
||||
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
|
||||
healthRoute(router, licenseState, encryptedSavedObjects);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const elasticsearch = elasticsearchServiceMock.createSetup();
|
||||
|
@ -163,6 +206,7 @@ describe('healthRoute', () => {
|
|||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": true,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { LicenseState } from '../lib/license_state';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { AlertingFrameworkHealth } from '../types';
|
||||
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
|
||||
|
||||
interface XPackUsageSecurity {
|
||||
security?: {
|
||||
|
@ -26,7 +27,11 @@ interface XPackUsageSecurity {
|
|||
};
|
||||
}
|
||||
|
||||
export function healthRoute(router: IRouter, licenseState: LicenseState) {
|
||||
export function healthRoute(
|
||||
router: IRouter,
|
||||
licenseState: LicenseState,
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
|
||||
) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/alert/_health',
|
||||
|
@ -54,6 +59,7 @@ export function healthRoute(router: IRouter, licenseState: LicenseState) {
|
|||
|
||||
const frameworkHealth: AlertingFrameworkHealth = {
|
||||
isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
|
||||
hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey,
|
||||
};
|
||||
|
||||
return res.ok({
|
||||
|
|
|
@ -15864,8 +15864,6 @@
|
|||
"xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "AND",
|
||||
"xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "タイミング",
|
||||
"xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "タイミング",
|
||||
"xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta": "TLS を有効にする",
|
||||
"xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle": "アラート {action} を実行するには Elasticsearch と Kibana の間に TLS が必要です。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "メールに送信",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "変数を追加",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。",
|
||||
|
@ -15960,9 +15958,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "HTTP ヘッダーを追加",
|
||||
"xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText": "{numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}を削除できませんでした",
|
||||
"xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText": "{numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}を削除しました",
|
||||
"xpack.triggersActionsUI.components.securityCallOut.enableTlsCta": "TLS を有効にする",
|
||||
"xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription": "アラートは API キー に依存し、キーを使用するには Elasticsearch と Kibana の間に TLS が必要です。",
|
||||
"xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle": "トランスポートレイヤーセキュリティを有効にする",
|
||||
"xpack.triggersActionsUI.connectors.breadcrumbTitle": "コネクター",
|
||||
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル",
|
||||
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ",
|
||||
|
@ -15986,8 +15981,8 @@
|
|||
"xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名前が必要です。",
|
||||
"xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "さらにアクションを表示",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel": "コネクターを作成",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody": "Kibana でトリガーできるメール、Slack, Elasticsearch、およびサードパーティサービスを構成します。",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle": "初めてのコネクターを作成する",
|
||||
"xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody": "Kibana でトリガーできるメール、Slack, Elasticsearch、およびサードパーティサービスを構成します。",
|
||||
"xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle": "初めてのコネクターを作成する",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle": "コネクターを削除できません",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel": "{count} 件を削除",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "このコネクターを削除",
|
||||
|
@ -16037,7 +16032,6 @@
|
|||
"xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction": "作成",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください。",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
|
||||
|
@ -16074,7 +16068,6 @@
|
|||
"xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存",
|
||||
"xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "アラートを更新できません。",
|
||||
"xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "「{alertName}」 を更新しました",
|
||||
"xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction": "編集中",
|
||||
"xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "削除",
|
||||
"xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "このアクションは無効です",
|
||||
"xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} コネクター",
|
||||
|
@ -16126,9 +16119,9 @@
|
|||
"xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "有効にする",
|
||||
"xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "ミュート",
|
||||
"xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "アクション",
|
||||
"xpack.triggersActionsUI.sections.alertsList.emptyButton": "アラートの作成",
|
||||
"xpack.triggersActionsUI.sections.alertsList.emptyDesc": "トリガーが起きたときにメール、Slack、または別のコネクターを通してアラートを受信します。",
|
||||
"xpack.triggersActionsUI.sections.alertsList.emptyTitle": "初めてのアラートを作成する",
|
||||
"xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "アラートの作成",
|
||||
"xpack.triggersActionsUI.components.emptyPrompt.emptyDesc": "トリガーが起きたときにメール、Slack、または別のコネクターを通してアラートを受信します。",
|
||||
"xpack.triggersActionsUI.components.emptyPrompt.emptyTitle": "初めてのアラートを作成する",
|
||||
"xpack.triggersActionsUI.sections.alertsList.multipleTitle": "アラート",
|
||||
"xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索",
|
||||
"xpack.triggersActionsUI.sections.alertsList.singleTitle": "アラート",
|
||||
|
|
|
@ -15868,8 +15868,6 @@
|
|||
"xpack.triggersActionsUI.common.expressionItems.threshold.andLabel": "且",
|
||||
"xpack.triggersActionsUI.common.expressionItems.threshold.descriptionLabel": "当",
|
||||
"xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle": "当",
|
||||
"xpack.triggersActionsUI.components.alertActionSecurityCallOut.enableTlsCta": "启用 TLS",
|
||||
"xpack.triggersActionsUI.components.alertActionSecurityCallOut.tlsDisabledTitle": "告警 {action} 在 Elasticsearch 和 Kibana 之间需要 TLS。",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.actionTypeTitle": "发送到电子邮件",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.addVariablePopoverButton": "添加变量",
|
||||
"xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。",
|
||||
|
@ -15964,9 +15962,6 @@
|
|||
"xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "添加 HTTP 标头",
|
||||
"xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText": "无法删除 {numErrors, number} 个{numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}",
|
||||
"xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText": "已删除 {numSuccesses, number} 个{numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}",
|
||||
"xpack.triggersActionsUI.components.securityCallOut.enableTlsCta": "启用 TLS",
|
||||
"xpack.triggersActionsUI.components.securityCallOut.tlsDisabledDescription": "Alerting 依赖于在 Elasticsearch 和 Kibana 之间需要 TLS 的 API 密钥。",
|
||||
"xpack.triggersActionsUI.components.securityCallOut.tlsDisabledTitle": "启用传输层安全",
|
||||
"xpack.triggersActionsUI.connectors.breadcrumbTitle": "连接器",
|
||||
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消",
|
||||
"xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ",
|
||||
|
@ -15991,8 +15986,8 @@
|
|||
"xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名称必填。",
|
||||
"xpack.triggersActionsUI.sections.actionForm.getMoreActionsTitle": "获取更多的操作",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel": "创建连接器",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody": "配置电子邮件、Slack、Elasticsearch 和 Kibana 可以触发的第三方服务。",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle": "创建您的首个连接器",
|
||||
"xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody": "配置电子邮件、Slack、Elasticsearch 和 Kibana 可以触发的第三方服务。",
|
||||
"xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle": "创建您的首个连接器",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteDisabledTitle": "无法删除连接器",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.buttons.deleteLabel": "删除 {count} 个",
|
||||
"xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.actions.deleteActionDescription": "删除此连接器",
|
||||
|
@ -16042,7 +16037,6 @@
|
|||
"xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.securityCalloutAction": "创建",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引。",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭",
|
||||
"xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
|
||||
|
@ -16079,7 +16073,6 @@
|
|||
"xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存",
|
||||
"xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "无法更新告警。",
|
||||
"xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "已更新“{alertName}”",
|
||||
"xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction": "正在编辑",
|
||||
"xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "删除",
|
||||
"xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "此操作已禁用",
|
||||
"xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} 连接器",
|
||||
|
@ -16131,9 +16124,9 @@
|
|||
"xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle": "启用",
|
||||
"xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle": "静音",
|
||||
"xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.popoverButtonTitle": "操作",
|
||||
"xpack.triggersActionsUI.sections.alertsList.emptyButton": "创建告警",
|
||||
"xpack.triggersActionsUI.sections.alertsList.emptyDesc": "触发条件满足时通过电子邮件、Slack 或其他连接器接收告警。",
|
||||
"xpack.triggersActionsUI.sections.alertsList.emptyTitle": "创建您的首个告警",
|
||||
"xpack.triggersActionsUI.components.emptyPrompt.emptyButton": "创建告警",
|
||||
"xpack.triggersActionsUI.components.emptyPrompt.emptyDesc": "触发条件满足时通过电子邮件、Slack 或其他连接器接收告警。",
|
||||
"xpack.triggersActionsUI.components.emptyPrompt.emptyTitle": "创建您的首个告警",
|
||||
"xpack.triggersActionsUI.sections.alertsList.multipleTitle": "告警",
|
||||
"xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索",
|
||||
"xpack.triggersActionsUI.sections.alertsList.singleTitle": "告警",
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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,13 @@
|
|||
@mixin padBannerWith($size) {
|
||||
padding-left: $size;
|
||||
padding-right: $size;
|
||||
}
|
||||
|
||||
.alertingHealthCheck__body {
|
||||
@include padBannerWith(2 * $euiSize);
|
||||
}
|
||||
|
||||
.alertingFlyoutHealthCheck__body {
|
||||
@include padBannerWith(2 * $euiSize);
|
||||
margin-top: $euiSize;
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { HealthCheck } from './health_check';
|
||||
|
||||
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('health check', () => {
|
||||
test('renders spinner while health is loading', async () => {
|
||||
http.get.mockImplementationOnce(() => new Promise(() => {}));
|
||||
|
||||
const { queryByText, container } = render(
|
||||
<HealthCheck http={http} docLinks={docLinks}>
|
||||
<p>{'shouldnt render'}</p>
|
||||
</HealthCheck>
|
||||
);
|
||||
await act(async () => {
|
||||
// wait for useEffect to run
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName('euiLoadingSpinner').length).toBe(1);
|
||||
expect(queryByText('shouldnt render')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders children if keys are enabled', async () => {
|
||||
http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true });
|
||||
|
||||
const { queryByText } = render(
|
||||
<HealthCheck http={http} docLinks={docLinks}>
|
||||
<p>{'should render'}</p>
|
||||
</HealthCheck>
|
||||
);
|
||||
await act(async () => {
|
||||
// wait for useEffect to run
|
||||
});
|
||||
expect(queryByText('should render')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders warning if keys are disabled', async () => {
|
||||
http.get.mockImplementationOnce(async () => ({
|
||||
isSufficientlySecure: false,
|
||||
hasPermanentEncryptionKey: true,
|
||||
}));
|
||||
|
||||
const { queryAllByText } = render(
|
||||
<HealthCheck http={http} docLinks={docLinks}>
|
||||
<p>{'should render'}</p>
|
||||
</HealthCheck>
|
||||
);
|
||||
await act(async () => {
|
||||
// wait for useEffect to run
|
||||
});
|
||||
|
||||
const [description, action] = queryAllByText(/TLS/i);
|
||||
|
||||
expect(description.textContent).toMatchInlineSnapshot(
|
||||
`"Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. Learn how to enable TLS."`
|
||||
);
|
||||
|
||||
expect(action.textContent).toMatchInlineSnapshot(`"Learn how to enable TLS."`);
|
||||
|
||||
expect(action.getAttribute('href')).toMatchInlineSnapshot(
|
||||
`"elastic.co/guide/en/kibana/current/configuring-tls.html"`
|
||||
);
|
||||
});
|
||||
|
||||
test('renders warning if encryption key is ephemeral', async () => {
|
||||
http.get.mockImplementationOnce(async () => ({
|
||||
isSufficientlySecure: true,
|
||||
hasPermanentEncryptionKey: false,
|
||||
}));
|
||||
|
||||
const { queryByText, queryByRole } = render(
|
||||
<HealthCheck http={http} docLinks={docLinks}>
|
||||
<p>{'should render'}</p>
|
||||
</HealthCheck>
|
||||
);
|
||||
await act(async () => {
|
||||
// wait for useEffect to run
|
||||
});
|
||||
|
||||
const description = queryByRole(/banner/i);
|
||||
expect(description!.textContent).toMatchInlineSnapshot(
|
||||
`"To create an alert, set a value for xpack.encrypted_saved_objects.encryptionKey in your kibana.yml file. Learn how."`
|
||||
);
|
||||
|
||||
const action = queryByText(/Learn/i);
|
||||
expect(action!.textContent).toMatchInlineSnapshot(`"Learn how."`);
|
||||
expect(action!.getAttribute('href')).toMatchInlineSnapshot(
|
||||
`"elastic.co/guide/en/kibana/current/alert-action-settings-kb.html#general-alert-action-settings"`
|
||||
);
|
||||
});
|
||||
|
||||
test('renders warning if encryption key is ephemeral and keys are disabled', async () => {
|
||||
http.get.mockImplementationOnce(async () => ({
|
||||
isSufficientlySecure: false,
|
||||
hasPermanentEncryptionKey: false,
|
||||
}));
|
||||
|
||||
const { queryByText } = render(
|
||||
<HealthCheck http={http} docLinks={docLinks}>
|
||||
<p>{'should render'}</p>
|
||||
</HealthCheck>
|
||||
);
|
||||
await act(async () => {
|
||||
// wait for useEffect to run
|
||||
});
|
||||
|
||||
const description = queryByText(/Transport Layer Security/i);
|
||||
|
||||
expect(description!.textContent).toMatchInlineSnapshot(
|
||||
`"You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. Learn how"`
|
||||
);
|
||||
|
||||
const action = queryByText(/Learn/i);
|
||||
expect(action!.textContent).toMatchInlineSnapshot(`"Learn how"`);
|
||||
expect(action!.getAttribute('href')).toMatchInlineSnapshot(
|
||||
`"elastic.co/guide/en/kibana/current/alerting-getting-started.html#alerting-setup-prerequisites"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* 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 } from 'fp-ts/lib/Option';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { EuiLink, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DocLinksStart, HttpSetup } from 'kibana/public';
|
||||
|
||||
import { EuiEmptyPrompt, EuiCode } from '@elastic/eui';
|
||||
import { AlertingFrameworkHealth } from '../../types';
|
||||
import { health } from '../lib/alert_api';
|
||||
import './health_check.scss';
|
||||
|
||||
interface Props {
|
||||
docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>;
|
||||
http: HttpSetup;
|
||||
inFlyout?: boolean;
|
||||
}
|
||||
|
||||
export const HealthCheck: React.FunctionComponent<Props> = ({
|
||||
docLinks,
|
||||
http,
|
||||
children,
|
||||
inFlyout = false,
|
||||
}) => {
|
||||
const [alertingHealth, setAlertingHealth] = React.useState<Option<AlertingFrameworkHealth>>(none);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async function() {
|
||||
setAlertingHealth(some(await health({ http })));
|
||||
})();
|
||||
}, [http]);
|
||||
|
||||
const className = inFlyout ? 'alertingFlyoutHealthCheck' : 'alertingHealthCheck';
|
||||
|
||||
return pipe(
|
||||
alertingHealth,
|
||||
fold(
|
||||
() => <EuiLoadingSpinner size="m" />,
|
||||
healthCheck => {
|
||||
return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? (
|
||||
<Fragment>{children}</Fragment>
|
||||
) : !healthCheck.isSufficientlySecure && !healthCheck.hasPermanentEncryptionKey ? (
|
||||
<TlsAndEncryptionError docLinks={docLinks} className={className} />
|
||||
) : !healthCheck.hasPermanentEncryptionKey ? (
|
||||
<EncryptionError docLinks={docLinks} className={className} />
|
||||
) : (
|
||||
<TlsError docLinks={docLinks} className={className} />
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
type PromptErrorProps = Pick<Props, 'docLinks'> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const TlsAndEncryptionError = ({
|
||||
docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION },
|
||||
className,
|
||||
}: PromptErrorProps) => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="watchesApp"
|
||||
data-test-subj="actionNeededEmptyPrompt"
|
||||
className={className}
|
||||
titleSize="xs"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorTitle"
|
||||
defaultMessage="Additional setup required"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<div className={`${className}__body`}>
|
||||
<p role="banner">
|
||||
{i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionError', {
|
||||
defaultMessage:
|
||||
'You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. ',
|
||||
})}
|
||||
<EuiLink
|
||||
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`}
|
||||
external
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction',
|
||||
{
|
||||
defaultMessage: 'Learn how',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const EncryptionError = ({
|
||||
docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION },
|
||||
className,
|
||||
}: PromptErrorProps) => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="watchesApp"
|
||||
data-test-subj="actionNeededEmptyPrompt"
|
||||
className={className}
|
||||
titleSize="xs"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.healthCheck.encryptionErrorTitle"
|
||||
defaultMessage="You must set an encryption key"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<div className={`${className}__body`}>
|
||||
<p role="banner">
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.components.healthCheck.encryptionErrorBeforeKey',
|
||||
{
|
||||
defaultMessage: 'To create an alert, set a value for ',
|
||||
}
|
||||
)}
|
||||
<EuiCode>{'xpack.encrypted_saved_objects.encryptionKey'}</EuiCode>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAfterKey',
|
||||
{
|
||||
defaultMessage: ' in your kibana.yml file. ',
|
||||
}
|
||||
)}
|
||||
<EuiLink
|
||||
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`}
|
||||
external
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAction',
|
||||
{
|
||||
defaultMessage: 'Learn how.',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const TlsError = ({
|
||||
docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION },
|
||||
className,
|
||||
}: PromptErrorProps) => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="watchesApp"
|
||||
data-test-subj="actionNeededEmptyPrompt"
|
||||
className={className}
|
||||
titleSize="xs"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.healthCheck.tlsErrorTitle"
|
||||
defaultMessage="You must enable Transport Layer Security"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<div className={`${className}__body`}>
|
||||
<p role="banner">
|
||||
{i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsError', {
|
||||
defaultMessage:
|
||||
'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ',
|
||||
})}
|
||||
<EuiLink
|
||||
href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`}
|
||||
external
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsErrorAction', {
|
||||
defaultMessage: 'Learn how to enable TLS.',
|
||||
})}
|
||||
</EuiLink>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -0,0 +1,3 @@
|
|||
.actEmptyConnectorsPrompt__logo + .actEmptyConnectorsPrompt__logo {
|
||||
margin-left: $euiSize;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import './empty_connectors_prompt.scss';
|
||||
|
||||
export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="createFirstConnectorEmptyPrompt"
|
||||
title={
|
||||
<Fragment>
|
||||
<EuiIcon type="logoSlack" size="xl" className="actEmptyConnectorsPrompt__logo" />
|
||||
<EuiIcon type="logoGmail" size="xl" className="actEmptyConnectorsPrompt__logo" />
|
||||
<EuiIcon type="logoWebhook" size="xl" className="actEmptyConnectorsPrompt__logo" />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyTitle"
|
||||
defaultMessage="Create your first connector"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</Fragment>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionEmptyBody"
|
||||
defaultMessage="Configure email, Slack, Elasticsearch, and third-party services that Kibana can trigger."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="createFirstActionButton"
|
||||
key="create-action"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
onClick={onCTAClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.emptyConnectorsPrompt.addActionButtonLabel"
|
||||
defaultMessage="Create connector"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
export const EmptyPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => (
|
||||
<EuiEmptyPrompt
|
||||
iconType="watchesApp"
|
||||
data-test-subj="createFirstAlertEmptyPrompt"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.emptyPrompt.emptyTitle"
|
||||
defaultMessage="Create your first alert"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.emptyPrompt.emptyDesc"
|
||||
defaultMessage="Receive an alert through email, Slack, or another connector when a trigger is hit."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="createFirstAlertButton"
|
||||
key="create-action"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
onClick={onCTAClicked}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.components.emptyPrompt.emptyButton"
|
||||
defaultMessage="Create alert"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* 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"`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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>
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
|
@ -29,8 +29,8 @@ 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';
|
||||
import { HealthCheck } from './components/health_check';
|
||||
|
||||
interface MatchParams {
|
||||
section: Section;
|
||||
|
@ -88,7 +88,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<SecurityEnabledCallOut docLinks={docLinks} http={http} />
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle size="m">
|
||||
|
@ -142,9 +141,27 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
|||
|
||||
<Switch>
|
||||
{canShowActions && (
|
||||
<Route exact path={routeToConnectors} component={ActionsConnectorsList} />
|
||||
<Route
|
||||
exact
|
||||
path={routeToConnectors}
|
||||
component={() => (
|
||||
<HealthCheck docLinks={docLinks} http={http}>
|
||||
<ActionsConnectorsList />
|
||||
</HealthCheck>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{canShowAlerts && (
|
||||
<Route
|
||||
exact
|
||||
path={routeToAlerts}
|
||||
component={() => (
|
||||
<HealthCheck docLinks={docLinks} http={http}>
|
||||
<AlertsList />
|
||||
</HealthCheck>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{canShowAlerts && <Route exact path={routeToAlerts} component={AlertsList} />}
|
||||
</Switch>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
.actConnectorsList__logo + .actConnectorsList__logo {
|
||||
margin-left: $euiSize;
|
||||
}
|
||||
|
||||
.actConnectorsList__tableRowDisabled {
|
||||
background-color: $euiColorLightestShade;
|
||||
|
||||
|
|
|
@ -10,9 +10,6 @@ import {
|
|||
EuiInMemoryTable,
|
||||
EuiSpacer,
|
||||
EuiButton,
|
||||
EuiIcon,
|
||||
EuiEmptyPrompt,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiIconTip,
|
||||
|
@ -30,6 +27,7 @@ import { ActionsConnectorsContextProvider } from '../../../context/actions_conne
|
|||
import { checkActionTypeEnabled } from '../../../lib/check_action_type_enabled';
|
||||
import './actions_connectors_list.scss';
|
||||
import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types';
|
||||
import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt';
|
||||
|
||||
export const ActionsConnectorsList: React.FunctionComponent = () => {
|
||||
const { http, toastNotifications, capabilities, actionTypeRegistry } = useAppDependencies();
|
||||
|
@ -324,51 +322,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const emptyPrompt = (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="createFirstConnectorEmptyPrompt"
|
||||
title={
|
||||
<Fragment>
|
||||
<EuiIcon type="logoSlack" size="xl" className="actConnectorsList__logo" />
|
||||
<EuiIcon type="logoGmail" size="xl" className="actConnectorsList__logo" />
|
||||
<EuiIcon type="logoWebhook" size="xl" className="actConnectorsList__logo" />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyTitle"
|
||||
defaultMessage="Create your first connector"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</Fragment>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.addActionEmptyBody"
|
||||
defaultMessage="Configure email, Slack, Elasticsearch, and third-party services that Kibana can trigger."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="createFirstActionButton"
|
||||
key="create-action"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
onClick={() => setAddFlyoutVisibility(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.actionsConnectorsList.addActionButtonLabel"
|
||||
defaultMessage="Create connector"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const noPermissionPrompt = (
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
|
@ -420,7 +373,9 @@ export const ActionsConnectorsList: React.FunctionComponent = () => {
|
|||
</EuiFlexGroup>
|
||||
)}
|
||||
{data.length !== 0 && table}
|
||||
{data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && emptyPrompt}
|
||||
{data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && (
|
||||
<EmptyConnectorsPrompt onCTAClicked={() => setAddFlyoutVisibility(true)} />
|
||||
)}
|
||||
{data.length === 0 && !canSave && noPermissionPrompt}
|
||||
<ActionsConnectorsContextProvider
|
||||
value={{
|
||||
|
|
|
@ -53,7 +53,10 @@ describe('alert_add', () => {
|
|||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||
};
|
||||
|
||||
mockes.http.get.mockResolvedValue({ isSufficientlySecure: true });
|
||||
mockes.http.get.mockResolvedValue({
|
||||
isSufficientlySecure: true,
|
||||
hasPermanentEncryptionKey: true,
|
||||
});
|
||||
|
||||
const alertType = {
|
||||
id: 'my-alert-type',
|
||||
|
|
|
@ -25,7 +25,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 { HealthCheck } from '../../components/health_check';
|
||||
import { PLUGIN } from '../../constants/plugin';
|
||||
|
||||
interface AlertAddProps {
|
||||
|
@ -154,62 +154,54 @@ 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}
|
||||
dispatch={dispatch}
|
||||
errors={errors}
|
||||
canChangeTrigger={canChangeTrigger}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancelSaveAlertButton" onClick={closeFlyout}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
data-test-subj="saveAlertButton"
|
||||
type="submit"
|
||||
iconType="check"
|
||||
isDisabled={hasErrors || hasActionErrors}
|
||||
isLoading={isSaving}
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
const savedAlert = await onSaveAlert();
|
||||
setIsSaving(false);
|
||||
if (savedAlert) {
|
||||
closeFlyout();
|
||||
if (reloadAlerts) {
|
||||
reloadAlerts();
|
||||
<HealthCheck docLinks={docLinks} http={http} inFlyout={true}>
|
||||
<EuiFlyoutBody>
|
||||
<AlertForm
|
||||
alert={alert}
|
||||
dispatch={dispatch}
|
||||
errors={errors}
|
||||
canChangeTrigger={canChangeTrigger}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancelSaveAlertButton" onClick={closeFlyout}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
data-test-subj="saveAlertButton"
|
||||
type="submit"
|
||||
iconType="check"
|
||||
isDisabled={hasErrors || hasActionErrors}
|
||||
isLoading={isSaving}
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
const savedAlert = await onSaveAlert();
|
||||
setIsSaving(false);
|
||||
if (savedAlert) {
|
||||
closeFlyout();
|
||||
if (reloadAlerts) {
|
||||
reloadAlerts();
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</HealthCheck>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
|
|
|
@ -36,7 +36,10 @@ describe('alert_edit', () => {
|
|||
docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' },
|
||||
};
|
||||
|
||||
mockedCoreSetup.http.get.mockResolvedValue({ isSufficientlySecure: true });
|
||||
mockedCoreSetup.http.get.mockResolvedValue({
|
||||
isSufficientlySecure: true,
|
||||
hasPermanentEncryptionKey: true,
|
||||
});
|
||||
|
||||
const alertType = {
|
||||
id: 'my-alert-type',
|
||||
|
|
|
@ -26,7 +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 { HealthCheck } from '../../components/health_check';
|
||||
import { PLUGIN } from '../../constants/plugin';
|
||||
|
||||
interface AlertEditProps {
|
||||
|
@ -137,77 +137,69 @@ export const AlertEdit = ({
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<AlertActionSecurityCallOut
|
||||
docLinks={docLinks}
|
||||
action={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertEdit.securityCalloutAction',
|
||||
{
|
||||
defaultMessage: 'editing',
|
||||
}
|
||||
)}
|
||||
http={http}
|
||||
/>
|
||||
<EuiFlyoutBody>
|
||||
{hasActionsDisabled && (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle',
|
||||
{ defaultMessage: 'This alert has actions that are disabled' }
|
||||
)}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
)}
|
||||
<AlertForm
|
||||
alert={alert}
|
||||
dispatch={dispatch}
|
||||
errors={errors}
|
||||
canChangeTrigger={false}
|
||||
setHasActionsDisabled={setHasActionsDisabled}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancelSaveEditedAlertButton" onClick={closeFlyout}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
data-test-subj="saveEditedAlertButton"
|
||||
type="submit"
|
||||
iconType="check"
|
||||
isDisabled={hasErrors || hasActionErrors}
|
||||
isLoading={isSaving}
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
const savedAlert = await onSaveAlert();
|
||||
setIsSaving(false);
|
||||
if (savedAlert) {
|
||||
closeFlyout();
|
||||
if (reloadAlerts) {
|
||||
reloadAlerts();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
<HealthCheck docLinks={docLinks} http={http} inFlyout={true}>
|
||||
<EuiFlyoutBody>
|
||||
{hasActionsDisabled && (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
title={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle',
|
||||
{ defaultMessage: 'This alert has actions that are disabled' }
|
||||
)}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
)}
|
||||
<AlertForm
|
||||
alert={alert}
|
||||
dispatch={dispatch}
|
||||
errors={errors}
|
||||
canChangeTrigger={false}
|
||||
setHasActionsDisabled={setHasActionsDisabled}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="cancelSaveEditedAlertButton" onClick={closeFlyout}>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
data-test-subj="saveEditedAlertButton"
|
||||
type="submit"
|
||||
iconType="check"
|
||||
isDisabled={hasErrors || hasActionErrors}
|
||||
isLoading={isSaving}
|
||||
onClick={async () => {
|
||||
setIsSaving(true);
|
||||
const savedAlert = await onSaveAlert();
|
||||
setIsSaving(false);
|
||||
if (savedAlert) {
|
||||
closeFlyout();
|
||||
if (reloadAlerts) {
|
||||
reloadAlerts();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</HealthCheck>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiEmptyPrompt,
|
||||
EuiLink,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
|
@ -36,6 +35,7 @@ import { loadActionTypes } from '../../../lib/action_connector_api';
|
|||
import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities';
|
||||
import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
|
||||
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
|
||||
import { EmptyPrompt } from '../../../components/prompts/empty_prompt';
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
|
||||
|
@ -292,44 +292,6 @@ export const AlertsList: React.FunctionComponent = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const emptyPrompt = (
|
||||
<EuiEmptyPrompt
|
||||
iconType="watchesApp"
|
||||
data-test-subj="createFirstAlertEmptyPrompt"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertsList.emptyTitle"
|
||||
defaultMessage="Create your first alert"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertsList.emptyDesc"
|
||||
defaultMessage="Receive an alert through email, Slack, or another connector when a trigger is hit."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj="createFirstAlertButton"
|
||||
key="create-action"
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
onClick={() => setAlertFlyoutVisibility(true)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.alertsList.emptyButton"
|
||||
defaultMessage="Create alert"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const table = (
|
||||
<Fragment>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
|
@ -473,7 +435,7 @@ export const AlertsList: React.FunctionComponent = () => {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
emptyPrompt
|
||||
<EmptyPrompt onCTAClicked={() => setAlertFlyoutVisibility(true)} />
|
||||
)}
|
||||
<AlertsContextProvider
|
||||
value={{
|
||||
|
|
Loading…
Reference in a new issue