diff --git a/x-pack/plugins/alerts/server/lib/license_state.test.ts b/x-pack/plugins/alerts/server/lib/license_state.test.ts index 07074b9187547..a1c326656f735 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.test.ts @@ -248,7 +248,7 @@ describe('ensureLicenseForAlertType()', () => { expect(() => licenseState.ensureLicenseForAlertType(alertType) ).toThrowErrorMatchingInlineSnapshot( - `"Alert test is disabled because it requires a Gold license. Contact your administrator to upgrade your license."` + `"Alert test is disabled because it requires a Gold license. Go to License Management to view upgrade options."` ); }); diff --git a/x-pack/plugins/alerts/server/lib/license_state.ts b/x-pack/plugins/alerts/server/lib/license_state.ts index f95c6cb42a17b..238b2e97c4cdf 100644 --- a/x-pack/plugins/alerts/server/lib/license_state.ts +++ b/x-pack/plugins/alerts/server/lib/license_state.ts @@ -9,6 +9,7 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { assertNever } from '@kbn/std'; +import { capitalize } from 'lodash'; import { Observable, Subscription } from 'rxjs'; import { LicensingPluginStart } from '../../../licensing/server'; import { ILicense, LicenseType } from '../../../licensing/common/types'; @@ -190,8 +191,11 @@ export class LicenseState { throw new AlertTypeDisabledError( i18n.translate('xpack.alerts.serverSideErrors.invalidLicenseErrorMessage', { defaultMessage: - 'Alert {alertTypeId} is disabled because it requires a Gold license. Contact your administrator to upgrade your license.', - values: { alertTypeId: alertType.id }, + 'Alert {alertTypeId} is disabled because it requires a {licenseType} license. Go to License Management to view upgrade options.', + values: { + alertTypeId: alertType.id, + licenseType: capitalize(alertType.minimumLicenseRequired), + }, }), 'license_invalid' ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 018d2d572eea0..5e0bf7501eb11 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4830,7 +4830,6 @@ "xpack.alerts.server.healthStatus.degraded": "アラートフレームワークは劣化しました", "xpack.alerts.server.healthStatus.unavailable": "アラートフレームワークを使用できません", "xpack.alerts.serverSideErrors.expirerdLicenseErrorMessage": "{licenseType} ライセンスの期限が切れたのでアラートタイプ {alertTypeId} は無効です。", - "xpack.alerts.serverSideErrors.invalidLicenseErrorMessage": "アラート {alertTypeId} は無効です。Gold ライセンスが必要です。ライセンスをアップグレードするには、管理者に問い合わせてください。", "xpack.alerts.serverSideErrors.unavailableLicenseErrorMessage": "現時点でライセンス情報を入手できないため、アラートタイプ {alertTypeId} は無効です。", "xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage": "アラートを利用できません。現在ライセンス情報が利用できません。", "xpack.apm.a.thresholdMet": "しきい値一致", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5a9695b8ddc3d..d0dbd750853a2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4836,7 +4836,6 @@ "xpack.alerts.server.healthStatus.degraded": "告警框架已降级", "xpack.alerts.server.healthStatus.unavailable": "告警框架不可用", "xpack.alerts.serverSideErrors.expirerdLicenseErrorMessage": "告警类型 {alertTypeId} 已禁用,因为您的{licenseType}许可证已过期。", - "xpack.alerts.serverSideErrors.invalidLicenseErrorMessage": "告警 {alertTypeId} 已禁用,因为它需要黄金级许可证。请联系管理员升级您的许可证。", "xpack.alerts.serverSideErrors.unavailableLicenseErrorMessage": "告警类型 {alertTypeId} 已禁用,因为许可证信息当前不可用。", "xpack.alerts.serverSideErrors.unavailableLicenseInformationErrorMessage": "告警不可用 - 许可信息当前不可用。", "xpack.apm.a.thresholdMet": "已达到阈值", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index fb34c95f93de2..fc41022dfb7b0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -127,11 +127,16 @@ describe('alerts_list component empty', () => { wrapper.find('button[data-test-subj="createFirstAlertButton"]').simulate('click'); - // When the AlertAdd component is rendered, it waits for the healthcheck to resolve - await new Promise((resolve) => { - setTimeout(resolve, 1000); + await act(async () => { + // When the AlertAdd component is rendered, it waits for the healthcheck to resolve + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + + await nextTick(); + wrapper.update(); }); - wrapper.update(); + expect(wrapper.find('AlertAdd').exists()).toEqual(true); }); }); @@ -139,104 +144,131 @@ describe('alerts_list component empty', () => { describe('alerts_list component with items', () => { let wrapper: ReactWrapper; + const mockedAlertsData = [ + { + id: '1', + name: 'test alert', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + { + id: '2', + name: 'test alert ok', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'ok', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + { + id: '3', + name: 'test alert pending', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + }, + { + id: '4', + name: 'test alert error', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'error', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: AlertExecutionStatusErrorReasons.Unknown, + message: 'test', + }, + }, + }, + { + id: '5', + name: 'test alert license error', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], + params: { name: 'test alert type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'error', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: AlertExecutionStatusErrorReasons.License, + message: 'test', + }, + }, + }, + ]; + async function setup() { loadAlerts.mockResolvedValue({ page: 1, perPage: 10000, total: 4, - data: [ - { - id: '1', - name: 'test alert', - tags: ['tag1'], - enabled: true, - alertTypeId: 'test_alert_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test alert type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'active', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: null, - }, - }, - { - id: '2', - name: 'test alert ok', - tags: ['tag1'], - enabled: true, - alertTypeId: 'test_alert_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test alert type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'ok', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: null, - }, - }, - { - id: '3', - name: 'test alert pending', - tags: ['tag1'], - enabled: true, - alertTypeId: 'test_alert_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test alert type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'pending', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: null, - }, - }, - { - id: '4', - name: 'test alert error', - tags: ['tag1'], - enabled: true, - alertTypeId: 'test_alert_type', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'alert', params: { message: 'test' } }], - params: { name: 'test alert type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'error', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: { - reason: AlertExecutionStatusErrorReasons.Unknown, - message: 'test', - }, - }, - }, - ], + data: mockedAlertsData, }); loadActionTypes.mockResolvedValue([ { @@ -271,21 +303,66 @@ describe('alerts_list component with items', () => { it('renders table of alerts', async () => { await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(4); - expect(wrapper.find('[data-test-subj="alertsTableCell-status"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="alertStatus-active"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="alertStatus-error"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="alertStatus-ok"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="alertStatus-pending"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="alertStatus-unknown"]').length).toBe(0); + expect(wrapper.find('EuiTableRow')).toHaveLength(mockedAlertsData.length); + expect(wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-status"]').length).toEqual( + mockedAlertsData.length + ); + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-active"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-ok"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-pending"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-unknown"]').length).toEqual(0); + + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').length).toEqual(2); + expect(wrapper.find('[data-test-subj="alertStatus-error-tooltip"]').length).toEqual(2); + expect( + wrapper.find('EuiButtonEmpty[data-test-subj="alertStatus-error-license-fix"]').length + ).toEqual(1); + expect(wrapper.find('[data-test-subj="refreshAlertsButton"]').exists()).toBeTruthy(); + + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').first().text()).toEqual( + 'Error' + ); + expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').last().text()).toEqual( + 'License Error' + ); }); it('loads alerts when refresh button is clicked', async () => { await setup(); wrapper.find('[data-test-subj="refreshAlertsButton"]').first().simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(loadAlerts).toHaveBeenCalled(); }); + + it('renders license errors and manage license modal on click', async () => { + global.open = jest.fn(); + await setup(); + expect(wrapper.find('ManageLicenseModal').exists()).toBeFalsy(); + expect( + wrapper.find('EuiButtonEmpty[data-test-subj="alertStatus-error-license-fix"]').length + ).toEqual(1); + wrapper + .find('EuiButtonEmpty[data-test-subj="alertStatus-error-license-fix"]') + .simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('ManageLicenseModal').exists()).toBeTruthy(); + expect(wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').text()).toEqual( + 'Manage license' + ); + wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + expect(global.open).toHaveBeenCalled(); + }); }); describe('alerts_list component empty with show only capability', () => { @@ -308,7 +385,9 @@ describe('alerts_list component empty with show only capability', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + loadAlertTypes.mockResolvedValue([ + { id: 'test_alert_type', name: 'some alert type', authorizedConsumers: {} }, + ]); loadAllActions.mockResolvedValue([]); // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.alertTypeRegistry = alertTypeRegistry; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 76680a60a24e1..11761cec7cdbb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -53,14 +53,15 @@ import { AlertExecutionStatus, AlertExecutionStatusValues, ALERTS_FEATURE_ID, + AlertExecutionStatusErrorReasons, } from '../../../../../../alerts/common'; import { hasAllPrivilege } from '../../../lib/capabilities'; -import { alertsStatusesTranslationsMapping } from '../translations'; +import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; -import { checkAlertTypeEnabled } from '../../../lib/check_alert_type_enabled'; import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; import './alerts_list.scss'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; +import { ManageLicenseModal } from './manage_license_modal'; const ENTER_KEY = 13; @@ -97,7 +98,11 @@ export const AlertsList: React.FunctionComponent = () => { const [actionTypesFilter, setActionTypesFilter] = useState([]); const [alertStatusesFilter, setAlertStatusesFilter] = useState([]); const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); - const [dissmissAlertErrors, setDissmissAlertErrors] = useState(false); + const [dismissAlertErrors, setDismissAlertErrors] = useState(false); + const [manageLicenseModalOpts, setManageLicenseModalOpts] = useState<{ + licenseType: string; + alertTypeId: string; + } | null>(null); const [alertsStatusesTotal, setAlertsStatusesTotal] = useState>( AlertExecutionStatusValues.reduce( (prev: Record, status: string) => @@ -238,25 +243,64 @@ export const AlertsList: React.FunctionComponent = () => { } } + const renderAlertExecutionStatus = ( + executionStatus: AlertExecutionStatus, + item: AlertTableItem + ) => { + const healthColor = getHealthColor(executionStatus.status); + const tooltipMessage = + executionStatus.status === 'error' ? `Error: ${executionStatus?.error?.message}` : null; + const isLicenseError = + executionStatus.error?.reason === AlertExecutionStatusErrorReasons.License; + const statusMessage = isLicenseError + ? ALERT_STATUS_LICENSE_ERROR + : alertsStatusesTranslationsMapping[executionStatus.status]; + + const health = ( + + {statusMessage} + + ); + + const healthWithTooltip = tooltipMessage ? ( + + {health} + + ) : ( + health + ); + + return ( + + {healthWithTooltip} + {isLicenseError && ( + + + setManageLicenseModalOpts({ + licenseType: alertTypesState.data.get(item.alertTypeId)?.minimumLicenseRequired!, + alertTypeId: item.alertTypeId, + }) + } + > + + + + )} + + ); + }; + const alertsTableColumns = [ - { - field: 'executionStatus', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', - { defaultMessage: 'Status' } - ), - sortable: false, - truncateText: false, - 'data-test-subj': 'alertsTableCell-status', - render: (executionStatus: AlertExecutionStatus) => { - const healthColor = getHealthColor(executionStatus.status); - return ( - - {alertsStatusesTranslationsMapping[executionStatus.status]} - - ); - }, - }, { field: 'name', name: i18n.translate( @@ -265,12 +309,10 @@ export const AlertsList: React.FunctionComponent = () => { ), sortable: false, truncateText: true, + width: '35%', 'data-test-subj': 'alertsTableCell-name', render: (name: string, alert: AlertTableItem) => { - const checkEnabledResult = checkAlertTypeEnabled( - alertTypesState.data.get(alert.alertTypeId) - ); - const link = ( + return ( { @@ -280,17 +322,20 @@ export const AlertsList: React.FunctionComponent = () => { {name} ); - return checkEnabledResult.isEnabled ? ( - link - ) : ( - - {link} - - ); + }, + }, + { + field: 'executionStatus', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', + { defaultMessage: 'Status' } + ), + sortable: false, + truncateText: false, + width: '150px', + 'data-test-subj': 'alertsTableCell-status', + render: (executionStatus: AlertExecutionStatus, item: AlertTableItem) => { + return renderAlertExecutionStatus(executionStatus, item); }, }, { @@ -492,7 +537,7 @@ export const AlertsList: React.FunctionComponent = () => { - {!dissmissAlertErrors && alertsStatusesTotal.error > 0 ? ( + {!dismissAlertErrors && alertsStatusesTotal.error > 0 ? ( { defaultMessage="View" /> - setDissmissAlertErrors(true)}> + setDismissAlertErrors(true)}> { setPage(changedPage); }} /> + {manageLicenseModalOpts && ( + { + window.open(`${http.basePath.get()}/app/management/stack/license_management`, '_blank'); + setManageLicenseModalOpts(null); + }} + onCancel={() => setManageLicenseModalOpts(null)} + /> + )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx new file mode 100644 index 0000000000000..f13e5fd96d2ad --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { capitalize } from 'lodash'; + +interface Props { + licenseType: string; + alertTypeId: string; + onConfirm: () => void; + onCancel: () => void; +} + +export const ManageLicenseModal: React.FC = ({ + licenseType, + alertTypeId, + onConfirm, + onCancel, +}) => { + const licenseRequired = capitalize(licenseType); + return ( + + +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts index 0b8bba9ffe95a..1a2c576b1fa28 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/translations.ts @@ -28,6 +28,13 @@ export const ALERT_STATUS_ERROR = i18n.translate( } ); +export const ALERT_STATUS_LICENSE_ERROR = i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertStatusLicenseError', + { + defaultMessage: 'License Error', + } +); + export const ALERT_STATUS_PENDING = i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertStatusPending', { diff --git a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts index 488b39eabb637..211d1acb2a005 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/alerts/gold_noop_alert_type.ts @@ -22,7 +22,7 @@ export default function emailTest({ getService }: FtrProviderContext) { statusCode: 403, error: 'Forbidden', message: - 'Alert test.gold.noop is disabled because it requires a Gold license. Contact your administrator to upgrade your license.', + 'Alert test.gold.noop is disabled because it requires a Gold license. Go to License Management to view upgrade options.', }); }); });