Skip to content

Commit

Permalink
[Alerting] License Errors on Alert List View (#89920) (#90932)
Browse files Browse the repository at this point in the history
* Adding tooltips to alert list and modal for license upgrade

* Fixing typings

* Custom License Error status. Moving modal to alerts list page

* Adding unit test

* Cleanup

* Unit tests

* Removing tooltip from alert name

* License

* PR fixes

* Updating modal wording

* Updating license state error message

* i18n fix

* Fixing functional test

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
ymao1 and kibanamachine authored Feb 10, 2021
1 parent 76f539e commit d113601
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 149 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/alerts/server/lib/license_state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."`
);
});

Expand Down
8 changes: 6 additions & 2 deletions x-pack/plugins/alerts/server/lib/license_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'
);
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -4835,7 +4835,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": "しきい値一致",
Expand Down
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -4841,7 +4841,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": "已达到阈值",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,116 +127,148 @@ 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);
});
});

describe('alerts_list component with items', () => {
let wrapper: ReactWrapper<any>;

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([
{
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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;
Expand Down
Loading

0 comments on commit d113601

Please sign in to comment.