From 329f1de367348e1f73b93d616c7f9d6844e8a2ac Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Tue, 3 May 2022 22:50:19 +0200 Subject: [PATCH 01/18] Retain APIKey when disabling/enabling a rule --- .../server/rules_client/rules_client.ts | 81 ++- .../server/rules_client/tests/disable.test.ts | 123 +---- .../server/rules_client/tests/enable.test.ts | 172 +++--- .../update_api_key_modal_confirmation.tsx | 63 +++ .../public/application/lib/rule_api/index.ts | 1 + .../lib/rule_api/update_api_key.test.ts | 26 + .../lib/rule_api/update_api_key.ts | 14 + .../collapsed_item_actions.test.tsx | 5 + .../components/collapsed_item_actions.tsx | 13 + .../rules_list/components/rules_list.test.tsx | 496 ++++++++++-------- .../rules_list/components/rules_list.tsx | 20 +- 11 files changed, 552 insertions(+), 462 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 00f67437ae4f2..73dcd0a2daf11 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1386,7 +1386,7 @@ export class RulesClient { } private async enableWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; + let existingApiKey: string | null = null; let attributes: RawRule; let version: string | undefined; @@ -1395,14 +1395,11 @@ export class RulesClient { await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { namespace: this.namespace, }); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + existingApiKey = decryptedAlert.attributes.apiKey; attributes = decryptedAlert.attributes; version = decryptedAlert.version; } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); + this.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`); // Still attempt to load the attributes and version using SOC const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; @@ -1444,19 +1441,10 @@ export class RulesClient { if (attributes.enabled === false) { const username = await this.getUserName(); - let createdAPIKey = null; - try { - createdAPIKey = await this.createAPIKey( - this.generateAPIKeyName(attributes.alertTypeId, attributes.name) - ); - } catch (error) { - throw Boom.badRequest(`Error enabling rule: could not create API key - ${error.message}`); - } - const updateAttributes = this.updateMeta({ ...attributes, + ...(!existingApiKey && (await this.createNewAPIKeySet({ attributes, username }))), enabled: true, - ...this.apiKeyAsAlertAttributes(createdAPIKey, username), updatedBy: username, updatedAt: new Date().toISOString(), executionStatus: { @@ -1467,15 +1455,10 @@ export class RulesClient { warning: null, }, }); + try { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { - // Avoid unused API key - markApiKeyForInvalidation( - { apiKey: updateAttributes.apiKey }, - this.logger, - this.unsecuredSavedObjectsClient - ); throw e; } const scheduledTask = await this.scheduleRule({ @@ -1488,16 +1471,28 @@ export class RulesClient { await this.unsecuredSavedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id, }); - if (apiKeyToInvalidate) { - await markApiKeyForInvalidation( - { apiKey: apiKeyToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ); - } } } + private async createNewAPIKeySet({ + attributes, + username, + }: { + attributes: RawRule; + username: string | null; + }): Promise> { + let createdAPIKey = null; + try { + createdAPIKey = await this.createAPIKey( + this.generateAPIKeyName(attributes.alertTypeId, attributes.name) + ); + } catch (error) { + throw Boom.badRequest(`Error enabling rule: could not create API key - ${error.message}`); + } + + return this.apiKeyAsAlertAttributes(createdAPIKey, username); + } + public async disable({ id }: { id: string }): Promise { return await retryIfConflicts( this.logger, @@ -1507,7 +1502,6 @@ export class RulesClient { } private async disableWithOCC({ id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; let attributes: RawRule; let version: string | undefined; @@ -1516,14 +1510,10 @@ export class RulesClient { await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { namespace: this.namespace, }); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; attributes = decryptedAlert.attributes; version = decryptedAlert.version; } catch (e) { - // We'll skip invalidating the API key since we failed to load the decrypted saved object - this.logger.error( - `disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` - ); + this.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`); // Still attempt to load the attributes and version using SOC const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; @@ -1612,30 +1602,19 @@ export class RulesClient { await this.unsecuredSavedObjectsClient.update( 'alert', id, - this.updateMeta({ + { ...attributes, enabled: false, scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, updatedBy: await this.getUserName(), updatedAt: new Date().toISOString(), - }), + }, { version } ); - await Promise.all([ - attributes.scheduledTaskId - ? this.taskManager.removeIfExists(attributes.scheduledTaskId) - : null, - apiKeyToInvalidate - ? await markApiKeyForInvalidation( - { apiKey: apiKeyToInvalidate }, - this.logger, - this.unsecuredSavedObjectsClient - ) - : null, - ]); + if (attributes.scheduledTaskId) { + await this.taskManager.removeIfExists(attributes.scheduledTaskId); + } } } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index f15b647a8e396..e77df720affab 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -14,7 +14,6 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; import { AlertingAuthorization } from '../../authorization/alerting_authorization'; import { ActionsAuthorization } from '@kbn/actions-plugin/server'; -import { InvalidatePendingApiKey } from '../../types'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { eventLoggerMock } from '@kbn/event-log-plugin/server/event_logger.mock'; @@ -107,6 +106,7 @@ describe('disable()', () => { attributes: { ...existingAlert.attributes, apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyOwner: 'elastic', }, version: '123', references: [], @@ -188,15 +188,6 @@ describe('disable()', () => { }); test('disables an alert', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -210,12 +201,9 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ @@ -235,21 +223,9 @@ describe('disable()', () => { } ); expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); - expect( - (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId - ).toBe('123'); }); test('disables the rule with calling event log to "recover" the alert instances from the task state', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); const scheduledTaskId = 'task-123'; taskManager.get.mockResolvedValue({ id: scheduledTaskId, @@ -292,12 +268,10 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, + scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ @@ -317,9 +291,6 @@ describe('disable()', () => { } ); expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); - expect( - (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId - ).toBe('123'); expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({ @@ -362,15 +333,6 @@ describe('disable()', () => { }); test('disables the rule even if unable to retrieve task manager doc to generate recovery event log events', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); taskManager.get.mockRejectedValueOnce(new Error('Fail')); await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); @@ -385,12 +347,9 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ @@ -410,9 +369,6 @@ describe('disable()', () => { } ); expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); - expect( - (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId - ).toBe('123'); expect(eventLogger.logEvent).toHaveBeenCalledTimes(0); expect(rulesClientParams.logger.warn).toHaveBeenCalledWith( @@ -422,16 +378,6 @@ describe('disable()', () => { test('falls back when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); - await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -445,12 +391,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ @@ -470,7 +411,6 @@ describe('disable()', () => { } ); expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123'); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't disable already disabled alerts`, async () => { @@ -483,56 +423,19 @@ describe('disable()', () => { }, }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); - await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.removeIfExists).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - test(`doesn't invalidate when no API key is used`, async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); - - await rulesClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when failing to load decrypted saved object', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt: '2019-02-12T21:01:22.479Z', - }, - references: [], - }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await rulesClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(taskManager.removeIfExists).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(rulesClientParams.logger.error).toHaveBeenCalledWith( - 'disable(): Failed to load API key to invalidate on alert 1: Fail' + 'disable(): Failed to load API key of alert 1: Fail' ); }); @@ -544,14 +447,6 @@ describe('disable()', () => { ); }); - test('swallows error when invalidate API key throws', async () => { - unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); - await rulesClient.disable({ id: '1' }); - expect(rulesClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' - ); - }); - test('throws when failing to remove task from task manager', async () => { taskManager.removeIfExists.mockRejectedValueOnce(new Error('Failed to remove task')); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 0a4737006d557..45344ff599c6b 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -16,7 +16,6 @@ import { AlertingAuthorization } from '../../authorization/alerting_authorizatio import { ActionsAuthorization } from '@kbn/actions-plugin/server'; import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; -import { InvalidatePendingApiKey } from '../../types'; import { getBeforeSetup, setGlobalDate } from './lib'; const taskManager = taskManagerMock.createStart(); @@ -47,23 +46,22 @@ const rulesClientParams: jest.Mocked = { auditLogger, }; -beforeEach(() => { - getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); - (auditLogger.log as jest.Mock).mockClear(); -}); - setGlobalDate(); describe('enable()', () => { let rulesClient: RulesClient; - const existingAlert = { + + const existingRule = { id: '1', type: 'alert', attributes: { + name: 'name', consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', actions: [ { group: 'default', @@ -80,23 +78,24 @@ describe('enable()', () => { references: [], }; + const existingRuleWithoutApiKey = { + ...existingRule, + attributes: { + ...existingRule.attributes, + apiKey: null, + apiKeyOwner: null, + }, + }; + beforeEach(() => { + getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); + (auditLogger.log as jest.Mock).mockClear(); rulesClient = new RulesClient(rulesClientParams); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRule); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingRuleWithoutApiKey); rulesClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, - }); taskManager.schedule.mockResolvedValue({ id: '1', scheduledAt: new Date(), @@ -183,38 +182,17 @@ describe('enable()', () => { }); test('enables a rule', async () => { - const createdAt = new Date().toISOString(); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt, - }, - references: [], - }); - await rulesClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); - expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { + name: 'name', schedule: { interval: '10s' }, alertTypeId: 'myType', consumer: 'myApp', @@ -224,8 +202,8 @@ describe('enable()', () => { }, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', - apiKey: null, - apiKeyOwner: null, + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', actions: [ { group: 'default', @@ -272,40 +250,65 @@ describe('enable()', () => { }); }); - test('invalidates API key if ever one existed prior to updating', async () => { - const createdAt = new Date().toISOString(); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt, - }, - references: [], + test('enables a rule that does not have an apiKey', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRuleWithoutApiKey); + rulesClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, }); - await rulesClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect( - (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId - ).toBe('123'); + expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); + expect(rulesClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/name'); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + name: 'name', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + updatedAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + executionStatus: { + status: 'pending', + lastDuration: 0, + lastExecutionDate: '2019-02-12T21:01:22.479Z', + error: null, + warning: null, + }, + }, + { + version: '123', + } + ); }); test(`doesn't enable already enabled alerts`, async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...existingAlert, + ...existingRuleWithoutApiKey, attributes: { - ...existingAlert.attributes, + ...existingRuleWithoutApiKey.attributes, enabled: true, }, }); @@ -328,6 +331,7 @@ describe('enable()', () => { 'alert', '1', { + name: 'name', schedule: { interval: '10s' }, alertTypeId: 'myType', consumer: 'myApp', @@ -365,10 +369,12 @@ describe('enable()', () => { }); test('throws an error if API key creation throws', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRuleWithoutApiKey); + rulesClientParams.createAPIKey.mockImplementation(() => { throw new Error('no'); }); - expect( + await expect( async () => await rulesClient.enable({ id: '1' }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Error enabling rule: could not create API key - no"` @@ -381,7 +387,7 @@ describe('enable()', () => { await rulesClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(rulesClientParams.logger.error).toHaveBeenCalledWith( - 'enable(): Failed to load API key to invalidate on alert 1: Fail' + 'enable(): Failed to load API key of alert 1: Fail' ); }); @@ -399,31 +405,17 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { - const createdAt = new Date().toISOString(); rulesClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'api_key_pending_invalidation', - attributes: { - apiKeyId: '123', - createdAt, - }, - references: [], - }); await expect(rulesClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update"` ); expect(rulesClientParams.getUserName).toHaveBeenCalled(); - expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); - expect( - (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId - ).toBe('123'); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); expect(taskManager.schedule).not.toHaveBeenCalled(); }); @@ -431,9 +423,9 @@ describe('enable()', () => { test('throws error when failing to update the second time', async () => { unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ - ...existingAlert, + ...existingRuleWithoutApiKey, attributes: { - ...existingAlert.attributes, + ...existingRuleWithoutApiKey.attributes, enabled: true, }, }); @@ -445,7 +437,6 @@ describe('enable()', () => { `"Fail to update second time"` ); expect(rulesClientParams.getUserName).toHaveBeenCalled(); - expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); expect(taskManager.schedule).toHaveBeenCalled(); }); @@ -457,16 +448,15 @@ describe('enable()', () => { `"Fail to schedule"` ); expect(rulesClientParams.getUserName).toHaveBeenCalled(); - expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); test('enables a rule if conflict errors received when scheduling a task', async () => { const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, + ...existingRuleWithoutApiKey, attributes: { - ...existingAlert.attributes, + ...existingRuleWithoutApiKey.attributes, enabled: true, apiKey: null, apiKeyOwner: null, @@ -492,11 +482,11 @@ describe('enable()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); - expect(rulesClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { + name: 'name', schedule: { interval: '10s' }, alertTypeId: 'myType', consumer: 'myApp', @@ -506,8 +496,8 @@ describe('enable()', () => { }, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', - apiKey: null, - apiKeyOwner: null, + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'elastic', actions: [ { group: 'default', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx new file mode 100644 index 0000000000000..9282675276f33 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiConfirmModal } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { HttpSetup } from '@kbn/core/public'; +import { useKibana } from '../../common/lib/kibana'; +export const UpdateApiKeyModalConfirmation = ({ + onCancel, + idsToUpdate, + apiUpdateApiKeyCall, + setIsLoadingState, + onUpdated, +}: { + onCancel: () => void; + idsToUpdate: string[]; + apiUpdateApiKeyCall: ({ id, http }: { id: string; http: HttpSetup }) => Promise; + setIsLoadingState: (isLoading: boolean) => void; + onUpdated: () => void; +}) => { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const [updateModalFlyoutVisible, setUpdateModalVisibility] = useState(false); + + useEffect(() => { + setUpdateModalVisibility(idsToUpdate.length > 0); + }, [idsToUpdate]); + + return updateModalFlyoutVisible ? ( + { + setUpdateModalVisibility(false); + onCancel(); + }} + onConfirm={async () => { + setUpdateModalVisibility(false); + setIsLoadingState(true); + try { + await apiUpdateApiKeyCall({ id: idsToUpdate[0], http }); + toasts.addSuccess('Updated'); + } catch (e) { + toasts.addError(e, { title: 'Failed' }); + } + setIsLoadingState(false); + onUpdated(); + }} + cancelButtonText="Cancel" + confirmButtonText="OK" + > + Are you sure + + ) : null; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index 89ede79f4a21d..9c0a97f2b8e58 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -27,3 +27,4 @@ export { updateRule } from './update'; export { resolveRule } from './resolve_rule'; export { snoozeRule } from './snooze'; export { unsnoozeRule } from './unsnooze'; +export { updateAPIKey } from './update_api_key'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.test.ts new file mode 100644 index 0000000000000..15d1e6a58596e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { updateAPIKey } from './update_api_key'; + +const http = httpServiceMock.createStartContract(); +beforeEach(() => jest.resetAllMocks()); + +describe('updateAPIKey', () => { + test('should call _update_api_key rule API', async () => { + const result = await updateAPIKey({ http, id: '1/' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/alerting/rule/1%2F/_update_api_key", + ], + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts new file mode 100644 index 0000000000000..bc10217d441a2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update_api_key.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; + +export async function updateAPIKey({ id, http }: { id: string; http: HttpSetup }): Promise { + return http.post( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_update_api_key` + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx index 8ce6736aee8ad..53938b7da370b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx @@ -21,6 +21,7 @@ const disableRule = jest.fn(); const enableRule = jest.fn(); const unmuteRule = jest.fn(); const muteRule = jest.fn(); +const onUpdateAPIKey = jest.fn(); export const tick = (ms = 0) => new Promise((resolve) => { @@ -91,6 +92,7 @@ describe('CollapsedItemActions', () => { enableRule, unmuteRule, muteRule, + onUpdateAPIKey, }; }; @@ -118,6 +120,7 @@ describe('CollapsedItemActions', () => { expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="updateApiKey"]').exists()).toBeFalsy(); wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); await act(async () => { @@ -130,6 +133,7 @@ describe('CollapsedItemActions', () => { expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="updateApiKey"]').exists()).toBeTruthy(); expect( wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled @@ -143,6 +147,7 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); + expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update APIKey'); }); test('handles case when rule is unmuted and enabled and mute is clicked', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index 4fcecc3410f17..8129fef548c5b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -23,6 +23,7 @@ export type ComponentOpts = { onRuleChanged: () => void; setRulesToDelete: React.Dispatch>; onEditRule: (item: RuleTableItem) => void; + onUpdateAPIKey: (id: string[]) => void; } & Pick; export const CollapsedItemActions: React.FunctionComponent = ({ @@ -34,6 +35,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ muteRule, setRulesToDelete, onEditRule, + onUpdateAPIKey, }: ComponentOpts) => { const { ruleTypeRegistry } = useKibana().services; @@ -53,6 +55,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( @@ -143,6 +146,15 @@ export const CollapsedItemActions: React.FunctionComponent = ({ { defaultMessage: 'Delete rule' } ), }, + { + disabled: !item.isEditable, + 'data-test-subj': 'updateApiKey', + onClick: () => { + setIsPopoverOpen(!isPopoverOpen); + onUpdateAPIKey([item.id]); + }, + name: 'Update APIKey', + }, ], }, ]; @@ -161,6 +173,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ panels={panels} className="actCollapsedItemActions" data-test-subj="collapsedActionPanel" + data-testid="collapsedActionPanel" /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 727898d42a076..36b424c568ffa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -6,9 +6,9 @@ */ import * as React from 'react'; +import { fireEvent, act, render, screen } from '@testing-library/react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { ReactWrapper } from 'enzyme'; -import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; import { RulesList, percentileFields } from './rules_list'; @@ -22,8 +22,10 @@ import { import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/monitoring_utils'; import { useKibana } from '../../../../common/lib/kibana'; -jest.mock('../../../../common/lib/kibana'); +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { IToasts } from '@kbn/core/public'; +jest.mock('../../../../common/lib/kibana'); jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), loadAllActions: jest.fn(), @@ -32,6 +34,7 @@ jest.mock('../../../lib/rule_api', () => ({ loadRules: jest.fn(), loadRuleTypes: jest.fn(), loadRuleAggregations: jest.fn(), + updateAPIKey: jest.fn(), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, @@ -59,7 +62,7 @@ jest.mock('../../../lib/capabilities', () => ({ hasShowActionsCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), })); -const { loadRules, loadRuleTypes, loadRuleAggregations } = +const { loadRules, loadRuleTypes, loadRuleAggregations, updateAPIKey } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -95,6 +98,208 @@ ruleTypeRegistry.list.mockReturnValue([ruleType]); actionTypeRegistry.list.mockReturnValue([]); const useKibanaMock = useKibana as jest.Mocked; +const mockedRulesData = [ + { + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '1s' }, + actions: [], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + monitoring: { + execution: { + history: [ + { + success: true, + duration: 1000000, + }, + { + success: true, + duration: 200000, + }, + { + success: false, + duration: 300000, + }, + ], + calculated_metrics: { + success_ratio: 0.66, + p50: 200000, + p95: 300000, + p99: 300000, + }, + }, + }, + }, + { + id: '2', + name: 'test rule ok', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'ok', + lastDuration: 61000, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + monitoring: { + execution: { + history: [ + { + success: true, + duration: 100000, + }, + { + success: true, + duration: 500000, + }, + ], + calculated_metrics: { + success_ratio: 1, + p50: 0, + p95: 100000, + p99: 500000, + }, + }, + }, + }, + { + id: '3', + name: 'test rule pending', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'pending', + lastDuration: 30234, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: null, + }, + monitoring: { + execution: { + history: [{ success: false, duration: 100 }], + calculated_metrics: { + success_ratio: 0, + }, + }, + }, + }, + { + id: '4', + name: 'test rule error', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'error', + lastDuration: 122000, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: RuleExecutionStatusErrorReasons.Unknown, + message: 'test', + }, + }, + }, + { + id: '5', + name: 'test rule license error', + tags: [], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'error', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: RuleExecutionStatusErrorReasons.License, + message: 'test', + }, + }, + }, + { + id: '6', + name: 'test rule warning', + tags: [], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule type name' }, + scheduledTaskId: null, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'warning', + lastDuration: 500, + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + warning: { + reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, + message: 'test', + }, + }, + }, +]; + describe('rules_list component empty', () => { let wrapper: ReactWrapper; async function setup() { @@ -162,208 +367,6 @@ describe('rules_list component empty', () => { describe('rules_list component with items', () => { let wrapper: ReactWrapper; - const mockedRulesData = [ - { - id: '1', - name: 'test rule', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '1s' }, - actions: [], - params: { name: 'test rule type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'active', - lastDuration: 500, - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: null, - }, - monitoring: { - execution: { - history: [ - { - success: true, - duration: 1000000, - }, - { - success: true, - duration: 200000, - }, - { - success: false, - duration: 300000, - }, - ], - calculated_metrics: { - success_ratio: 0.66, - p50: 200000, - p95: 300000, - p99: 300000, - }, - }, - }, - }, - { - id: '2', - name: 'test rule ok', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test rule type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'ok', - lastDuration: 61000, - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: null, - }, - monitoring: { - execution: { - history: [ - { - success: true, - duration: 100000, - }, - { - success: true, - duration: 500000, - }, - ], - calculated_metrics: { - success_ratio: 1, - p50: 0, - p95: 100000, - p99: 500000, - }, - }, - }, - }, - { - id: '3', - name: 'test rule pending', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test rule type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'pending', - lastDuration: 30234, - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: null, - }, - monitoring: { - execution: { - history: [{ success: false, duration: 100 }], - calculated_metrics: { - success_ratio: 0, - }, - }, - }, - }, - { - id: '4', - name: 'test rule error', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'error', - lastDuration: 122000, - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: { - reason: RuleExecutionStatusErrorReasons.Unknown, - message: 'test', - }, - }, - }, - { - id: '5', - name: 'test rule license error', - tags: [], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'error', - lastDuration: 500, - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: { - reason: RuleExecutionStatusErrorReasons.License, - message: 'test', - }, - }, - }, - { - id: '6', - name: 'test rule warning', - tags: [], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule type name' }, - scheduledTaskId: null, - createdBy: null, - updatedBy: null, - apiKeyOwner: null, - throttle: '1m', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'warning', - lastDuration: 500, - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - warning: { - reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, - message: 'test', - }, - }, - }, - ]; - async function setup(editable: boolean = true) { loadRules.mockResolvedValue({ page: 1, @@ -1084,3 +1087,86 @@ describe('rules_list with disabled items', () => { ).toEqual('This rule type requires a Platinum license.'); }); }); + +describe('Update Api Key', () => { + const addSuccess = jest.fn(); + const addError = jest.fn(); + + beforeAll(() => { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: mockedRulesData, + }); + loadActionTypes.mockResolvedValue([]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + useKibanaMock().services.notifications.toasts = { + addSuccess, + addError, + } as unknown as IToasts; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Updates the apiKey successfully', async () => { + updateAPIKey.mockResolvedValueOnce(204); + render( + + + + ); + expect(await screen.findByText('test rule ok')).toBeInTheDocument(); + + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Update APIKey')); + expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Cancel')); + expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Update APIKey')); + expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByText('OK')); + }); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); + expect(loadRules).toHaveBeenCalledTimes(2); + expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + expect(addSuccess).toHaveBeenCalledWith('Updated'); + }); + + it('Update apiKey fails', async () => { + updateAPIKey.mockRejectedValueOnce(500); + render( + + + + ); + + expect(await screen.findByText('test rule ok')).toBeInTheDocument(); + + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Update APIKey')); + expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByText('OK')); + }); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); + expect(loadRules).toHaveBeenCalledTimes(2); + expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + expect(addError).toHaveBeenCalledWith(500, { title: 'Failed' }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 57c59f3f09782..ec35886f04eb2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -78,6 +78,7 @@ import { snoozeRule, unsnoozeRule, deleteRules, + updateAPIKey, } from '../../../lib/rule_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; @@ -98,6 +99,7 @@ import { RuleDurationFormat } from './rule_duration_format'; import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; import { triggersActionsUiConfig } from '../../../../common/lib/config_api'; +import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation'; const ENTER_KEY = 13; @@ -208,6 +210,7 @@ export const RulesList: React.FunctionComponent = () => { totalItemCount: 0, }); const [rulesToDelete, setRulesToDelete] = useState([]); + const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); const onRuleEdit = (ruleItem: RuleTableItem) => { setEditFlyoutVisibility(true); setCurrentRuleToEdit(ruleItem); @@ -865,6 +868,7 @@ export const RulesList: React.FunctionComponent = () => { onRuleChanged={() => loadRulesData()} setRulesToDelete={setRulesToDelete} onEditRule={() => onRuleEdit(item)} + onUpdateAPIKey={setRulesToUpdateAPIKey} /> @@ -1274,7 +1278,7 @@ export const RulesList: React.FunctionComponent = () => { await loadRulesData(); }} onErrors={async () => { - // Refresh the rules from the server, some rules may have beend deleted + // Refresh the rules from the server, some rules may have been deleted await loadRulesData(); setRulesToDelete([]); }} @@ -1293,6 +1297,20 @@ export const RulesList: React.FunctionComponent = () => { setRulesState({ ...rulesState, isLoading }); }} /> + { + setRulesToUpdateAPIKey([]); + }} + idsToUpdate={rulesToUpdateAPIKey} + apiUpdateApiKeyCall={updateAPIKey} + setIsLoadingState={(isLoading: boolean) => { + setRulesState({ ...rulesState, isLoading }); + }} + onUpdated={async () => { + setRulesToUpdateAPIKey([]); + await loadRulesData(); + }} + /> {getRulesList()} {ruleFlyoutVisible && ( From 76b3afec368e90f9d725043929d0abfd6e94749d Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Thu, 5 May 2022 14:45:19 +0200 Subject: [PATCH 02/18] fix failing test --- .../rules_list/components/rules_list.test.tsx | 1630 +++++++++-------- 1 file changed, 821 insertions(+), 809 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index c7d875467be51..0527695f26011 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -69,9 +69,9 @@ jest.mock('../../../../common/get_experimental_features', () => ({ const { loadRules, loadRuleTypes, loadRuleAggregations, updateAPIKey } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); + const actionTypeRegistry = actionTypeRegistryMock.create(); const ruleTypeRegistry = ruleTypeRegistryMock.create(); - const ruleType = { id: 'test_rule_type', description: 'test', @@ -100,11 +100,8 @@ const ruleTypeFromApi = { }; ruleTypeRegistry.list.mockReturnValue([ruleType]); actionTypeRegistry.list.mockReturnValue([]); -const useKibanaMock = useKibana as jest.Mocked; -beforeEach(() => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); -}); +const useKibanaMock = useKibana as jest.Mocked; const mockedRulesData = [ { @@ -308,906 +305,921 @@ const mockedRulesData = [ }, ]; -describe('rules_list component empty', () => { - let wrapper: ReactWrapper; - async function setup() { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], +describe('rules_list', () => { + beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); + }); + + describe('Update Api Key', () => { + const addSuccess = jest.fn(); + const addError = jest.fn(); + + beforeAll(() => { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: mockedRulesData, + }); + loadActionTypes.mockResolvedValue([]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + useKibanaMock().services.notifications.toasts = { + addSuccess, + addError, + } as unknown as IToasts; }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + afterEach(() => { + jest.clearAllMocks(); + }); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + it('Updates the apiKey successfully', async () => { + updateAPIKey.mockResolvedValueOnce(204); + render( + + + + ); + expect(await screen.findByText('test rule ok')).toBeInTheDocument(); - wrapper = mountWithIntl(); + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } + fireEvent.click(screen.getByText('Update APIKey')); + expect(screen.getByText('Confirm Update')).toBeInTheDocument(); - it('renders empty list', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="createFirstRuleEmptyPrompt"]').exists()).toBeTruthy(); - }); + fireEvent.click(screen.getByText('Cancel')); + expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); - it('renders Create rule button', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="createFirstRuleButton"]').find('EuiButton')).toHaveLength( - 1 - ); - expect(wrapper.find('RuleAdd').exists()).toBeFalsy(); + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - wrapper.find('button[data-test-subj="createFirstRuleButton"]').simulate('click'); + fireEvent.click(screen.getByText('Update APIKey')); + expect(screen.getByText('Confirm Update')).toBeInTheDocument(); - await act(async () => { - // When the RuleAdd component is rendered, it waits for the healthcheck to resolve - await new Promise((resolve) => { - setTimeout(resolve, 1000); + await act(async () => { + fireEvent.click(screen.getByText('OK')); }); - - await nextTick(); - wrapper.update(); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); + expect(loadRules).toHaveBeenCalledTimes(2); + expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + expect(addSuccess).toHaveBeenCalledWith('Updated'); }); - expect(wrapper.find('RuleAdd').exists()).toEqual(true); - }); -}); + it('Update apiKey fails', async () => { + updateAPIKey.mockRejectedValueOnce(500); + render( + + + + ); -describe('rules_list component with items', () => { - let wrapper: ReactWrapper; + expect(await screen.findByText('test rule ok')).toBeInTheDocument(); - async function setup(editable: boolean = true) { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 4, - data: mockedRulesData, - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - loadRuleAggregations.mockResolvedValue({ - ruleEnabledStatus: { enabled: 2, disabled: 0 }, - ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, - ruleMutedStatus: { muted: 0, unmuted: 2 }, + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Update APIKey')); + expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByText('OK')); + }); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); + expect(loadRules).toHaveBeenCalledTimes(2); + expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + expect(addError).toHaveBeenCalledWith(500, { title: 'Failed' }); }); + }); - const ruleTypeMock: RuleTypeModel = { - id: 'test_rule_type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: () => { - return { errors: {} }; - }, - ruleParamsExpression: jest.fn(), - requiresAppContext: !editable, - }; + describe('rules_list component empty', () => { + let wrapper: ReactWrapper; + async function setup() { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: [], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); - ruleTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - await act(async () => { - await nextTick(); - wrapper.update(); - }); + wrapper = mountWithIntl(); - expect(loadRules).toHaveBeenCalled(); - expect(loadActionTypes).toHaveBeenCalled(); - expect(loadRuleAggregations).toHaveBeenCalled(); - } - - it('renders table of rules', async () => { - // Use fake timers so we don't have to wait for the EuiToolTip timeout - jest.useFakeTimers(); - await setup(); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(mockedRulesData.length); - - // Name and rule type column - const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-name"]'); - expect(ruleNameColumns.length).toEqual(mockedRulesData.length); - mockedRulesData.forEach((rule, index) => { - expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${ruleTypeFromApi.name}`); - }); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } - // Tags column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-tagsPopover"]').length - ).toEqual(mockedRulesData.length); - // only show tags popover if tags exist on rule - const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagBadge"]'); - expect(tagsBadges.length).toEqual( - mockedRulesData.filter((data) => data.tags.length > 0).length - ); - - // Last run column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastExecutionDate"]').length - ).toEqual(mockedRulesData.length); - - // Last run tooltip - wrapper - .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') - .first() - .simulate('mouseOver'); - - // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); - - wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.'); - - wrapper - .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') - .first() - .simulate('mouseOut'); - - // Schedule interval column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-interval"]').length - ).toEqual(mockedRulesData.length); - - // Schedule interval tooltip - wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOver'); - - // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); - - wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe( - 'Below configured minimum intervalRule interval of 1 second is below the minimum configured interval of 1 minute. This may impact alerting performance.' - ); - - wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOut'); - - // Duration column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-duration"]').length - ).toEqual(mockedRulesData.length); - // show warning if duration is long - const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); - expect(durationWarningIcon.length).toEqual( - mockedRulesData.filter( - (data) => data.executionStatus.lastDuration > parseDuration(ruleTypeFromApi.ruleTaskTimeout) - ).length - ); - - // Duration tooltip - wrapper.find('[data-test-subj="rulesTableCell-durationTooltip"]').first().simulate('mouseOver'); - - // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); - - wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe( - 'The length of time it took for the rule to run (mm:ss).' - ); - - // Last response column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastResponse"]').length - ).toEqual(mockedRulesData.length); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-warning"]').length).toEqual(1); - expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2); - expect( - wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length - ).toEqual(1); - - expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy(); - - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( - 'Error' - ); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').last().text()).toEqual( - 'License Error' - ); - - // Status control column - expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length).toEqual( - mockedRulesData.length - ); - - // Monitoring column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"]').length - ).toEqual(mockedRulesData.length); - const ratios = wrapper.find( - 'EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"] span[data-test-subj="successRatio"]' - ); - - mockedRulesData.forEach((rule, index) => { - if (rule.monitoring) { - expect(ratios.at(index).text()).toEqual( - `${rule.monitoring.execution.calculated_metrics.success_ratio * 100}%` - ); - } else { - expect(ratios.at(index).text()).toEqual(`N/A`); - } + it('renders empty list', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="createFirstRuleEmptyPrompt"]').exists()).toBeTruthy(); }); - // P50 column is rendered initially - expect( - wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`).exists() - ).toBeTruthy(); - - let percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` - ); - - mockedRulesData.forEach((rule, index) => { - if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { - // Ensure the table cells are getting the correct values - expect(percentiles.at(index).text()).toEqual( - getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50) - ); - // Ensure the tooltip is showing the correct content - expect( - wrapper - .find( - 'EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' - ) - .at(index) - .props().content - ).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50)); - } else { - expect(percentiles.at(index).text()).toEqual('N/A'); - } + it('renders Create rule button', async () => { + await setup(); + expect( + wrapper.find('[data-test-subj="createFirstRuleButton"]').find('EuiButton') + ).toHaveLength(1); + expect(wrapper.find('RuleAdd').exists()).toBeFalsy(); + + wrapper.find('button[data-test-subj="createFirstRuleButton"]').simulate('click'); + + await act(async () => { + // When the RuleAdd component is rendered, it waits for the healthcheck to resolve + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('RuleAdd').exists()).toEqual(true); }); + }); - // Click column to sort by P50 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P50], - direction: 'asc', + describe('rules_list component with items', () => { + let wrapper: ReactWrapper; + + async function setup(editable: boolean = true) { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 4, + data: mockedRulesData, + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', }, - }) - ); - - // Click column again to reverse sort by P50 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P50], - direction: 'desc', + { + id: 'test2', + name: 'Test2', }, - }) - ); - - // Hover over percentile selection button - wrapper - .find('[data-test-subj="percentileSelectablePopover-iconButton"]') - .first() - .simulate('click'); - - jest.runAllTimers(); - wrapper.update(); - - // Percentile Selection - expect( - wrapper.find('[data-test-subj="percentileSelectablePopover-selectable"]').exists() - ).toBeTruthy(); - - const percentileOptions = wrapper.find( - '[data-test-subj="percentileSelectablePopover-selectable"] li' - ); - expect(percentileOptions.length).toEqual(3); - - // Select P95 - percentileOptions.at(1).simulate('click'); - - jest.runAllTimers(); - wrapper.update(); - - expect( - wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`).exists() - ).toBeTruthy(); - - percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` - ); - - mockedRulesData.forEach((rule, index) => { - if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { - expect(percentiles.at(index).text()).toEqual( - getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) - ); - } else { - expect(percentiles.at(index).text()).toEqual('N/A'); - } - }); + ]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + loadRuleAggregations.mockResolvedValue({ + ruleEnabledStatus: { enabled: 2, disabled: 0 }, + ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, + ruleMutedStatus: { muted: 0, unmuted: 2 }, + }); - // Click column to sort by P95 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P95], - direction: 'asc', + const ruleTypeMock: RuleTypeModel = { + id: 'test_rule_type', + iconClass: 'test', + description: 'Rule when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; }, - }) - ); - - // Click column again to reverse sort by P95 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P95], - direction: 'desc', - }, - }) - ); + ruleParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - // Clearing all mocks will also reset fake timers. - jest.clearAllMocks(); - }); + expect(loadRules).toHaveBeenCalled(); + expect(loadActionTypes).toHaveBeenCalled(); + expect(loadRuleAggregations).toHaveBeenCalled(); + } + + it('renders table of rules', async () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(mockedRulesData.length); + + // Name and rule type column + const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-name"]'); + expect(ruleNameColumns.length).toEqual(mockedRulesData.length); + mockedRulesData.forEach((rule, index) => { + expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${ruleTypeFromApi.name}`); + }); - it('loads rules when refresh button is clicked', async () => { - await setup(); - wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click'); + // Tags column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-tagsPopover"]').length + ).toEqual(mockedRulesData.length); + // only show tags popover if tags exist on rule + const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagBadge"]'); + expect(tagsBadges.length).toEqual( + mockedRulesData.filter((data) => data.tags.length > 0).length + ); + + // Last run column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastExecutionDate"]').length + ).toEqual(mockedRulesData.length); + + // Last run tooltip + wrapper + .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') + .first() + .simulate('mouseOver'); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); - await act(async () => { - await nextTick(); wrapper.update(); - }); + expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.'); - expect(loadRules).toHaveBeenCalled(); - }); + wrapper + .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') + .first() + .simulate('mouseOut'); - 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="ruleStatus-error-license-fix"]').length - ).toEqual(1); - wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + // Schedule interval column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-interval"]').length + ).toEqual(mockedRulesData.length); - 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(); - }); + // Schedule interval tooltip + wrapper + .find('[data-test-subj="ruleInterval-config-tooltip-0"]') + .first() + .simulate('mouseOver'); - it('sorts rules when clicking the name column', async () => { - await setup(); - wrapper - .find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton') - .first() - .simulate('click'); + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); - await act(async () => { - await nextTick(); wrapper.update(); - }); + expect(wrapper.find('.euiToolTipPopover').text()).toBe( + 'Below configured minimum intervalRule interval of 1 second is below the minimum configured interval of 1 minute. This may impact alerting performance.' + ); + + wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOut'); + + // Duration column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-duration"]').length + ).toEqual(mockedRulesData.length); + // show warning if duration is long + const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); + expect(durationWarningIcon.length).toEqual( + mockedRulesData.filter( + (data) => + data.executionStatus.lastDuration > parseDuration(ruleTypeFromApi.ruleTaskTimeout) + ).length + ); + + // Duration tooltip + wrapper + .find('[data-test-subj="rulesTableCell-durationTooltip"]') + .first() + .simulate('mouseOver'); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: 'name', - direction: 'desc', - }, - }) - ); - }); + wrapper.update(); + expect(wrapper.find('.euiToolTipPopover').text()).toBe( + 'The length of time it took for the rule to run (mm:ss).' + ); + + // Last response column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastResponse"]').length + ).toEqual(mockedRulesData.length); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-warning"]').length).toEqual(1); + expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2); + expect( + wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length + ).toEqual(1); + + expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy(); + + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( + 'Error' + ); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').last().text()).toEqual( + 'License Error' + ); + + // Status control column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length + ).toEqual(mockedRulesData.length); + + // Monitoring column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"]').length + ).toEqual(mockedRulesData.length); + const ratios = wrapper.find( + 'EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"] span[data-test-subj="successRatio"]' + ); + + mockedRulesData.forEach((rule, index) => { + if (rule.monitoring) { + expect(ratios.at(index).text()).toEqual( + `${rule.monitoring.execution.calculated_metrics.success_ratio * 100}%` + ); + } else { + expect(ratios.at(index).text()).toEqual(`N/A`); + } + }); - it('sorts rules when clicking the status control column', async () => { - await setup(); - wrapper - .find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton') - .first() - .simulate('click'); + // P50 column is rendered initially + expect( + wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`).exists() + ).toBeTruthy(); + + let percentiles = wrapper.find( + `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` + ); + + mockedRulesData.forEach((rule, index) => { + if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { + // Ensure the table cells are getting the correct values + expect(percentiles.at(index).text()).toEqual( + getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50) + ); + // Ensure the tooltip is showing the correct content + expect( + wrapper + .find( + 'EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' + ) + .at(index) + .props().content + ).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50)); + } else { + expect(percentiles.at(index).text()).toEqual('N/A'); + } + }); - await act(async () => { - await nextTick(); - wrapper.update(); - }); + // Click column to sort by P50 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P50], + direction: 'asc', + }, + }) + ); + + // Click column again to reverse sort by P50 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P50], + direction: 'desc', + }, + }) + ); - expect(loadRules).toHaveBeenLastCalledWith( - expect.objectContaining({ - sort: { - field: 'enabled', - direction: 'asc', - }, - }) - ); - }); + // Hover over percentile selection button + wrapper + .find('[data-test-subj="percentileSelectablePopover-iconButton"]') + .first() + .simulate('click'); - it('renders edit and delete buttons when user can manage rules', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); - }); + jest.runAllTimers(); + wrapper.update(); - it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { - await setup(false); - expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); - }); + // Percentile Selection + expect( + wrapper.find('[data-test-subj="percentileSelectablePopover-selectable"]').exists() + ).toBeTruthy(); - it('renders brief', async () => { - await setup(); - - // { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 } - expect(wrapper.find('EuiHealth[data-test-subj="totalOkRulesCount"]').text()).toEqual('Ok: 1'); - expect(wrapper.find('EuiHealth[data-test-subj="totalActiveRulesCount"]').text()).toEqual( - 'Active: 2' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalErrorRulesCount"]').text()).toEqual( - 'Error: 3' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalPendingRulesCount"]').text()).toEqual( - 'Pending: 4' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalUnknownRulesCount"]').text()).toEqual( - 'Unknown: 5' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalWarningRulesCount"]').text()).toEqual( - 'Warning: 6' - ); - }); + const percentileOptions = wrapper.find( + '[data-test-subj="percentileSelectablePopover-selectable"] li' + ); + expect(percentileOptions.length).toEqual(3); - it('does not render the status filter if the feature flag is off', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeFalsy(); - }); + // Select P95 + percentileOptions.at(1).simulate('click'); - it('renders the status filter if the experiment is on', async () => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); - await setup(); - expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeTruthy(); - }); + jest.runAllTimers(); + wrapper.update(); - it('can filter by rule states', async () => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); - loadRules.mockReset(); - await setup(); + expect( + wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`).exists() + ).toBeTruthy(); + + percentiles = wrapper.find( + `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` + ); + + mockedRulesData.forEach((rule, index) => { + if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { + expect(percentiles.at(index).text()).toEqual( + getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) + ); + } else { + expect(percentiles.at(index).text()).toEqual('N/A'); + } + }); - expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]); + // Click column to sort by P95 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P95], + direction: 'asc', + }, + }) + ); + + // Click column again to reverse sort by P95 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P95], + direction: 'desc', + }, + }) + ); - wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click'); + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); + }); - wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click'); + it('loads rules when refresh button is clicked', async () => { + await setup(); + wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click'); - expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']); + await act(async () => { + await nextTick(); + wrapper.update(); + }); - wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); + expect(loadRules).toHaveBeenCalled(); + }); - expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']); + 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="ruleStatus-error-license-fix"]').length + ).toEqual(1); + wrapper + .find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]') + .simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); + 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(); + }); - expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); - }); -}); + it('sorts rules when clicking the name column', async () => { + await setup(); + wrapper + .find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton') + .first() + .simulate('click'); -describe('rules_list component empty with show only capability', () => { - let wrapper: ReactWrapper; + await act(async () => { + await nextTick(); + wrapper.update(); + }); - async function setup() { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - loadRuleTypes.mockResolvedValue([ - { id: 'test_rule_type', name: 'some rule type', authorizedConsumers: {} }, - ]); - loadAllActions.mockResolvedValue([]); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); - - await act(async () => { - await nextTick(); - wrapper.update(); + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: 'name', + direction: 'desc', + }, + }) + ); }); - } - it('not renders create rule button', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="createRuleButton"]')).toHaveLength(0); - }); -}); + it('sorts rules when clicking the status control column', async () => { + await setup(); + wrapper + .find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton') + .first() + .simulate('click'); -describe('rules_list with show only capability', () => { - let wrapper: ReactWrapper; + await act(async () => { + await nextTick(); + wrapper.update(); + }); - async function setup(editable: boolean = true) { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - name: 'test rule', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test rule 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 rule 2', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule 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, + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + sort: { + field: 'enabled', + direction: 'asc', }, - }, - ], + }) + ); }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - - const ruleTypeMock: RuleTypeModel = { - id: 'test_rule_type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: () => { - return { errors: {} }; - }, - ruleParamsExpression: jest.fn(), - requiresAppContext: !editable, - }; - ruleTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + it('renders edit and delete buttons when user can manage rules', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); + }); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { + await setup(false); + expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); + }); - await act(async () => { - await nextTick(); - wrapper.update(); + it('renders brief', async () => { + await setup(); + + // { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 } + expect(wrapper.find('EuiHealth[data-test-subj="totalOkRulesCount"]').text()).toEqual('Ok: 1'); + expect(wrapper.find('EuiHealth[data-test-subj="totalActiveRulesCount"]').text()).toEqual( + 'Active: 2' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalErrorRulesCount"]').text()).toEqual( + 'Error: 3' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalPendingRulesCount"]').text()).toEqual( + 'Pending: 4' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalUnknownRulesCount"]').text()).toEqual( + 'Unknown: 5' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalWarningRulesCount"]').text()).toEqual( + 'Warning: 6' + ); }); - } - it('renders table of rules with edit button disabled', async () => { - await setup(false); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); - }); + it('does not render the status filter if the feature flag is off', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeFalsy(); + }); - it('renders table of rules with delete button disabled', async () => { - const { hasAllPrivilege } = jest.requireMock('../../../lib/capabilities'); - hasAllPrivilege.mockReturnValue(false); - await setup(false); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); - }); + it('renders the status filter if the experiment is on', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + await setup(); + expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeTruthy(); + }); - it('renders table of rules with actions menu collapsedItemActions', async () => { - await setup(false); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="collapsedItemActions"]').length).toBeGreaterThan(0); - }); -}); + it('can filter by rule states', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + loadRules.mockReset(); + await setup(); -describe('rules_list with disabled items', () => { - let wrapper: ReactWrapper; + expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]); - async function setup() { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - name: 'test rule', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test rule 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 rule 2', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type_disabled_by_license', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule 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, - }, - }, - ], - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - - loadRuleTypes.mockResolvedValue([ - ruleTypeFromApi, - { - id: 'test_rule_type_disabled_by_license', - name: 'some rule type that is not allowed', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - actionVariables: { context: [], state: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - minimumLicenseRequired: 'platinum', - enabledInLicense: false, - authorizedConsumers: { - [ALERTS_FEATURE_ID]: { read: true, all: true }, - }, - }, - ]); - loadAllActions.mockResolvedValue([]); + wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click'); - ruleTypeRegistry.has.mockReturnValue(false); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click'); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']); - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders rules list with disabled indicator if disabled due to license', async () => { - await setup(); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); - expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( - 'actRulesList__tableRowDisabled' - ); - expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( - 1 - ); - expect( - wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().type - ).toEqual('questionInCircle'); - expect( - wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content - ).toEqual('This rule type requires a Platinum license.'); - }); -}); + wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); + + expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']); -describe('Update Api Key', () => { - const addSuccess = jest.fn(); - const addError = jest.fn(); + wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - beforeAll(() => { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: mockedRulesData, + expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); }); - loadActionTypes.mockResolvedValue([]); - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - useKibanaMock().services.notifications.toasts = { - addSuccess, - addError, - } as unknown as IToasts; }); - afterEach(() => { - jest.clearAllMocks(); - }); + describe('rules_list component empty with show only capability', () => { + let wrapper: ReactWrapper; - it('Updates the apiKey successfully', async () => { - updateAPIKey.mockResolvedValueOnce(204); - render( - - - - ); - expect(await screen.findByText('test rule ok')).toBeInTheDocument(); + async function setup() { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: [], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ + { id: 'test_rule_type', name: 'some rule type', authorizedConsumers: {} }, + ]); + loadAllActions.mockResolvedValue([]); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } - fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); - expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + it('not renders create rule button', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="createRuleButton"]')).toHaveLength(0); + }); + }); - fireEvent.click(screen.getByText('Update APIKey')); - expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + describe('rules_list with show only capability', () => { + let wrapper: ReactWrapper; - fireEvent.click(screen.getByText('Cancel')); - expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + async function setup(editable: boolean = true) { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 2, + data: [ + { + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule 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 rule 2', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule 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, + }, + }, + ], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + + const ruleTypeMock: RuleTypeModel = { + id: 'test_rule_type', + iconClass: 'test', + description: 'Rule when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } - fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); - expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + it('renders table of rules with edit button disabled', async () => { + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); + }); - fireEvent.click(screen.getByText('Update APIKey')); - expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + it('renders table of rules with delete button disabled', async () => { + const { hasAllPrivilege } = jest.requireMock('../../../lib/capabilities'); + hasAllPrivilege.mockReturnValue(false); + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); + }); - await act(async () => { - fireEvent.click(screen.getByText('OK')); + it('renders table of rules with actions menu collapsedItemActions', async () => { + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="collapsedItemActions"]').length).toBeGreaterThan(0); }); - expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); - expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); - expect(addSuccess).toHaveBeenCalledWith('Updated'); }); - it('Update apiKey fails', async () => { - updateAPIKey.mockRejectedValueOnce(500); - render( - - - - ); + describe('rules_list with disabled items', () => { + let wrapper: ReactWrapper; + + async function setup() { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 2, + data: [ + { + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule 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 rule 2', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type_disabled_by_license', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule 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, + }, + }, + ], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); - expect(await screen.findByText('test rule ok')).toBeInTheDocument(); + loadRuleTypes.mockResolvedValue([ + ruleTypeFromApi, + { + id: 'test_rule_type_disabled_by_license', + name: 'some rule type that is not allowed', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'platinum', + enabledInLicense: false, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + }, + }, + ]); + loadAllActions.mockResolvedValue([]); - fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); - expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + ruleTypeRegistry.has.mockReturnValue(false); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - fireEvent.click(screen.getByText('Update APIKey')); - expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); - await act(async () => { - fireEvent.click(screen.getByText('OK')); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('renders rules list with disabled indicator if disabled due to license', async () => { + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); + expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( + 'actRulesList__tableRowDisabled' + ); + expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( + 1 + ); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().type + ).toEqual('questionInCircle'); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content + ).toEqual('This rule type requires a Platinum license.'); }); - expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); - expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); - expect(addError).toHaveBeenCalledWith(500, { title: 'Failed' }); }); }); From ad84a32d02c0f2462b697518e95ebeb77b25171c Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Thu, 5 May 2022 20:40:21 +0200 Subject: [PATCH 03/18] Fix failing test --- x-pack/plugins/alerting/server/rules_client/rules_client.ts | 3 ++- .../plugins/alerting/server/rules_client/tests/disable.test.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 73dcd0a2daf11..1a794beee3030 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1604,6 +1604,8 @@ export class RulesClient { id, { ...attributes, + ...(!attributes.apiKeyOwner && { apiKeyOwner: null }), + ...(!attributes.apiKey && { apiKey: null }), enabled: false, scheduledTaskId: null, updatedBy: await this.getUserName(), @@ -1611,7 +1613,6 @@ export class RulesClient { }, { version } ); - if (attributes.scheduledTaskId) { await this.taskManager.removeIfExists(attributes.scheduledTaskId); } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index e77df720affab..e5ceab1f152c4 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -392,6 +392,8 @@ describe('disable()', () => { alertTypeId: 'myType', enabled: false, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ From 487dfbf131162441c4e911db274f217d2d6a07d4 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Fri, 6 May 2022 18:14:26 +0200 Subject: [PATCH 04/18] Translations --- .../update_api_key_modal_confirmation.tsx | 40 +++++++++++++++---- .../components/collapsed_item_actions.tsx | 17 ++++---- .../rules_list/components/rules_list.test.tsx | 29 +++++++------- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx index 9282675276f33..211347b017988 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx @@ -6,6 +6,7 @@ */ import { EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { HttpSetup } from '@kbn/core/public'; import { useKibana } from '../../common/lib/kibana'; @@ -35,9 +36,11 @@ export const UpdateApiKeyModalConfirmation = ({ return updateModalFlyoutVisible ? ( { setUpdateModalVisibility(false); onCancel(); @@ -47,17 +50,40 @@ export const UpdateApiKeyModalConfirmation = ({ setIsLoadingState(true); try { await apiUpdateApiKeyCall({ id: idsToUpdate[0], http }); - toasts.addSuccess('Updated'); + toasts.addSuccess( + i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.successMessage', { + defaultMessage: 'API Key has been updated', + }) + ); } catch (e) { - toasts.addError(e, { title: 'Failed' }); + toasts.addError(e, { + title: i18n.translate( + 'xpack.triggersActionsUI.updateApiKeyConfirmModal.failureMessage', + { + defaultMessage: 'Failed to update the API Key', + } + ), + }); } setIsLoadingState(false); onUpdated(); }} - cancelButtonText="Cancel" - confirmButtonText="OK" + cancelButtonText={i18n.translate( + 'xpack.triggersActionsUI.updateApiKeyConfirmModal.cancelButton', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.triggersActionsUI.updateApiKeyConfirmModal.confirmButton', + { + defaultMessage: 'Update', + } + )} > - Are you sure + {i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.description', { + defaultMessage: "You can't recover the old API Key", + })} ) : null; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index 8129fef548c5b..4ee0e37f53105 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -136,24 +136,27 @@ export const CollapsedItemActions: React.FunctionComponent = ({ }, { disabled: !item.isEditable, - 'data-test-subj': 'deleteRule', + 'data-test-subj': 'updateApiKey', onClick: () => { setIsPopoverOpen(!isPopoverOpen); - setRulesToDelete([item.id]); + onUpdateAPIKey([item.id]); }, name: i18n.translate( - 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.deleteRuleTitle', - { defaultMessage: 'Delete rule' } + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActions.updateApiKey', + { defaultMessage: 'Update API Key' } ), }, { disabled: !item.isEditable, - 'data-test-subj': 'updateApiKey', + 'data-test-subj': 'deleteRule', onClick: () => { setIsPopoverOpen(!isPopoverOpen); - onUpdateAPIKey([item.id]); + setRulesToDelete([item.id]); }, - name: 'Update APIKey', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.deleteRuleTitle', + { defaultMessage: 'Delete rule' } + ), }, ], }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 0527695f26011..95e2922a9763e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -334,7 +334,7 @@ describe('rules_list', () => { jest.clearAllMocks(); }); - it('Updates the apiKey successfully', async () => { + it('Updates the Api Key successfully', async () => { updateAPIKey.mockResolvedValueOnce(204); render( @@ -346,28 +346,27 @@ describe('rules_list', () => { fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update APIKey')); - expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API Key')); + expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); fireEvent.click(screen.getByText('Cancel')); - expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); + expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update APIKey')); - expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API Key')); await act(async () => { - fireEvent.click(screen.getByText('OK')); + fireEvent.click(screen.getByText('Update')); }); expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); - expect(addSuccess).toHaveBeenCalledWith('Updated'); + expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); + expect(addSuccess).toHaveBeenCalledWith('API Key has been updated'); }); - it('Update apiKey fails', async () => { + it('Update Api Key fails', async () => { updateAPIKey.mockRejectedValueOnce(500); render( @@ -380,16 +379,16 @@ describe('rules_list', () => { fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update APIKey')); - expect(screen.getByText('Confirm Update')).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API Key')); + expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); await act(async () => { - fireEvent.click(screen.getByText('OK')); + fireEvent.click(screen.getByText('Update')); }); expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText('Confirm Update')).not.toBeInTheDocument(); - expect(addError).toHaveBeenCalledWith(500, { title: 'Failed' }); + expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); + expect(addError).toHaveBeenCalledWith(500, { title: 'Failed to update the API Key' }); }); }); From 7c5cbd9e47f4498bab0ec206db50ee8ae673609b Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 9 May 2022 14:52:58 +0200 Subject: [PATCH 05/18] Add delete and Update API Key buttons to rule details page --- .../components/rule_actions_popopver.scss | 9 + .../components/rule_actions_popover.tsx | 98 ++ .../components/rule_details.test.tsx | 1443 +++++++++-------- .../rule_details/components/rule_details.tsx | 131 +- .../collapsed_item_actions.test.tsx | 2 +- 5 files changed, 965 insertions(+), 718 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss new file mode 100644 index 0000000000000..ac64ece21f73b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss @@ -0,0 +1,9 @@ +.ruleActionsPopover { + .euiContextMenuItem:hover { + text-decoration: none; + } +} + +button[data-test-subj='deleteRuleButton'] { + color: $euiColorDanger; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx new file mode 100644 index 0000000000000..7aba7888df05b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx @@ -0,0 +1,98 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; +import './rule_actions_popopver.scss'; +import { Rule } from '../../../..'; + +export interface RuleActionsPopoverProps { + rule: Rule; + onRefresh: () => void; + onDelete: (ruleId: string) => void; + onApiKeyUpdate: (ruleId: string) => void; +} + +export const RuleActionsPopover: React.FunctionComponent = ({ + rule, + onRefresh, + onDelete, + onApiKeyUpdate, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(!isPopoverOpen)} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.popoverButtonTitle', + { defaultMessage: 'Actions' } + )} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + ownFocus + panelPaddingSize="none" + > + { + setIsPopoverOpen(false); + onRefresh(); + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.refreshRulesButtonLabel', + { defaultMessage: 'Refresh' } + ), + }, + { + disabled: false, + 'data-test-subj': 'updateAPIKeyButton', + onClick: () => { + setIsPopoverOpen(false); + onApiKeyUpdate(rule.id); + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.updateAPIKeyButtonLabel', + { defaultMessage: 'Update API Key' } + ), + }, + { + disabled: false, + 'data-test-subj': 'deleteRuleButton', + onClick: () => { + onDelete(rule.id); + setIsPopoverOpen(false); + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel', + { defaultMessage: 'Delete rule' } + ), + }, + ], + }, + ]} + className="ruleActionsPopover" + data-test-subj="ruleActionsPopover" + data-testid="ruleActionsPopover" + /> + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index fe17dde8c1282..54fbe7f7ab78a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -44,6 +44,12 @@ jest.mock('../../../lib/action_connector_api', () => ({ loadAllActions: jest.fn().mockResolvedValue([]), })); +jest.mock('../../../lib/rule_api', () => ({ + updateAPIKey: jest.fn(), + deleteRules: jest.fn(), +})); +const { updateAPIKey, deleteRules } = jest.requireMock('../../../lib/rule_api'); + jest.mock('../../../lib/capabilities', () => ({ hasAllPrivilege: jest.fn(() => true), hasSaveRulesCapability: jest.fn(() => true), @@ -82,199 +88,205 @@ const ruleType: RuleType = { }; describe('rule_details', () => { - it('renders the rule name as a title', () => { - const rule = mockRule(); - expect( - shallow( - - ).find('EuiPageHeader') - ).toBeTruthy(); - }); + describe('page', () => { + it('renders the rule name as a title', () => { + const rule = mockRule(); + expect( + shallow( + + ).find('EuiPageHeader') + ).toBeTruthy(); + }); - it('renders the rule type badge', () => { - const rule = mockRule(); - expect( - shallow( - - ).find({ruleType.name}) - ).toBeTruthy(); - }); + it('renders the rule type badge', () => { + const rule = mockRule(); + expect( + shallow( + + ).find({ruleType.name}) + ).toBeTruthy(); + }); - it('renders the rule error banner with error message, when rule has a license error', () => { - const rule = mockRule({ - enabled: true, - executionStatus: { - status: 'error', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - error: { - reason: RuleExecutionStatusErrorReasons.License, - message: 'test', + it('renders the rule error banner with error message, when rule has a license error', () => { + const rule = mockRule({ + enabled: true, + executionStatus: { + status: 'error', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + error: { + reason: RuleExecutionStatusErrorReasons.License, + message: 'test', + }, }, - }, + }); + const wrapper = shallow( + + ); + expect( + wrapper.find('[data-test-subj="ruleErrorBanner"]').first().text() + ).toMatchInlineSnapshot(`" Cannot run rule, test "`); }); - const wrapper = shallow( - - ); - expect(wrapper.find('[data-test-subj="ruleErrorBanner"]').first().text()).toMatchInlineSnapshot( - `" Cannot run rule, test "` - ); - }); - it('renders the rule warning banner with warning message, when rule status is a warning', () => { - const rule = mockRule({ - enabled: true, - executionStatus: { - status: 'warning', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - warning: { - reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, - message: 'warning message', + it('renders the rule warning banner with warning message, when rule status is a warning', () => { + const rule = mockRule({ + enabled: true, + executionStatus: { + status: 'warning', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + warning: { + reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS, + message: 'warning message', + }, }, - }, + }); + const wrapper = shallow( + + ); + expect( + wrapper.find('[data-test-subj="ruleWarningBanner"]').first().text() + ).toMatchInlineSnapshot(`" Action limit exceeded warning message"`); }); - const wrapper = shallow( - - ); - expect( - wrapper.find('[data-test-subj="ruleWarningBanner"]').first().text() - ).toMatchInlineSnapshot(`" Action limit exceeded warning message"`); - }); - it('displays a toast message when interval is less than configured minimum', async () => { - const rule = mockRule({ - schedule: { - interval: '1s', - }, - }); - const wrapper = mountWithIntl( - - ); + it('displays a toast message when interval is less than configured minimum', async () => { + const rule = mockRule({ + schedule: { + interval: '1s', + }, + }); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - await act(async () => { - await nextTick(); - wrapper.update(); + expect(useKibanaMock().services.notifications.toasts.addInfo).toHaveBeenCalled(); }); - expect(useKibanaMock().services.notifications.toasts.addInfo).toHaveBeenCalled(); - }); + describe('actions', () => { + it('renders an rule action', () => { + const rule = mockRule({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); - describe('actions', () => { - it('renders an rule action', () => { - const rule = mockRule({ - actions: [ + const actionTypes: ActionType[] = [ { - group: 'default', - id: uuid.v4(), - params: {}, - actionTypeId: '.server-log', + id: '.server-log', + name: 'Server log', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', }, - ], + ]; + + expect( + mountWithIntl( + + ).containsMatchingElement( + + {actionTypes[0].name} + + ) + ).toBeTruthy(); }); - const actionTypes: ActionType[] = [ - { - id: '.server-log', - name: 'Server log', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - ]; + it('renders a counter for multiple rule action', () => { + const rule = mockRule({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.email', + }, + ], + }); + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + { + id: '.email', + name: 'Send email', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; - expect( - mountWithIntl( + const details = mountWithIntl( - ).containsMatchingElement( - - {actionTypes[0].name} - - ) - ).toBeTruthy(); + ); + + expect( + details.containsMatchingElement( + + {actionTypes[0].name} + + ) + ).toBeTruthy(); + + expect( + details.containsMatchingElement( + + {actionTypes[1].name} + + ) + ).toBeTruthy(); + }); }); - it('renders a counter for multiple rule action', () => { - const rule = mockRule({ - actions: [ - { - group: 'default', - id: uuid.v4(), - params: {}, - actionTypeId: '.server-log', - }, - { - group: 'default', - id: uuid.v4(), - params: {}, - actionTypeId: '.email', - }, - ], + describe('links', () => { + it('links to the app that created the rule', () => { + const rule = mockRule(); + expect( + shallow( + + ).find('ViewInApp') + ).toBeTruthy(); }); - const actionTypes: ActionType[] = [ - { - id: '.server-log', - name: 'Server log', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - { - id: '.email', - name: 'Send email', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - ]; - - const details = mountWithIntl( - - ); - - expect( - details.containsMatchingElement( - - {actionTypes[0].name} - - ) - ).toBeTruthy(); - - expect( - details.containsMatchingElement( - - {actionTypes[1].name} - - ) - ).toBeTruthy(); - }); - }); - describe('links', () => { - it('links to the app that created the rule', () => { - const rule = mockRule(); - expect( - shallow( + it('links to the Edit flyout', () => { + const rule = mockRule(); + const pageHeaderProps = shallow( - ).find('ViewInApp') - ).toBeTruthy(); - }); - - it('links to the Edit flyout', () => { - const rule = mockRule(); - const pageHeaderProps = shallow( - - ) - .find('EuiPageHeader') - .props() as EuiPageHeaderProps; - const rightSideItems = pageHeaderProps.rightSideItems; - expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + ) + .find('EuiPageHeader') + .props() as EuiPageHeaderProps; + const rightSideItems = pageHeaderProps.rightSideItems; + expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` { `); + }); }); }); -}); -describe('disable/enable functionality', () => { - it('should show that the rule is enabled', () => { - const rule = mockRule({ - enabled: true, + describe('disable/enable functionality', () => { + it('should show that the rule is enabled', () => { + const rule = mockRule({ + enabled: true, + }); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first(); + + expect(actionsElem.text()).toEqual('Enabled'); }); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first(); - expect(actionsElem.text()).toEqual('Enabled'); - }); + it('should show that the rule is disabled', async () => { + const rule = mockRule({ + enabled: false, + }); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first(); - it('should show that the rule is disabled', async () => { - const rule = mockRule({ - enabled: false, + expect(actionsElem.text()).toEqual('Disabled'); }); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper.find('[data-test-subj="statusDropdown"]').first(); - expect(actionsElem.text()).toEqual('Disabled'); - }); + it('should disable the rule when picking disable in the dropdown', async () => { + const rule = mockRule({ + enabled: true, + }); + const disableRule = jest.fn(); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper + .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') + .first(); + actionsElem.simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - it('should disable the rule when picking disable in the dropdown', async () => { - const rule = mockRule({ - enabled: true, - }); - const disableRule = jest.fn(); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + await act(async () => { + const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); + const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); + actionsMenuItemElem.at(1).simulate('click'); + await nextTick(); + }); - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - await nextTick(); + expect(disableRule).toHaveBeenCalledTimes(1); }); - expect(disableRule).toHaveBeenCalledTimes(1); - }); + it('if rule is already disable should do nothing when picking disable in the dropdown', async () => { + const rule = mockRule({ + enabled: false, + }); + const disableRule = jest.fn(); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper + .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') + .first(); + actionsElem.simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - it('if rule is already disable should do nothing when picking disable in the dropdown', async () => { - const rule = mockRule({ - enabled: false, - }); - const disableRule = jest.fn(); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + await act(async () => { + const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); + const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); + actionsMenuItemElem.at(1).simulate('click'); + await nextTick(); + }); - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - await nextTick(); + expect(disableRule).toHaveBeenCalledTimes(0); }); - expect(disableRule).toHaveBeenCalledTimes(0); - }); + it('should enable the rule when picking enable in the dropdown', async () => { + const rule = mockRule({ + enabled: false, + }); + const enableRule = jest.fn(); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper + .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') + .first(); + actionsElem.simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - it('should enable the rule when picking enable in the dropdown', async () => { - const rule = mockRule({ - enabled: false, - }); - const enableRule = jest.fn(); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + await act(async () => { + const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); + const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); + actionsMenuItemElem.at(0).simulate('click'); + await nextTick(); + }); - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); - actionsMenuItemElem.at(0).simulate('click'); - await nextTick(); + expect(enableRule).toHaveBeenCalledTimes(1); }); - expect(enableRule).toHaveBeenCalledTimes(1); - }); + it('if rule is already enable should do nothing when picking enable in the dropdown', async () => { + const rule = mockRule({ + enabled: true, + }); + const enableRule = jest.fn(); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper + .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') + .first(); + actionsElem.simulate('click'); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); - it('if rule is already enable should do nothing when picking enable in the dropdown', async () => { - const rule = mockRule({ - enabled: true, - }); - const enableRule = jest.fn(); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + await act(async () => { + const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); + const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); + actionsMenuItemElem.at(0).simulate('click'); + await nextTick(); + }); - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); - actionsMenuItemElem.at(0).simulate('click'); - await nextTick(); + expect(enableRule).toHaveBeenCalledTimes(0); }); - expect(enableRule).toHaveBeenCalledTimes(0); - }); + it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => { + const rule = mockRule({ + enabled: true, + }); - it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => { - const rule = mockRule({ - enabled: true, - }); + const disableRule = jest.fn(async () => { + await new Promise((resolve) => setTimeout(resolve, 6000)); + }); + const enableRule = jest.fn(); + const wrapper = mountWithIntl( + + ); - const disableRule = jest.fn(async () => { - await new Promise((resolve) => setTimeout(resolve, 6000)); - }); - const enableRule = jest.fn(); - const wrapper = mountWithIntl( - - ); - - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - actionsElem.simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + const actionsElem = wrapper + .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') + .first(); + actionsElem.simulate('click'); - await act(async () => { - const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); - const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); - actionsMenuItemElem.at(1).simulate('click'); - }); + await act(async () => { + await nextTick(); + wrapper.update(); + }); - await act(async () => { - await nextTick(); - wrapper.update(); - }); + await act(async () => { + const actionsMenuElem = wrapper.find('[data-test-subj="ruleStatusMenu"]'); + const actionsMenuItemElem = actionsMenuElem.first().find('.euiContextMenuItem'); + actionsMenuItemElem.at(1).simulate('click'); + }); - await act(async () => { - expect(disableRule).toHaveBeenCalled(); - expect( - wrapper.find('[data-test-subj="statusDropdown"] .euiBadge__childButton .euiLoadingSpinner') - .length - ).toBeGreaterThan(0); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await act(async () => { + expect(disableRule).toHaveBeenCalled(); + expect( + wrapper.find( + '[data-test-subj="statusDropdown"] .euiBadge__childButton .euiLoadingSpinner' + ).length + ).toBeGreaterThan(0); + }); }); }); -}); -describe('snooze functionality', () => { - it('should render "Snooze Indefinitely" when rule is enabled and mute all', () => { - const rule = mockRule({ - enabled: true, - muteAll: true, + describe('snooze functionality', () => { + it('should render "Snooze Indefinitely" when rule is enabled and mute all', () => { + const rule = mockRule({ + enabled: true, + muteAll: true, + }); + const wrapper = mountWithIntl( + + ); + const actionsElem = wrapper + .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') + .first(); + expect(actionsElem.text()).toEqual('Snoozed'); + expect(wrapper.find('[data-test-subj="remainingSnoozeTime"]').first().text()).toEqual( + 'Indefinitely' + ); }); - const wrapper = mountWithIntl( - - ); - const actionsElem = wrapper - .find('[data-test-subj="statusDropdown"] .euiBadge__childButton') - .first(); - expect(actionsElem.text()).toEqual('Snoozed'); - expect(wrapper.find('[data-test-subj="remainingSnoozeTime"]').first().text()).toEqual( - 'Indefinitely' - ); }); -}); -describe('edit button', () => { - const actionTypes: ActionType[] = [ - { - id: '.server-log', - name: 'Server log', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - ]; - ruleTypeRegistry.has.mockReturnValue(true); - const ruleTypeR: RuleTypeModel = { - id: 'my-rule-type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: () => { - return { errors: {} }; - }, - ruleParamsExpression: jest.fn(), - requiresAppContext: false, - }; - ruleTypeRegistry.get.mockReturnValue(ruleTypeR); - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - - it('should render an edit button when rule and actions are editable', () => { - const rule = mockRule({ - enabled: true, - muteAll: false, - actions: [ - { - group: 'default', - id: uuid.v4(), - params: {}, - actionTypeId: '.server-log', - }, - ], - }); - const pageHeaderProps = shallow( - - ) - .find('EuiPageHeader') - .props() as EuiPageHeaderProps; - const rightSideItems = pageHeaderProps.rightSideItems; - expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + describe('edit button', () => { + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; + ruleTypeRegistry.has.mockReturnValue(true); + const ruleTypeR: RuleTypeModel = { + id: 'my-rule-type', + iconClass: 'test', + description: 'Rule when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: jest.fn(), + requiresAppContext: false, + }; + ruleTypeRegistry.get.mockReturnValue(ruleTypeR); + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + it('should render an edit button when rule and actions are editable', () => { + const rule = mockRule({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const pageHeaderProps = shallow( + + ) + .find('EuiPageHeader') + .props() as EuiPageHeaderProps; + const rightSideItems = pageHeaderProps.rightSideItems; + expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` { `); - }); + }); - it('should not render an edit button when rule editable but actions arent', () => { - const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); - hasExecuteActionsCapability.mockReturnValueOnce(false); - const rule = mockRule({ - enabled: true, - muteAll: false, - actions: [ - { - group: 'default', - id: uuid.v4(), - params: {}, - actionTypeId: '.server-log', - }, - ], + it('should not render an edit button when rule editable but actions arent', () => { + const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); + hasExecuteActionsCapability.mockReturnValueOnce(false); + const rule = mockRule({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + expect( + shallow( + + ) + .find(EuiButtonEmpty) + .find('[name="edit"]') + .first() + .exists() + ).toBeFalsy(); }); - expect( - shallow( + + it('should render an edit button when rule editable but actions arent when there are no actions on the rule', async () => { + const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); + hasExecuteActionsCapability.mockReturnValueOnce(false); + const rule = mockRule({ + enabled: true, + muteAll: false, + actions: [], + }); + const pageHeaderProps = shallow( ) - .find(EuiButtonEmpty) - .find('[name="edit"]') - .first() - .exists() - ).toBeFalsy(); - }); - - it('should render an edit button when rule editable but actions arent when there are no actions on the rule', async () => { - const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); - hasExecuteActionsCapability.mockReturnValueOnce(false); - const rule = mockRule({ - enabled: true, - muteAll: false, - actions: [], - }); - const pageHeaderProps = shallow( - - ) - .find('EuiPageHeader') - .props() as EuiPageHeaderProps; - const rightSideItems = pageHeaderProps.rightSideItems; - expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + .find('EuiPageHeader') + .props() as EuiPageHeaderProps; + const rightSideItems = pageHeaderProps.rightSideItems; + expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` { `); + }); }); -}); -describe('broken connector indicator', () => { - const actionTypes: ActionType[] = [ - { - id: '.server-log', - name: 'Server log', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - ]; - ruleTypeRegistry.has.mockReturnValue(true); - const ruleTypeR: RuleTypeModel = { - id: 'my-rule-type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: () => { - return { errors: {} }; - }, - ruleParamsExpression: jest.fn(), - requiresAppContext: false, - }; - ruleTypeRegistry.get.mockReturnValue(ruleTypeR); - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - const { loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); - loadAllActions.mockResolvedValue([ - { - secrets: {}, - isMissingSecrets: false, - id: 'connector-id-1', - actionTypeId: '.server-log', - name: 'Test connector', - config: {}, - isPreconfigured: false, - isDeprecated: false, - }, - { - secrets: {}, - isMissingSecrets: false, - id: 'connector-id-2', - actionTypeId: '.server-log', - name: 'Test connector 2', - config: {}, - isPreconfigured: false, - isDeprecated: false, - }, - ]); - - it('should not render broken connector indicator or warning if all rule actions connectors exist', async () => { - const rule = mockRule({ - enabled: true, - muteAll: false, - actions: [ - { - group: 'default', - id: 'connector-id-1', - params: {}, - actionTypeId: '.server-log', - }, - { - group: 'default', - id: 'connector-id-2', - params: {}, - actionTypeId: '.server-log', - }, - ], + describe('broken connector indicator', () => { + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; + ruleTypeRegistry.has.mockReturnValue(true); + const ruleTypeR: RuleTypeModel = { + id: 'my-rule-type', + iconClass: 'test', + description: 'Rule when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: jest.fn(), + requiresAppContext: false, + }; + ruleTypeRegistry.get.mockReturnValue(ruleTypeR); + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + const { loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); + loadAllActions.mockResolvedValue([ + { + secrets: {}, + isMissingSecrets: false, + id: 'connector-id-1', + actionTypeId: '.server-log', + name: 'Test connector', + config: {}, + isPreconfigured: false, + isDeprecated: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'connector-id-2', + actionTypeId: '.server-log', + name: 'Test connector 2', + config: {}, + isPreconfigured: false, + isDeprecated: false, + }, + ]); + + it('should not render broken connector indicator or warning if all rule actions connectors exist', async () => { + const rule = mockRule({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: 'connector-id-1', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-2', + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const brokenConnectorIndicator = wrapper + .find('[data-test-subj="actionWithBrokenConnector"]') + .first(); + const brokenConnectorWarningBanner = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') + .first(); + expect(brokenConnectorIndicator.exists()).toBeFalsy(); + expect(brokenConnectorWarningBanner.exists()).toBeFalsy(); }); - const wrapper = mountWithIntl( - - ); - await act(async () => { - await nextTick(); - wrapper.update(); + + it('should render broken connector indicator and warning if any rule actions connector does not exist', async () => { + const rule = mockRule({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: 'connector-id-1', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-2', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-doesnt-exist', + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const brokenConnectorIndicator = wrapper + .find('[data-test-subj="actionWithBrokenConnector"]') + .first(); + const brokenConnectorWarningBanner = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') + .first(); + const brokenConnectorWarningBannerAction = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]') + .first(); + expect(brokenConnectorIndicator.exists()).toBeTruthy(); + expect(brokenConnectorWarningBanner.exists()).toBeTruthy(); + expect(brokenConnectorWarningBannerAction.exists()).toBeTruthy(); + }); + + it('should render broken connector indicator and warning with no edit button if any rule actions connector does not exist and user has no edit access', async () => { + const rule = mockRule({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: 'connector-id-1', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-2', + params: {}, + actionTypeId: '.server-log', + }, + { + group: 'default', + id: 'connector-id-doesnt-exist', + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); + hasExecuteActionsCapability.mockReturnValue(false); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const brokenConnectorIndicator = wrapper + .find('[data-test-subj="actionWithBrokenConnector"]') + .first(); + const brokenConnectorWarningBanner = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') + .first(); + const brokenConnectorWarningBannerAction = wrapper + .find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]') + .first(); + expect(brokenConnectorIndicator.exists()).toBeTruthy(); + expect(brokenConnectorWarningBanner.exists()).toBeTruthy(); + expect(brokenConnectorWarningBannerAction.exists()).toBeFalsy(); }); - const brokenConnectorIndicator = wrapper - .find('[data-test-subj="actionWithBrokenConnector"]') - .first(); - const brokenConnectorWarningBanner = wrapper - .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') - .first(); - expect(brokenConnectorIndicator.exists()).toBeFalsy(); - expect(brokenConnectorWarningBanner.exists()).toBeFalsy(); }); - it('should render broken connector indicator and warning if any rule actions connector does not exist', async () => { - const rule = mockRule({ - enabled: true, - muteAll: false, - actions: [ - { - group: 'default', - id: 'connector-id-1', - params: {}, - actionTypeId: '.server-log', - }, - { - group: 'default', - id: 'connector-id-2', - params: {}, - actionTypeId: '.server-log', - }, - { - group: 'default', - id: 'connector-id-doesnt-exist', - params: {}, - actionTypeId: '.server-log', - }, - ], - }); - const wrapper = mountWithIntl( - - ); - await act(async () => { - await nextTick(); - wrapper.update(); + describe('refresh button', () => { + it('should call requestRefresh when clicked', async () => { + const rule = mockRule(); + const requestRefresh = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const actionsButton = wrapper.find('[data-test-subj="ruleActionsButton"]').first(); + actionsButton.simulate('click'); + + const refreshButton = wrapper.find('[data-test-subj="refreshRulesButton"]').first(); + expect(refreshButton.exists()).toBeTruthy(); + + refreshButton.simulate('click'); + expect(requestRefresh).toHaveBeenCalledTimes(1); }); - const brokenConnectorIndicator = wrapper - .find('[data-test-subj="actionWithBrokenConnector"]') - .first(); - const brokenConnectorWarningBanner = wrapper - .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') - .first(); - const brokenConnectorWarningBannerAction = wrapper - .find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]') - .first(); - expect(brokenConnectorIndicator.exists()).toBeTruthy(); - expect(brokenConnectorWarningBanner.exists()).toBeTruthy(); - expect(brokenConnectorWarningBannerAction.exists()).toBeTruthy(); }); - it('should render broken connector indicator and warning with no edit button if any rule actions connector does not exist and user has no edit access', async () => { - const rule = mockRule({ - enabled: true, - muteAll: false, - actions: [ - { - group: 'default', - id: 'connector-id-1', - params: {}, - actionTypeId: '.server-log', - }, - { - group: 'default', - id: 'connector-id-2', - params: {}, - actionTypeId: '.server-log', - }, - { - group: 'default', - id: 'connector-id-doesnt-exist', - params: {}, - actionTypeId: '.server-log', - }, - ], - }); - const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); - hasExecuteActionsCapability.mockReturnValue(false); - const wrapper = mountWithIntl( - - ); - await act(async () => { - await nextTick(); - wrapper.update(); + describe('update API Key button', () => { + it('should call update api key when clicked', async () => { + const rule = mockRule(); + const requestRefresh = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const actionsButton = wrapper.find('[data-test-subj="ruleActionsButton"]').first(); + actionsButton.simulate('click'); + + const updateButton = wrapper.find('[data-test-subj="updateAPIKeyButton"]').first(); + expect(updateButton.exists()).toBeTruthy(); + + updateButton.simulate('click'); + + const confirm = wrapper.find('[data-test-subj="updateApiKeyIdsConfirmation"]').first(); + expect(confirm.exists()).toBeTruthy(); + + const confirmButton = wrapper.find('[data-test-subj="confirmModalConfirmButton"]').first(); + expect(confirmButton.exists()).toBeTruthy(); + + confirmButton.simulate('click'); + + expect(updateAPIKey).toHaveBeenCalledTimes(1); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: rule.id })); }); - const brokenConnectorIndicator = wrapper - .find('[data-test-subj="actionWithBrokenConnector"]') - .first(); - const brokenConnectorWarningBanner = wrapper - .find('[data-test-subj="actionWithBrokenConnectorWarningBanner"]') - .first(); - const brokenConnectorWarningBannerAction = wrapper - .find('[data-test-subj="actionWithBrokenConnectorWarningBannerEdit"]') - .first(); - expect(brokenConnectorIndicator.exists()).toBeTruthy(); - expect(brokenConnectorWarningBanner.exists()).toBeTruthy(); - expect(brokenConnectorWarningBannerAction.exists()).toBeFalsy(); }); -}); -describe('refresh button', () => { - it('should call requestRefresh when clicked', async () => { - const rule = mockRule(); - const requestRefresh = jest.fn(); - const wrapper = mountWithIntl( - - ); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - const refreshButton = wrapper.find('[data-test-subj="refreshRulesButton"]').first(); - expect(refreshButton.exists()).toBeTruthy(); + describe('delete rule button', () => { + it('should delete the rule when clicked', async () => { + deleteRules.mockResolvedValueOnce({ successes: ['1'], errors: [] }); + const rule = mockRule(); + const requestRefresh = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const actionsButton = wrapper.find('[data-test-subj="ruleActionsButton"]').first(); + actionsButton.simulate('click'); + + const updateButton = wrapper.find('[data-test-subj="deleteRuleButton"]').first(); + expect(updateButton.exists()).toBeTruthy(); + + updateButton.simulate('click'); + + const confirm = wrapper.find('[data-test-subj="deleteIdsConfirmation"]').first(); + expect(confirm.exists()).toBeTruthy(); - refreshButton.simulate('click'); - expect(requestRefresh).toHaveBeenCalledTimes(1); + const confirmButton = wrapper.find('[data-test-subj="confirmModalConfirmButton"]').first(); + expect(confirmButton.exists()).toBeTruthy(); + + confirmButton.simulate('click'); + + expect(deleteRules).toHaveBeenCalledTimes(1); + expect(deleteRules).toHaveBeenCalledWith(expect.objectContaining({ ids: [rule.id] })); + }); }); -}); -function mockRule(overloads: Partial = {}): Rule { - return { - id: uuid.v4(), - enabled: true, - name: `rule-${uuid.v4()}`, - tags: [], - ruleTypeId: '.noop', - consumer: ALERTS_FEATURE_ID, - schedule: { interval: '1m' }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - apiKeyOwner: null, - throttle: null, - notifyWhen: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: new Date('2020-08-20T19:23:38Z'), - }, - ...overloads, - }; -} + function mockRule(overloads: Partial = {}): Rule { + return { + id: uuid.v4(), + enabled: true, + name: `rule-${uuid.v4()}`, + tags: [], + ruleTypeId: '.noop', + consumer: ALERTS_FEATURE_ID, + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + ...overloads, + }; + } +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index b3363159851d0..edfeb2be1a1a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -27,6 +27,10 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common'; +import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation'; +import { updateAPIKey, deleteRules } from '../../../lib/rule_api'; +import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; +import { RuleActionsPopover } from './rule_actions_popover'; import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { getAlertingSectionBreadcrumb, getRuleDetailsBreadcrumb } from '../../../lib/breadcrumb'; import { getCurrentDocTitle } from '../../../lib/doc_title'; @@ -45,7 +49,7 @@ import { import { RuleRouteWithApi } from './rule_route'; import { ViewInApp } from './view_in_app'; import { RuleEdit } from '../../rule_form'; -import { routeToRuleDetails } from '../../../constants'; +import { routeToRuleDetails, routeToRules } from '../../../constants'; import { rulesErrorReasonTranslationsMapping, rulesWarningReasonTranslationsMapping, @@ -90,6 +94,9 @@ export const RuleDetails: React.FunctionComponent = ({ dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } }); }; + const [rulesToDelete, setRulesToDelete] = useState([]); + const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState([]); + const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState(false); @@ -203,6 +210,10 @@ export const RuleDetails: React.FunctionComponent = ({ history.push(routeToRuleDetails.replace(`:ruleId`, rule.id)); }; + const goToRulesList = () => { + history.push(routeToRules); + }; + const getRuleStatusErrorReasonText = () => { if (rule.executionStatus.error && rule.executionStatus.error.reason) { return rulesErrorReasonTranslationsMapping[rule.executionStatus.error.reason]; @@ -219,40 +230,71 @@ export const RuleDetails: React.FunctionComponent = ({ } }; - const rightPageHeaderButtons = hasEditButton - ? [ - <> - setEditFlyoutVisibility(true)} - name="edit" - disabled={!ruleType.enabledInLicense} - > - - - {editFlyoutVisible && ( - { - setInitialRule(rule); - setEditFlyoutVisibility(false); - }} - actionTypeRegistry={actionTypeRegistry} - ruleTypeRegistry={ruleTypeRegistry} - ruleType={ruleType} - onSave={setRule} - /> - )} - , - ] - : []; + const rightPageHeaderButtons = hasEditButton ? ( + <> + setEditFlyoutVisibility(true)} + name="edit" + disabled={!ruleType.enabledInLicense} + > + + + {editFlyoutVisible && ( + { + setInitialRule(rule); + setEditFlyoutVisibility(false); + }} + actionTypeRegistry={actionTypeRegistry} + ruleTypeRegistry={ruleTypeRegistry} + ruleType={ruleType} + onSave={setRule} + /> + )} + + ) : null; return ( <> + { + setRulesToDelete([]); + goToRulesList(); + }} + onErrors={async () => { + // Refresh the rule from the server, it may have been deleted + await setRule(); + setRulesToDelete([]); + }} + onCancel={() => { + setRulesToDelete([]); + }} + apiDeleteCall={deleteRules} + idsToDelete={rulesToDelete} + singleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.singleTitle', { + defaultMessage: 'rule', + })} + multipleTitle="" + setIsLoadingState={() => {}} + /> + { + setRulesToUpdateAPIKey([]); + }} + idsToUpdate={rulesToUpdateAPIKey} + apiUpdateApiKeyCall={updateAPIKey} + setIsLoadingState={() => {}} + onUpdated={async () => { + setRulesToUpdateAPIKey([]); + setRule(); + }} + /> = ({ } rightSideItems={[ , - - - , - ...rightPageHeaderButtons, + { + setRulesToDelete([ruleId]); + }} + onApiKeyUpdate={(ruleId) => { + setRulesToUpdateAPIKey([ruleId]); + }} + />, + rightPageHeaderButtons, ]} /> @@ -397,7 +437,6 @@ export const RuleDetails: React.FunctionComponent = ({ ) : null} - {rule.enabled && rule.executionStatus.status === 'warning' ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx index 53938b7da370b..4169de3f4b889 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx @@ -147,7 +147,7 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); - expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update APIKey'); + expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update API Key'); }); test('handles case when rule is unmuted and enabled and mute is clicked', async () => { From 267abf4e5950c33b4010959d5ab909dc993396ee Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 9 May 2022 17:41:28 +0200 Subject: [PATCH 06/18] Remove Refresh button from the dropdown --- .../components/rule_actions_popover.test.tsx | 144 ++++++++++++++++++ .../components/rule_actions_popover.tsx | 23 +-- .../components/rule_details.test.tsx | 8 +- .../rule_details/components/rule_details.tsx | 20 ++- 4 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx new file mode 100644 index 0000000000000..5f88ba43184eb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import * as React from 'react'; +import { RuleActionsPopover } from './rule_actions_popover'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { Rule } from '../../../..'; +import { afterEach } from 'jest-circus'; + +describe('rule_actions_popover', () => { + const onDeleteMock = jest.fn(); + const onApiKeyUpdateMock = jest.fn(); + + function mockRule(overloads: Partial = {}): Rule { + return { + id: '12345', + enabled: true, + name: `rule-12345`, + tags: [], + ruleTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + ...overloads, + }; + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders all the buttons', () => { + const rule = mockRule(); + render( + + + + ); + + const actionButton = screen.getByTestId('ruleActionsButton'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + expect(screen.getByText('Update API Key')).toBeInTheDocument(); + expect(screen.getByText('Delete rule')).toBeInTheDocument(); + }); + + it('calls onDelete', async () => { + const rule = mockRule(); + render( + + + + ); + + const actionButton = screen.getByTestId('ruleActionsButton'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + + const deleteButton = screen.getByText('Delete rule'); + expect(deleteButton).toBeInTheDocument(); + fireEvent.click(deleteButton); + + expect(onDeleteMock).toHaveBeenCalledWith('12345'); + await waitFor(() => { + expect(screen.queryByText('Delete rule')).not.toBeInTheDocument(); + }); + }); + + it('calls onApiKeyUpdate', async () => { + const rule = mockRule(); + render( + + + + ); + + const actionButton = screen.getByTestId('ruleActionsButton'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + + const deleteButton = screen.getByText('Update API Key'); + expect(deleteButton).toBeInTheDocument(); + fireEvent.click(deleteButton); + + expect(onApiKeyUpdateMock).toHaveBeenCalledWith('12345'); + await waitFor(() => { + expect(screen.queryByText('Update API Key')).not.toBeInTheDocument(); + }); + }); + + it('disables buttons when the user does not have enough permission', async () => { + const rule = mockRule(); + render( + + + + ); + + const actionButton = screen.getByTestId('ruleActionsButton'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + + expect(screen.getByText('Delete rule').closest('button')).toBeDisabled(); + expect(screen.getByText('Update API Key').closest('button')).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx index 7aba7888df05b..fc3c073a42325 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx @@ -12,14 +12,14 @@ import { Rule } from '../../../..'; export interface RuleActionsPopoverProps { rule: Rule; - onRefresh: () => void; + canSaveRule: boolean; onDelete: (ruleId: string) => void; onApiKeyUpdate: (ruleId: string) => void; } export const RuleActionsPopover: React.FunctionComponent = ({ rule, - onRefresh, + canSaveRule, onDelete, onApiKeyUpdate, }) => { @@ -31,6 +31,7 @@ export const RuleActionsPopover: React.FunctionComponent setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( @@ -51,19 +52,7 @@ export const RuleActionsPopover: React.FunctionComponent { - setIsPopoverOpen(false); - onRefresh(); - }, - name: i18n.translate( - 'xpack.triggersActionsUI.sections.ruleDetails.refreshRulesButtonLabel', - { defaultMessage: 'Refresh' } - ), - }, - { - disabled: false, + disabled: !canSaveRule, 'data-test-subj': 'updateAPIKeyButton', onClick: () => { setIsPopoverOpen(false); @@ -75,11 +64,11 @@ export const RuleActionsPopover: React.FunctionComponent { - onDelete(rule.id); setIsPopoverOpen(false); + onDelete(rule.id); }, name: i18n.translate( 'xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index 54fbe7f7ab78a..780d875cf4ea4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -286,7 +286,7 @@ describe('rule_details', () => { .find('EuiPageHeader') .props() as EuiPageHeaderProps; const rightSideItems = pageHeaderProps.rightSideItems; - expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + expect(!!rightSideItems && rightSideItems[3]!).toMatchInlineSnapshot(` { .find('EuiPageHeader') .props() as EuiPageHeaderProps; const rightSideItems = pageHeaderProps.rightSideItems; - expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + expect(!!rightSideItems && rightSideItems[3]!).toMatchInlineSnapshot(` { .find('EuiPageHeader') .props() as EuiPageHeaderProps; const rightSideItems = pageHeaderProps.rightSideItems; - expect(!!rightSideItems && rightSideItems[2]!).toMatchInlineSnapshot(` + expect(!!rightSideItems && rightSideItems[3]!).toMatchInlineSnapshot(` { await nextTick(); wrapper.update(); }); - const actionsButton = wrapper.find('[data-test-subj="ruleActionsButton"]').first(); - actionsButton.simulate('click'); const refreshButton = wrapper.find('[data-test-subj="refreshRulesButton"]').first(); expect(refreshButton.exists()).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index edfeb2be1a1a4..9ffd6d38f94d8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -230,7 +230,7 @@ export const RuleDetails: React.FunctionComponent = ({ } }; - const rightPageHeaderButtons = hasEditButton ? ( + const editButton = hasEditButton ? ( <> = ({ } rightSideItems={[ - , { setRulesToDelete([ruleId]); }} @@ -406,7 +405,20 @@ export const RuleDetails: React.FunctionComponent = ({ setRulesToUpdateAPIKey([ruleId]); }} />, - rightPageHeaderButtons, + editButton, + + + , + , ]} /> From 94b7a3aa825d45507f48ff6ea7bf80d7e52f3f0c Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 9 May 2022 18:05:41 +0200 Subject: [PATCH 07/18] revert rules list --- .../rules_list/components/rules_list.test.tsx | 1609 ++++++++--------- 1 file changed, 799 insertions(+), 810 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 95e2922a9763e..e1805841e76dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -305,920 +305,909 @@ const mockedRulesData = [ }, ]; -describe('rules_list', () => { - beforeEach(() => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); - }); +beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); +}); - describe('Update Api Key', () => { - const addSuccess = jest.fn(); - const addError = jest.fn(); +describe('Update Api Key', () => { + const addSuccess = jest.fn(); + const addError = jest.fn(); - beforeAll(() => { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: mockedRulesData, - }); - loadActionTypes.mockResolvedValue([]); - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - useKibanaMock().services.notifications.toasts = { - addSuccess, - addError, - } as unknown as IToasts; + beforeAll(() => { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: mockedRulesData, }); + loadActionTypes.mockResolvedValue([]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + useKibanaMock().services.notifications.toasts = { + addSuccess, + addError, + } as unknown as IToasts; + }); - afterEach(() => { - jest.clearAllMocks(); - }); + afterEach(() => { + jest.clearAllMocks(); + }); - it('Updates the Api Key successfully', async () => { - updateAPIKey.mockResolvedValueOnce(204); - render( - - - - ); - expect(await screen.findByText('test rule ok')).toBeInTheDocument(); + it('Updates the Api Key successfully', async () => { + updateAPIKey.mockResolvedValueOnce(204); + render( + + + + ); + expect(await screen.findByText('test rule ok')).toBeInTheDocument(); - fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); - expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update API Key')); - expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API Key')); + expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); - fireEvent.click(screen.getByText('Cancel')); - expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); + fireEvent.click(screen.getByText('Cancel')); + expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); - fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); - expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update API Key')); + fireEvent.click(screen.getByText('Update API Key')); - await act(async () => { - fireEvent.click(screen.getByText('Update')); - }); - expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); - expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); - expect(addSuccess).toHaveBeenCalledWith('API Key has been updated'); + await act(async () => { + fireEvent.click(screen.getByText('Update')); }); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); + expect(loadRules).toHaveBeenCalledTimes(2); + expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); + expect(addSuccess).toHaveBeenCalledWith('API Key has been updated'); + }); - it('Update Api Key fails', async () => { - updateAPIKey.mockRejectedValueOnce(500); - render( - - - - ); + it('Update Api Key fails', async () => { + updateAPIKey.mockRejectedValueOnce(500); + render( + + + + ); - expect(await screen.findByText('test rule ok')).toBeInTheDocument(); + expect(await screen.findByText('test rule ok')).toBeInTheDocument(); - fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); - expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); + fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); + expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update API Key')); - expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API Key')); + expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); - await act(async () => { - fireEvent.click(screen.getByText('Update')); - }); - expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); - expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); - expect(addError).toHaveBeenCalledWith(500, { title: 'Failed to update the API Key' }); + await act(async () => { + fireEvent.click(screen.getByText('Update')); }); + expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); + expect(loadRules).toHaveBeenCalledTimes(2); + expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); + expect(addError).toHaveBeenCalledWith(500, { title: 'Failed to update the API Key' }); }); +}); - describe('rules_list component empty', () => { - let wrapper: ReactWrapper; - async function setup() { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); +describe('rules_list component empty', () => { + let wrapper: ReactWrapper; + async function setup() { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: [], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + wrapper = mountWithIntl(); - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders empty list', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="createFirstRuleEmptyPrompt"]').exists()).toBeTruthy(); + await act(async () => { + await nextTick(); + wrapper.update(); }); + } - it('renders Create rule button', async () => { - await setup(); - expect( - wrapper.find('[data-test-subj="createFirstRuleButton"]').find('EuiButton') - ).toHaveLength(1); - expect(wrapper.find('RuleAdd').exists()).toBeFalsy(); + it('renders empty list', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="createFirstRuleEmptyPrompt"]').exists()).toBeTruthy(); + }); - wrapper.find('button[data-test-subj="createFirstRuleButton"]').simulate('click'); + it('renders Create rule button', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="createFirstRuleButton"]').find('EuiButton')).toHaveLength( + 1 + ); + expect(wrapper.find('RuleAdd').exists()).toBeFalsy(); - await act(async () => { - // When the RuleAdd component is rendered, it waits for the healthcheck to resolve - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); + wrapper.find('button[data-test-subj="createFirstRuleButton"]').simulate('click'); - await nextTick(); - wrapper.update(); + await act(async () => { + // When the RuleAdd component is rendered, it waits for the healthcheck to resolve + await new Promise((resolve) => { + setTimeout(resolve, 1000); }); - expect(wrapper.find('RuleAdd').exists()).toEqual(true); + await nextTick(); + wrapper.update(); }); + + expect(wrapper.find('RuleAdd').exists()).toEqual(true); }); +}); - describe('rules_list component with items', () => { - let wrapper: ReactWrapper; +describe('rules_list component with items', () => { + let wrapper: ReactWrapper; - async function setup(editable: boolean = true) { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 4, - data: mockedRulesData, - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - loadRuleAggregations.mockResolvedValue({ - ruleEnabledStatus: { enabled: 2, disabled: 0 }, - ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, - ruleMutedStatus: { muted: 0, unmuted: 2 }, - }); + async function setup(editable: boolean = true) { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 4, + data: mockedRulesData, + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + loadRuleAggregations.mockResolvedValue({ + ruleEnabledStatus: { enabled: 2, disabled: 0 }, + ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, + ruleMutedStatus: { muted: 0, unmuted: 2 }, + }); - const ruleTypeMock: RuleTypeModel = { - id: 'test_rule_type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: () => { - return { errors: {} }; - }, - ruleParamsExpression: jest.fn(), - requiresAppContext: !editable, - }; - - ruleTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + const ruleTypeMock: RuleTypeModel = { + id: 'test_rule_type', + iconClass: 'test', + description: 'Rule when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; - expect(loadRules).toHaveBeenCalled(); - expect(loadActionTypes).toHaveBeenCalled(); - expect(loadRuleAggregations).toHaveBeenCalled(); - } - - it('renders table of rules', async () => { - // Use fake timers so we don't have to wait for the EuiToolTip timeout - jest.useFakeTimers(); - await setup(); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(mockedRulesData.length); - - // Name and rule type column - const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-name"]'); - expect(ruleNameColumns.length).toEqual(mockedRulesData.length); - mockedRulesData.forEach((rule, index) => { - expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${ruleTypeFromApi.name}`); - }); + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - // Tags column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-tagsPopover"]').length - ).toEqual(mockedRulesData.length); - // only show tags popover if tags exist on rule - const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagBadge"]'); - expect(tagsBadges.length).toEqual( - mockedRulesData.filter((data) => data.tags.length > 0).length - ); - - // Last run column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastExecutionDate"]').length - ).toEqual(mockedRulesData.length); - - // Last run tooltip - wrapper - .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') - .first() - .simulate('mouseOver'); - - // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + await act(async () => { + await nextTick(); wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.'); - - wrapper - .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') - .first() - .simulate('mouseOut'); - - // Schedule interval column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-interval"]').length - ).toEqual(mockedRulesData.length); + }); - // Schedule interval tooltip - wrapper - .find('[data-test-subj="ruleInterval-config-tooltip-0"]') - .first() - .simulate('mouseOver'); + expect(loadRules).toHaveBeenCalled(); + expect(loadActionTypes).toHaveBeenCalled(); + expect(loadRuleAggregations).toHaveBeenCalled(); + } + + it('renders table of rules', async () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(mockedRulesData.length); + + // Name and rule type column + const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-name"]'); + expect(ruleNameColumns.length).toEqual(mockedRulesData.length); + mockedRulesData.forEach((rule, index) => { + expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${ruleTypeFromApi.name}`); + }); - // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); + // Tags column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-tagsPopover"]').length + ).toEqual(mockedRulesData.length); + // only show tags popover if tags exist on rule + const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagBadge"]'); + expect(tagsBadges.length).toEqual( + mockedRulesData.filter((data) => data.tags.length > 0).length + ); + + // Last run column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastExecutionDate"]').length + ).toEqual(mockedRulesData.length); + + // Last run tooltip + wrapper + .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') + .first() + .simulate('mouseOver'); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('.euiToolTipPopover').text()).toBe('Start time of the last run.'); + + wrapper + .find('[data-test-subj="rulesTableCell-lastExecutionDateTooltip"]') + .first() + .simulate('mouseOut'); + + // Schedule interval column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-interval"]').length + ).toEqual(mockedRulesData.length); + + // Schedule interval tooltip + wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOver'); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('.euiToolTipPopover').text()).toBe( + 'Below configured minimum intervalRule interval of 1 second is below the minimum configured interval of 1 minute. This may impact alerting performance.' + ); + + wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOut'); + + // Duration column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-duration"]').length + ).toEqual(mockedRulesData.length); + // show warning if duration is long + const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); + expect(durationWarningIcon.length).toEqual( + mockedRulesData.filter( + (data) => data.executionStatus.lastDuration > parseDuration(ruleTypeFromApi.ruleTaskTimeout) + ).length + ); + + // Duration tooltip + wrapper.find('[data-test-subj="rulesTableCell-durationTooltip"]').first().simulate('mouseOver'); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('.euiToolTipPopover').text()).toBe( + 'The length of time it took for the rule to run (mm:ss).' + ); + + // Last response column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastResponse"]').length + ).toEqual(mockedRulesData.length); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-warning"]').length).toEqual(1); + expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2); + expect( + wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length + ).toEqual(1); + + expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy(); + + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( + 'Error' + ); + expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').last().text()).toEqual( + 'License Error' + ); + + // Status control column + expect(wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length).toEqual( + mockedRulesData.length + ); + + // Monitoring column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"]').length + ).toEqual(mockedRulesData.length); + const ratios = wrapper.find( + 'EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"] span[data-test-subj="successRatio"]' + ); + + mockedRulesData.forEach((rule, index) => { + if (rule.monitoring) { + expect(ratios.at(index).text()).toEqual( + `${rule.monitoring.execution.calculated_metrics.success_ratio * 100}%` + ); + } else { + expect(ratios.at(index).text()).toEqual(`N/A`); + } + }); - wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe( - 'Below configured minimum intervalRule interval of 1 second is below the minimum configured interval of 1 minute. This may impact alerting performance.' - ); - - wrapper.find('[data-test-subj="ruleInterval-config-tooltip-0"]').first().simulate('mouseOut'); - - // Duration column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-duration"]').length - ).toEqual(mockedRulesData.length); - // show warning if duration is long - const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); - expect(durationWarningIcon.length).toEqual( - mockedRulesData.filter( - (data) => - data.executionStatus.lastDuration > parseDuration(ruleTypeFromApi.ruleTaskTimeout) - ).length - ); - - // Duration tooltip - wrapper - .find('[data-test-subj="rulesTableCell-durationTooltip"]') - .first() - .simulate('mouseOver'); - - // Run the timers so the EuiTooltip will be visible - jest.runAllTimers(); + // P50 column is rendered initially + expect( + wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`).exists() + ).toBeTruthy(); + + let percentiles = wrapper.find( + `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` + ); + + mockedRulesData.forEach((rule, index) => { + if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { + // Ensure the table cells are getting the correct values + expect(percentiles.at(index).text()).toEqual( + getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50) + ); + // Ensure the tooltip is showing the correct content + expect( + wrapper + .find( + 'EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' + ) + .at(index) + .props().content + ).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50)); + } else { + expect(percentiles.at(index).text()).toEqual('N/A'); + } + }); - wrapper.update(); - expect(wrapper.find('.euiToolTipPopover').text()).toBe( - 'The length of time it took for the rule to run (mm:ss).' - ); - - // Last response column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-lastResponse"]').length - ).toEqual(mockedRulesData.length); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-active"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-ok"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-pending"]').length).toEqual(1); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-unknown"]').length).toEqual(0); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').length).toEqual(2); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-warning"]').length).toEqual(1); - expect(wrapper.find('[data-test-subj="ruleStatus-error-tooltip"]').length).toEqual(2); - expect( - wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').length - ).toEqual(1); - - expect(wrapper.find('[data-test-subj="refreshRulesButton"]').exists()).toBeTruthy(); - - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').first().text()).toEqual( - 'Error' - ); - expect(wrapper.find('EuiHealth[data-test-subj="ruleStatus-error"]').last().text()).toEqual( - 'License Error' - ); - - // Status control column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-status"]').length - ).toEqual(mockedRulesData.length); - - // Monitoring column - expect( - wrapper.find('EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"]').length - ).toEqual(mockedRulesData.length); - const ratios = wrapper.find( - 'EuiTableRowCell[data-test-subj="rulesTableCell-successRatio"] span[data-test-subj="successRatio"]' - ); - - mockedRulesData.forEach((rule, index) => { - if (rule.monitoring) { - expect(ratios.at(index).text()).toEqual( - `${rule.monitoring.execution.calculated_metrics.success_ratio * 100}%` - ); - } else { - expect(ratios.at(index).text()).toEqual(`N/A`); - } - }); + // Click column to sort by P50 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P50], + direction: 'asc', + }, + }) + ); + + // Click column again to reverse sort by P50 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P50], + direction: 'desc', + }, + }) + ); + + // Hover over percentile selection button + wrapper + .find('[data-test-subj="percentileSelectablePopover-iconButton"]') + .first() + .simulate('click'); + + jest.runAllTimers(); + wrapper.update(); + + // Percentile Selection + expect( + wrapper.find('[data-test-subj="percentileSelectablePopover-selectable"]').exists() + ).toBeTruthy(); + + const percentileOptions = wrapper.find( + '[data-test-subj="percentileSelectablePopover-selectable"] li' + ); + expect(percentileOptions.length).toEqual(3); + + // Select P95 + percentileOptions.at(1).simulate('click'); + + jest.runAllTimers(); + wrapper.update(); + + expect( + wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`).exists() + ).toBeTruthy(); + + percentiles = wrapper.find( + `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` + ); + + mockedRulesData.forEach((rule, index) => { + if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { + expect(percentiles.at(index).text()).toEqual( + getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) + ); + } else { + expect(percentiles.at(index).text()).toEqual('N/A'); + } + }); - // P50 column is rendered initially - expect( - wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`).exists() - ).toBeTruthy(); - - let percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` - ); - - mockedRulesData.forEach((rule, index) => { - if (typeof rule.monitoring?.execution.calculated_metrics.p50 === 'number') { - // Ensure the table cells are getting the correct values - expect(percentiles.at(index).text()).toEqual( - getFormattedDuration(rule.monitoring.execution.calculated_metrics.p50) - ); - // Ensure the tooltip is showing the correct content - expect( - wrapper - .find( - 'EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] [data-test-subj="rule-duration-format-tooltip"]' - ) - .at(index) - .props().content - ).toEqual(getFormattedMilliseconds(rule.monitoring.execution.calculated_metrics.p50)); - } else { - expect(percentiles.at(index).text()).toEqual('N/A'); - } - }); + // Click column to sort by P95 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P95], + direction: 'asc', + }, + }) + ); + + // Click column again to reverse sort by P95 + wrapper + .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) + .first() + .simulate('click'); + + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: percentileFields[Percentiles.P95], + direction: 'desc', + }, + }) + ); - // Click column to sort by P50 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P50], - direction: 'asc', - }, - }) - ); - - // Click column again to reverse sort by P50 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P50}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P50], - direction: 'desc', - }, - }) - ); + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); + }); - // Hover over percentile selection button - wrapper - .find('[data-test-subj="percentileSelectablePopover-iconButton"]') - .first() - .simulate('click'); + it('loads rules when refresh button is clicked', async () => { + await setup(); + wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click'); - jest.runAllTimers(); + await act(async () => { + await nextTick(); wrapper.update(); + }); - // Percentile Selection - expect( - wrapper.find('[data-test-subj="percentileSelectablePopover-selectable"]').exists() - ).toBeTruthy(); - - const percentileOptions = wrapper.find( - '[data-test-subj="percentileSelectablePopover-selectable"] li' - ); - expect(percentileOptions.length).toEqual(3); - - // Select P95 - percentileOptions.at(1).simulate('click'); + expect(loadRules).toHaveBeenCalled(); + }); - jest.runAllTimers(); + 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="ruleStatus-error-license-fix"]').length + ).toEqual(1); + wrapper.find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]').simulate('click'); + + await act(async () => { + await nextTick(); wrapper.update(); - - expect( - wrapper.find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`).exists() - ).toBeTruthy(); - - percentiles = wrapper.find( - `EuiTableRowCell[data-test-subj="rulesTableCell-ruleExecutionPercentile"] span[data-test-subj="rule-duration-format-value"]` - ); - - mockedRulesData.forEach((rule, index) => { - if (typeof rule.monitoring?.execution.calculated_metrics.p95 === 'number') { - expect(percentiles.at(index).text()).toEqual( - getFormattedDuration(rule.monitoring.execution.calculated_metrics.p95) - ); - } else { - expect(percentiles.at(index).text()).toEqual('N/A'); - } - }); - - // Click column to sort by P95 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P95], - direction: 'asc', - }, - }) - ); - - // Click column again to reverse sort by P95 - wrapper - .find(`[data-test-subj="rulesTable-${Percentiles.P95}ColumnName"]`) - .first() - .simulate('click'); - - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: percentileFields[Percentiles.P95], - direction: 'desc', - }, - }) - ); - - // Clearing all mocks will also reset fake timers. - jest.clearAllMocks(); }); - it('loads rules when refresh button is clicked', async () => { - await setup(); - wrapper.find('[data-test-subj="refreshRulesButton"]').first().simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(loadRules).toHaveBeenCalled(); - }); + 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(); + }); - 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="ruleStatus-error-license-fix"]').length - ).toEqual(1); - wrapper - .find('EuiButtonEmpty[data-test-subj="ruleStatus-error-license-fix"]') - .simulate('click'); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); + it('sorts rules when clicking the name column', async () => { + await setup(); + wrapper + .find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton') + .first() + .simulate('click'); - 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(); + await act(async () => { + await nextTick(); + wrapper.update(); }); - it('sorts rules when clicking the name column', async () => { - await setup(); - wrapper - .find('[data-test-subj="tableHeaderCell_name_0"] .euiTableHeaderButton') - .first() - .simulate('click'); + expect(loadRules).toHaveBeenCalledWith( + expect.objectContaining({ + sort: { + field: 'name', + direction: 'desc', + }, + }) + ); + }); - await act(async () => { - await nextTick(); - wrapper.update(); - }); + it('sorts rules when clicking the status control column', async () => { + await setup(); + wrapper + .find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton') + .first() + .simulate('click'); - expect(loadRules).toHaveBeenCalledWith( - expect.objectContaining({ - sort: { - field: 'name', - direction: 'desc', - }, - }) - ); + await act(async () => { + await nextTick(); + wrapper.update(); }); - it('sorts rules when clicking the status control column', async () => { - await setup(); - wrapper - .find('[data-test-subj="tableHeaderCell_enabled_8"] .euiTableHeaderButton') - .first() - .simulate('click'); + expect(loadRules).toHaveBeenLastCalledWith( + expect.objectContaining({ + sort: { + field: 'enabled', + direction: 'asc', + }, + }) + ); + }); - await act(async () => { - await nextTick(); - wrapper.update(); - }); + it('renders edit and delete buttons when user can manage rules', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); + }); - expect(loadRules).toHaveBeenLastCalledWith( - expect.objectContaining({ - sort: { - field: 'enabled', - direction: 'asc', - }, - }) - ); - }); + it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { + await setup(false); + expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); + }); - it('renders edit and delete buttons when user can manage rules', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); - }); + it('renders brief', async () => { + await setup(); + + // { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 } + expect(wrapper.find('EuiHealth[data-test-subj="totalOkRulesCount"]').text()).toEqual('Ok: 1'); + expect(wrapper.find('EuiHealth[data-test-subj="totalActiveRulesCount"]').text()).toEqual( + 'Active: 2' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalErrorRulesCount"]').text()).toEqual( + 'Error: 3' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalPendingRulesCount"]').text()).toEqual( + 'Pending: 4' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalUnknownRulesCount"]').text()).toEqual( + 'Unknown: 5' + ); + expect(wrapper.find('EuiHealth[data-test-subj="totalWarningRulesCount"]').text()).toEqual( + 'Warning: 6' + ); + }); - it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { - await setup(false); - expect(wrapper.find('[data-test-subj="ruleSidebarEditAction"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="ruleSidebarDeleteAction"]').exists()).toBeTruthy(); - }); + it('does not render the status filter if the feature flag is off', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeFalsy(); + }); - it('renders brief', async () => { - await setup(); - - // { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 } - expect(wrapper.find('EuiHealth[data-test-subj="totalOkRulesCount"]').text()).toEqual('Ok: 1'); - expect(wrapper.find('EuiHealth[data-test-subj="totalActiveRulesCount"]').text()).toEqual( - 'Active: 2' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalErrorRulesCount"]').text()).toEqual( - 'Error: 3' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalPendingRulesCount"]').text()).toEqual( - 'Pending: 4' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalUnknownRulesCount"]').text()).toEqual( - 'Unknown: 5' - ); - expect(wrapper.find('EuiHealth[data-test-subj="totalWarningRulesCount"]').text()).toEqual( - 'Warning: 6' - ); - }); + it('renders the status filter if the experiment is on', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + await setup(); + expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeTruthy(); + }); - it('does not render the status filter if the feature flag is off', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeFalsy(); - }); + it('can filter by rule states', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + loadRules.mockReset(); + await setup(); - it('renders the status filter if the experiment is on', async () => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); - await setup(); - expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeTruthy(); - }); + expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]); - it('can filter by rule states', async () => { - (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); - loadRules.mockReset(); - await setup(); + wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click'); - expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]); + wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click'); - wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click'); + expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']); - wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click'); + wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']); + expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']); - wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); + wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']); + expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); + }); +}); - wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); +describe('rules_list component empty with show only capability', () => { + let wrapper: ReactWrapper; - expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); + async function setup() { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 0, + data: [], }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + loadRuleTypes.mockResolvedValue([ + { id: 'test_rule_type', name: 'some rule type', authorizedConsumers: {} }, + ]); + loadAllActions.mockResolvedValue([]); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + } + + it('not renders create rule button', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="createRuleButton"]')).toHaveLength(0); }); +}); - describe('rules_list component empty with show only capability', () => { - let wrapper: ReactWrapper; +describe('rules_list with show only capability', () => { + let wrapper: ReactWrapper; - async function setup() { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 0, - data: [], - }); - loadActionTypes.mockResolvedValue([ + async function setup(editable: boolean = true) { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 2, + data: [ { - id: 'test', - name: 'Test', + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule 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: 'test2', - name: 'Test2', + id: '2', + name: 'test rule 2', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule 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, + }, }, - ]); - loadRuleTypes.mockResolvedValue([ - { id: 'test_rule_type', name: 'some rule type', authorizedConsumers: {} }, - ]); - loadAllActions.mockResolvedValue([]); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('not renders create rule button', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="createRuleButton"]')).toHaveLength(0); + ], }); - }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + + loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); + loadAllActions.mockResolvedValue([]); + + const ruleTypeMock: RuleTypeModel = { + id: 'test_rule_type', + iconClass: 'test', + description: 'Rule when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; - describe('rules_list with show only capability', () => { - let wrapper: ReactWrapper; + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - async function setup(editable: boolean = true) { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - name: 'test rule', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test rule 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 rule 2', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule 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, - }, - }, - ], - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, - { - id: 'test2', - name: 'Test2', - }, - ]); - - loadRuleTypes.mockResolvedValue([ruleTypeFromApi]); - loadAllActions.mockResolvedValue([]); - - const ruleTypeMock: RuleTypeModel = { - id: 'test_rule_type', - iconClass: 'test', - description: 'Rule when testing', - documentationUrl: 'https://localhost.local/docs', - validate: () => { - return { errors: {} }; - }, - ruleParamsExpression: jest.fn(), - requiresAppContext: !editable, - }; - - ruleTypeRegistry.has.mockReturnValue(true); - ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); - - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); - it('renders table of rules with edit button disabled', async () => { - await setup(false); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); + await act(async () => { + await nextTick(); + wrapper.update(); }); + } - it('renders table of rules with delete button disabled', async () => { - const { hasAllPrivilege } = jest.requireMock('../../../lib/capabilities'); - hasAllPrivilege.mockReturnValue(false); - await setup(false); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); - }); + it('renders table of rules with edit button disabled', async () => { + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); + }); - it('renders table of rules with actions menu collapsedItemActions', async () => { - await setup(false); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="collapsedItemActions"]').length).toBeGreaterThan(0); - }); + it('renders table of rules with delete button disabled', async () => { + const { hasAllPrivilege } = jest.requireMock('../../../lib/capabilities'); + hasAllPrivilege.mockReturnValue(false); + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); + }); + + it('renders table of rules with actions menu collapsedItemActions', async () => { + await setup(false); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="collapsedItemActions"]').length).toBeGreaterThan(0); }); +}); - describe('rules_list with disabled items', () => { - let wrapper: ReactWrapper; +describe('rules_list with disabled items', () => { + let wrapper: ReactWrapper; - async function setup() { - loadRules.mockResolvedValue({ - page: 1, - perPage: 10000, - total: 2, - data: [ - { - id: '1', - name: 'test rule', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type', - schedule: { interval: '5d' }, - actions: [], - params: { name: 'test rule 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 rule 2', - tags: ['tag1'], - enabled: true, - ruleTypeId: 'test_rule_type_disabled_by_license', - schedule: { interval: '5d' }, - actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], - params: { name: 'test rule 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, - }, - }, - ], - }); - loadActionTypes.mockResolvedValue([ - { - id: 'test', - name: 'Test', - }, + async function setup() { + loadRules.mockResolvedValue({ + page: 1, + perPage: 10000, + total: 2, + data: [ { - id: 'test2', - name: 'Test2', + id: '1', + name: 'test rule', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test rule 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, + }, }, - ]); - - loadRuleTypes.mockResolvedValue([ - ruleTypeFromApi, { - id: 'test_rule_type_disabled_by_license', - name: 'some rule type that is not allowed', - actionGroups: [{ id: 'default', name: 'Default' }], - recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, - actionVariables: { context: [], state: [] }, - defaultActionGroupId: 'default', - producer: ALERTS_FEATURE_ID, - minimumLicenseRequired: 'platinum', - enabledInLicense: false, - authorizedConsumers: { - [ALERTS_FEATURE_ID]: { read: true, all: true }, + id: '2', + name: 'test rule 2', + tags: ['tag1'], + enabled: true, + ruleTypeId: 'test_rule_type_disabled_by_license', + schedule: { interval: '5d' }, + actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }], + params: { name: 'test rule 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, }, }, - ]); - loadAllActions.mockResolvedValue([]); + ], + }); + loadActionTypes.mockResolvedValue([ + { + id: 'test', + name: 'Test', + }, + { + id: 'test2', + name: 'Test2', + }, + ]); + + loadRuleTypes.mockResolvedValue([ + ruleTypeFromApi, + { + id: 'test_rule_type_disabled_by_license', + name: 'some rule type that is not allowed', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + minimumLicenseRequired: 'platinum', + enabledInLicense: false, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + }, + }, + ]); + loadAllActions.mockResolvedValue([]); - ruleTypeRegistry.has.mockReturnValue(false); - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + ruleTypeRegistry.has.mockReturnValue(false); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; - // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - wrapper = mountWithIntl(); + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; + wrapper = mountWithIntl(); - await act(async () => { - await nextTick(); - wrapper.update(); - }); - } - - it('renders rules list with disabled indicator if disabled due to license', async () => { - await setup(); - expect(wrapper.find('EuiBasicTable')).toHaveLength(1); - expect(wrapper.find('EuiTableRow')).toHaveLength(2); - expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); - expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( - 'actRulesList__tableRowDisabled' - ); - expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( - 1 - ); - expect( - wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().type - ).toEqual('questionInCircle'); - expect( - wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content - ).toEqual('This rule type requires a Platinum license.'); + await act(async () => { + await nextTick(); + wrapper.update(); }); + } + + it('renders rules list with disabled indicator if disabled due to license', async () => { + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual(''); + expect(wrapper.find('EuiTableRow').at(1).prop('className')).toEqual( + 'actRulesList__tableRowDisabled' + ); + expect(wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').length).toBe( + 1 + ); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().type + ).toEqual('questionInCircle'); + expect( + wrapper.find('EuiIconTip[data-test-subj="ruleDisabledByLicenseTooltip"]').props().content + ).toEqual('This rule type requires a Platinum license.'); }); }); From c38f35000495c7bd6592fdfd878d88277b676ffe Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 9 May 2022 22:12:44 +0200 Subject: [PATCH 08/18] fix type check error --- .../rule_details/components/rule_actions_popover.test.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx index 5f88ba43184eb..91d9c568aec7c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx @@ -9,7 +9,6 @@ import * as React from 'react'; import { RuleActionsPopover } from './rule_actions_popover'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { Rule } from '../../../..'; -import { afterEach } from 'jest-circus'; describe('rule_actions_popover', () => { const onDeleteMock = jest.fn(); @@ -43,10 +42,6 @@ describe('rule_actions_popover', () => { }; } - afterEach(() => { - jest.clearAllMocks(); - }); - it('renders all the buttons', () => { const rule = mockRule(); render( From b485ff8fefa3cd917cec26de1fbe9cb922e7a3ee Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Tue, 10 May 2022 13:39:45 +0200 Subject: [PATCH 09/18] Add enable/disable rule buttons --- .../components/rule_actions_popover.test.tsx | 62 +++++++++++++++++++ .../components/rule_actions_popover.tsx | 19 ++++++ .../components/rule_details.test.tsx | 60 ++++++++++++++++++ .../rule_details/components/rule_details.tsx | 12 +++- 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx index 91d9c568aec7c..9a102d044a5ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx @@ -13,6 +13,7 @@ import { Rule } from '../../../..'; describe('rule_actions_popover', () => { const onDeleteMock = jest.fn(); const onApiKeyUpdateMock = jest.fn(); + const onEnableDisableMock = jest.fn(); function mockRule(overloads: Partial = {}): Rule { return { @@ -51,6 +52,7 @@ describe('rule_actions_popover', () => { onDelete={onDeleteMock} onApiKeyUpdate={onApiKeyUpdateMock} canSaveRule={true} + onEnableDisable={onEnableDisableMock} /> ); @@ -60,6 +62,7 @@ describe('rule_actions_popover', () => { fireEvent.click(actionButton); expect(screen.getByText('Update API Key')).toBeInTheDocument(); expect(screen.getByText('Delete rule')).toBeInTheDocument(); + expect(screen.getByText('Disable')).toBeInTheDocument(); }); it('calls onDelete', async () => { @@ -71,6 +74,7 @@ describe('rule_actions_popover', () => { onDelete={onDeleteMock} onApiKeyUpdate={onApiKeyUpdateMock} canSaveRule={true} + onEnableDisable={onEnableDisableMock} /> ); @@ -89,6 +93,61 @@ describe('rule_actions_popover', () => { }); }); + it('disables the rule', async () => { + const rule = mockRule(); + render( + + + + ); + + const actionButton = screen.getByTestId('ruleActionsButton'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + + const disableButton = screen.getByText('Disable'); + expect(disableButton).toBeInTheDocument(); + fireEvent.click(disableButton); + + expect(onEnableDisableMock).toHaveBeenCalledWith(false); + await waitFor(() => { + expect(screen.queryByText('Disable')).not.toBeInTheDocument(); + }); + }); + it('enables the rule', async () => { + const rule = mockRule({ enabled: false }); + render( + + + + ); + + const actionButton = screen.getByTestId('ruleActionsButton'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + + const enableButton = screen.getByText('Enable'); + expect(enableButton).toBeInTheDocument(); + fireEvent.click(enableButton); + + expect(onEnableDisableMock).toHaveBeenCalledWith(true); + await waitFor(() => { + expect(screen.queryByText('Disable')).not.toBeInTheDocument(); + }); + }); + it('calls onApiKeyUpdate', async () => { const rule = mockRule(); render( @@ -98,6 +157,7 @@ describe('rule_actions_popover', () => { onDelete={onDeleteMock} onApiKeyUpdate={onApiKeyUpdateMock} canSaveRule={true} + onEnableDisable={onEnableDisableMock} /> ); @@ -125,6 +185,7 @@ describe('rule_actions_popover', () => { onDelete={onDeleteMock} onApiKeyUpdate={onApiKeyUpdateMock} canSaveRule={false} + onEnableDisable={onEnableDisableMock} /> ); @@ -135,5 +196,6 @@ describe('rule_actions_popover', () => { expect(screen.getByText('Delete rule').closest('button')).toBeDisabled(); expect(screen.getByText('Update API Key').closest('button')).toBeDisabled(); + expect(screen.getByText('Disable').closest('button')).toBeDisabled(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx index fc3c073a42325..910bddba5a4c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx @@ -15,6 +15,7 @@ export interface RuleActionsPopoverProps { canSaveRule: boolean; onDelete: (ruleId: string) => void; onApiKeyUpdate: (ruleId: string) => void; + onEnableDisable: (enable: boolean) => void; } export const RuleActionsPopover: React.FunctionComponent = ({ @@ -22,6 +23,7 @@ export const RuleActionsPopover: React.FunctionComponent { const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -51,6 +53,23 @@ export const RuleActionsPopover: React.FunctionComponent { + setIsPopoverOpen(false); + onEnableDisable(!rule.enabled); + }, + name: !rule.enabled + ? i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.enableRuleButtonLabel', + { defaultMessage: 'Enable' } + ) + : i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel', + { defaultMessage: 'Disable' } + ), + }, { disabled: !canSaveRule, 'data-test-subj': 'updateAPIKeyButton', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx index adeda7f2590b4..0079042163583 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.test.tsx @@ -975,6 +975,66 @@ describe('rule_details', () => { }); }); + describe('enable/disable rule button', () => { + it('should disable the rule when clicked', async () => { + const rule = mockRule(); + const requestRefresh = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const actionsButton = wrapper.find('[data-test-subj="ruleActionsButton"]').first(); + actionsButton.simulate('click'); + + const disableButton = wrapper.find('[data-test-subj="disableButton"]').first(); + expect(disableButton.exists()).toBeTruthy(); + + disableButton.simulate('click'); + + expect(mockRuleApis.disableRule).toHaveBeenCalledTimes(1); + expect(mockRuleApis.disableRule).toHaveBeenCalledWith(rule); + }); + + it('should enable the rule when clicked', async () => { + const rule = { ...mockRule(), enabled: false }; + const requestRefresh = jest.fn(); + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + const actionsButton = wrapper.find('[data-test-subj="ruleActionsButton"]').first(); + actionsButton.simulate('click'); + + const enableButton = wrapper.find('[data-test-subj="disableButton"]').first(); + expect(enableButton.exists()).toBeTruthy(); + + enableButton.simulate('click'); + + expect(mockRuleApis.enableRule).toHaveBeenCalledTimes(1); + expect(mockRuleApis.enableRule).toHaveBeenCalledWith(rule); + }); + }); + function mockRule(overloads: Partial = {}): Rule { return { id: uuid.v4(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx index f709b68cc990b..ee7266fa06331 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_details.tsx @@ -273,7 +273,7 @@ export const RuleDetails: React.FunctionComponent = ({ }} onErrors={async () => { // Refresh the rule from the server, it may have been deleted - await setRule(); + await requestRefresh(); setRulesToDelete([]); }} onCancel={() => { @@ -296,7 +296,7 @@ export const RuleDetails: React.FunctionComponent = ({ setIsLoadingState={() => {}} onUpdated={async () => { setRulesToUpdateAPIKey([]); - setRule(); + requestRefresh(); }} /> = ({ onApiKeyUpdate={(ruleId) => { setRulesToUpdateAPIKey([ruleId]); }} + onEnableDisable={async (enable) => { + if (enable) { + await enableRule(rule); + } else { + await disableRule(rule); + } + requestRefresh(); + }} />, editButton, Date: Tue, 10 May 2022 22:32:39 +0200 Subject: [PATCH 10/18] Use correct mock data in enable rule test --- .../plugins/alerting/server/rules_client/tests/enable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 45344ff599c6b..786272cf539f6 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -92,7 +92,7 @@ describe('enable()', () => { (auditLogger.log as jest.Mock).mockClear(); rulesClient = new RulesClient(rulesClientParams); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRule); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingRuleWithoutApiKey); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingRule); rulesClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); From fba81a9b7c4a49bab3061c3772674d1eb5daebf6 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Wed, 11 May 2022 12:52:32 +0200 Subject: [PATCH 11/18] Update meta when disable a rule, Support updating multiple rules (Future proof) --- .../alerting/server/rules_client/rules_client.ts | 4 ++-- .../server/rules_client/tests/disable.test.ts | 13 ++++++++++++- .../update_api_key_modal_confirmation.tsx | 10 +++++++--- .../rules_list/components/rules_list.test.tsx | 1 - 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 7d0fe1fe96386..9ce37f5f086f8 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1615,7 +1615,7 @@ export class RulesClient { await this.unsecuredSavedObjectsClient.update( 'alert', id, - { + this.updateMeta({ ...attributes, ...(!attributes.apiKeyOwner && { apiKeyOwner: null }), ...(!attributes.apiKey && { apiKey: null }), @@ -1623,7 +1623,7 @@ export class RulesClient { scheduledTaskId: null, updatedBy: await this.getUserName(), updatedAt: new Date().toISOString(), - }, + }), { version } ); if (attributes.scheduledTaskId) { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index e5ceab1f152c4..2f4531bbde249 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -201,6 +201,9 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, scheduledTaskId: null, apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -268,7 +271,9 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, scheduledTaskId: null, apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -347,6 +352,9 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, scheduledTaskId: null, apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -391,6 +399,9 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, scheduledTaskId: null, apiKey: null, apiKeyOwner: null, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx index 211347b017988..a77cb1f5dc1c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx @@ -49,10 +49,12 @@ export const UpdateApiKeyModalConfirmation = ({ setUpdateModalVisibility(false); setIsLoadingState(true); try { - await apiUpdateApiKeyCall({ id: idsToUpdate[0], http }); + await Promise.all(idsToUpdate.map((id) => apiUpdateApiKeyCall({ id, http }))); toasts.addSuccess( i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.successMessage', { - defaultMessage: 'API Key has been updated', + defaultMessage: + 'API {idsToUpdate, plural, one {Key} other {Keys}} {idsToUpdate, plural, one {has} other {have}} been updated', + values: { idsToUpdate: idsToUpdate.length }, }) ); } catch (e) { @@ -60,7 +62,9 @@ export const UpdateApiKeyModalConfirmation = ({ title: i18n.translate( 'xpack.triggersActionsUI.updateApiKeyConfirmModal.failureMessage', { - defaultMessage: 'Failed to update the API Key', + defaultMessage: + 'Failed to update the API {idsToUpdate, plural, one {Key} other {Keys}}', + values: { idsToUpdate: idsToUpdate.length }, } ), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 4cc9e40cf0dc3..37bbebdc80b6e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -312,7 +312,6 @@ const mockedRulesData = [ beforeEach(() => { (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); }); - describe('Update Api Key', () => { const addSuccess = jest.fn(); const addError = jest.fn(); From ef30e4e909d137bbc8ae54aa3d7fd403e0b1e64f Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Wed, 11 May 2022 12:56:40 +0200 Subject: [PATCH 12/18] Update error message --- x-pack/plugins/alerting/server/rules_client/rules_client.ts | 2 +- .../plugins/alerting/server/rules_client/tests/enable.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 9ce37f5f086f8..1998a98b2d286 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1500,7 +1500,7 @@ export class RulesClient { this.generateAPIKeyName(attributes.alertTypeId, attributes.name) ); } catch (error) { - throw Boom.badRequest(`Error enabling rule: could not create API key - ${error.message}`); + throw Boom.badRequest(`Error creating API key for rule: ${error.message}`); } return this.apiKeyAsAlertAttributes(createdAPIKey, username); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 786272cf539f6..86694df950da4 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -376,9 +376,7 @@ describe('enable()', () => { }); await expect( async () => await rulesClient.enable({ id: '1' }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Error enabling rule: could not create API key - no"` - ); + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule: no"`); }); test('falls back when failing to getDecryptedAsInternalUser', async () => { From 92d4dce8928ab921b6f57fc5a39554f6357abb3f Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Wed, 11 May 2022 17:05:03 +0200 Subject: [PATCH 13/18] Copy and styling fixes --- .../update_api_key_modal_confirmation.tsx | 10 ++++--- .../components/rule_actions_popopver.scss | 8 +----- .../components/rule_actions_popover.test.tsx | 8 +++--- .../components/rule_actions_popover.tsx | 2 +- .../components/rule_details.test.tsx | 2 +- .../components/collapsed_item_actions.scss | 8 +----- .../collapsed_item_actions.test.tsx | 2 +- .../components/collapsed_item_actions.tsx | 2 +- .../rules_list/components/rules_list.test.tsx | 26 +++++++++++-------- 9 files changed, 31 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx index a77cb1f5dc1c1..93845ae3b366c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/update_api_key_modal_confirmation.tsx @@ -39,7 +39,7 @@ export const UpdateApiKeyModalConfirmation = ({ buttonColor="primary" data-test-subj="updateApiKeyIdsConfirmation" title={i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.title', { - defaultMessage: 'Update API Key', + defaultMessage: 'Update API key', })} onCancel={() => { setUpdateModalVisibility(false); @@ -53,7 +53,7 @@ export const UpdateApiKeyModalConfirmation = ({ toasts.addSuccess( i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.successMessage', { defaultMessage: - 'API {idsToUpdate, plural, one {Key} other {Keys}} {idsToUpdate, plural, one {has} other {have}} been updated', + 'API {idsToUpdate, plural, one {key} other {keys}} {idsToUpdate, plural, one {has} other {have}} been updated', values: { idsToUpdate: idsToUpdate.length }, }) ); @@ -63,7 +63,7 @@ export const UpdateApiKeyModalConfirmation = ({ 'xpack.triggersActionsUI.updateApiKeyConfirmModal.failureMessage', { defaultMessage: - 'Failed to update the API {idsToUpdate, plural, one {Key} other {Keys}}', + 'Failed to update the API {idsToUpdate, plural, one {key} other {keys}}', values: { idsToUpdate: idsToUpdate.length }, } ), @@ -86,7 +86,9 @@ export const UpdateApiKeyModalConfirmation = ({ )} > {i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.description', { - defaultMessage: "You can't recover the old API Key", + defaultMessage: + 'You will not be able to recover the old API {idsToUpdate, plural, one {key} other {keys}}', + values: { idsToUpdate: idsToUpdate.length }, })} ) : null; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss index ac64ece21f73b..b3cb695ecb44c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss @@ -1,9 +1,3 @@ -.ruleActionsPopover { - .euiContextMenuItem:hover { - text-decoration: none; - } -} - button[data-test-subj='deleteRuleButton'] { - color: $euiColorDanger; + color: $euiColorDangerText; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx index 9a102d044a5ea..bec45767bfee2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.test.tsx @@ -60,7 +60,7 @@ describe('rule_actions_popover', () => { const actionButton = screen.getByTestId('ruleActionsButton'); expect(actionButton).toBeInTheDocument(); fireEvent.click(actionButton); - expect(screen.getByText('Update API Key')).toBeInTheDocument(); + expect(screen.getByText('Update API key')).toBeInTheDocument(); expect(screen.getByText('Delete rule')).toBeInTheDocument(); expect(screen.getByText('Disable')).toBeInTheDocument(); }); @@ -166,13 +166,13 @@ describe('rule_actions_popover', () => { expect(actionButton).toBeInTheDocument(); fireEvent.click(actionButton); - const deleteButton = screen.getByText('Update API Key'); + const deleteButton = screen.getByText('Update API key'); expect(deleteButton).toBeInTheDocument(); fireEvent.click(deleteButton); expect(onApiKeyUpdateMock).toHaveBeenCalledWith('12345'); await waitFor(() => { - expect(screen.queryByText('Update API Key')).not.toBeInTheDocument(); + expect(screen.queryByText('Update API key')).not.toBeInTheDocument(); }); }); @@ -195,7 +195,7 @@ describe('rule_actions_popover', () => { fireEvent.click(actionButton); expect(screen.getByText('Delete rule').closest('button')).toBeDisabled(); - expect(screen.getByText('Update API Key').closest('button')).toBeDisabled(); + expect(screen.getByText('Update API key').closest('button')).toBeDisabled(); expect(screen.getByText('Disable').closest('button')).toBeDisabled(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx index 910bddba5a4c6..5f4457599bcb0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx @@ -79,7 +79,7 @@ export const RuleActionsPopover: React.FunctionComponent { }); }); - describe('update API Key button', () => { + describe('update API key button', () => { it('should call update api key when clicked', async () => { const rule = mockRule(); const requestRefresh = jest.fn(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss index ffe000073aa75..bddabb86eb4a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss @@ -1,9 +1,3 @@ -.actCollapsedItemActions { - .euiContextMenuItem:hover { - text-decoration: none; - } -} - button[data-test-subj='deleteRule'] { - color: $euiColorDanger; + color: $euiColorDangerText; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx index 4169de3f4b889..7759c940b8865 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.test.tsx @@ -147,7 +147,7 @@ describe('CollapsedItemActions', () => { expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule'); expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule'); - expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update API Key'); + expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update API key'); }); test('handles case when rule is unmuted and enabled and mute is clicked', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index 4ee0e37f53105..8dcc6fe16618b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -143,7 +143,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ }, name: i18n.translate( 'xpack.triggersActionsUI.sections.rulesList.collapsedItemActions.updateApiKey', - { defaultMessage: 'Update API Key' } + { defaultMessage: 'Update API key' } ), }, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 37bbebdc80b6e..7827033138fbb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -348,27 +348,29 @@ describe('Update Api Key', () => { fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update API Key')); - expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API key')); + expect(screen.getByText('You will not be able to recover the old API key')).toBeInTheDocument(); fireEvent.click(screen.getByText('Cancel')); - expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); + expect( + screen.queryByText('You will not be able to recover the old API key') + ).not.toBeInTheDocument(); fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update API Key')); + fireEvent.click(screen.getByText('Update API key')); await act(async () => { fireEvent.click(screen.getByText('Update')); }); expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); - expect(addSuccess).toHaveBeenCalledWith('API Key has been updated'); + expect(screen.queryByText("You can't recover the old API key")).not.toBeInTheDocument(); + expect(addSuccess).toHaveBeenCalledWith('API key has been updated'); }); - it('Update Api Key fails', async () => { + it('Update API key fails', async () => { updateAPIKey.mockRejectedValueOnce(500); render( @@ -381,16 +383,18 @@ describe('Update Api Key', () => { fireEvent.click(screen.getAllByTestId('selectActionButton')[1]); expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument(); - fireEvent.click(screen.getByText('Update API Key')); - expect(screen.getByText("You can't recover the old API Key")).toBeInTheDocument(); + fireEvent.click(screen.getByText('Update API key')); + expect(screen.getByText('You will not be able to recover the old API key')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Update')); }); expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' })); expect(loadRules).toHaveBeenCalledTimes(2); - expect(screen.queryByText("You can't recover the old API Key")).not.toBeInTheDocument(); - expect(addError).toHaveBeenCalledWith(500, { title: 'Failed to update the API Key' }); + expect( + screen.queryByText('You will not be able to recover the old API key') + ).not.toBeInTheDocument(); + expect(addError).toHaveBeenCalledWith(500, { title: 'Failed to update the API key' }); }); }); From 29168a7737549d1655da7ce80498c6ced835adf7 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Wed, 11 May 2022 19:43:51 +0200 Subject: [PATCH 14/18] Fix Delete modal text. remove checkAAD from test --- .../plugins/alerting/server/rules_client/rules_client.ts | 2 -- .../alerting/server/rules_client/tests/disable.test.ts | 5 ----- x-pack/plugins/translations/translations/ja-JP.json | 2 +- x-pack/plugins/translations/translations/zh-CN.json | 2 +- .../application/components/delete_modal_confirmation.tsx | 2 +- .../security_and_spaces/group1/tests/alerting/disable.ts | 7 ------- 6 files changed, 3 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 1998a98b2d286..f700c4364f74d 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1617,8 +1617,6 @@ export class RulesClient { id, this.updateMeta({ ...attributes, - ...(!attributes.apiKeyOwner && { apiKeyOwner: null }), - ...(!attributes.apiKey && { apiKey: null }), enabled: false, scheduledTaskId: null, updatedBy: await this.getUserName(), diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index 2f4531bbde249..7643bcd337d6c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -399,12 +399,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, - meta: { - versionApiKeyLastmodified: 'v7.10.0', - }, scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', actions: [ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fb1ca2dcd650a..7a839b11038b9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -31172,6 +31172,7 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Webフックポートが必要です。", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "ユーザー名が必要です。", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", + "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。", "unifiedSearch.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "タイトルがすでに保存されているクエリに使用されています", "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "保存", @@ -31263,7 +31264,6 @@ "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", - "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "{bothArguments} が true であることを条件とする", "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3b96df93ad35d..fc8755cb7dae2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -31208,6 +31208,7 @@ "xpack.watcher.watchActions.webhook.portIsRequiredValidationMessage": "Webhook 端口必填。", "xpack.watcher.watchActions.webhook.usernameIsRequiredIfPasswordValidationMessage": "“用户名”必填。", "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", + "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。", "unifiedSearch.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", "unifiedSearch.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", "unifiedSearch.search.searchBar.savedQueryFormSaveButtonText": "保存", @@ -31299,7 +31300,6 @@ "unifiedSearch.filter.options.invertNegatedFiltersButtonLabel": "反向包括", "unifiedSearch.filter.options.pinAllFiltersButtonLabel": "全部固定", "unifiedSearch.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", - "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。", "unifiedSearch.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为 true", "unifiedSearch.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", "unifiedSearch.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx index 7626cc49a5c4c..b8a11cc5892cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx @@ -54,7 +54,7 @@ export const DeleteModalConfirmation = ({ 'xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText', { defaultMessage: - "You can't recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.", + "You won't be able to recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.", values: { numIdsToDelete, singleTitle, multipleTitle }, } ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts index 864de743ea343..842a00366945a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/disable.ts @@ -334,13 +334,6 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte } catch (e) { expect(e.meta.statusCode).to.eql(404); } - // Ensure AAD isn't broken - await checkAAD({ - supertest, - spaceId: space.id, - type: 'alert', - id: createdAlert.id, - }); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); From d0c0dfc3eb3e2f65db771892fb6b1816a6e936b5 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 16 May 2022 20:41:20 +0200 Subject: [PATCH 15/18] Add functional test and use classname for styling --- .../components/rule_actions_popopver.scss | 2 +- .../components/rule_actions_popover.tsx | 1 + .../components/collapsed_item_actions.scss | 2 +- .../components/collapsed_item_actions.tsx | 1 + .../fixtures/plugins/alerts/server/routes.ts | 43 +++++++ .../common/lib/alert_utils.ts | 10 ++ .../group1/tests/alerting/index.ts | 1 + .../group1/tests/alerting/retain_api_key.ts | 106 ++++++++++++++++++ 8 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss index b3cb695ecb44c..f776a67fabf89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popopver.scss @@ -1,3 +1,3 @@ -button[data-test-subj='deleteRuleButton'] { +.ruleActionsPopover__deleteButton { color: $euiColorDangerText; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx index 5f4457599bcb0..0862bf3bf3c1c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_actions_popover.tsx @@ -84,6 +84,7 @@ export const RuleActionsPopover: React.FunctionComponent { setIsPopoverOpen(false); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss index bddabb86eb4a8..fe009998ff1c4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.scss @@ -1,3 +1,3 @@ -button[data-test-subj='deleteRule'] { +.collapsedItemActions__deleteButton { color: $euiColorDangerText; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx index 8dcc6fe16618b..7e65961ac80f0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/collapsed_item_actions.tsx @@ -148,6 +148,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ }, { disabled: !item.isEditable, + className: 'collapsedItemActions__deleteButton', 'data-test-subj': 'deleteRule', onClick: () => { setIsPopoverOpen(!isPopoverOpen); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index 837ca4adf217b..55fcff63e3d28 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -13,6 +13,7 @@ import { KibanaResponseFactory, IKibanaResponse, Logger, + SavedObject, } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { InvalidatePendingApiKey } from '@kbn/alerting-plugin/server/types'; @@ -364,4 +365,46 @@ export function defineRoutes( } } ); + + router.get( + { + path: '/api/alerting/rule/{id}/_get_api_key', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { id } = req.params; + const [, { encryptedSavedObjects, spaces }] = await core.getStartServices(); + + const spaceId = spaces ? spaces.spacesService.getSpaceId(req) : 'default'; + + let namespace: string | undefined; + if (spaces && spaceId) { + namespace = spaces.spacesService.spaceIdToNamespace(spaceId); + } + + try { + const { + attributes: { apiKey, apiKeyOwner }, + }: SavedObject = await encryptedSavedObjects + .getClient({ + includedHiddenTypes: ['alert'], + }) + .getDecryptedAsInternalUser('alert', id, { + namespace, + }); + + return res.ok({ body: { apiKey, apiKeyOwner } }); + } catch (err) { + return res.badRequest({ body: err }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index 436a98d4cf3f8..cc4c730743d46 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -76,6 +76,16 @@ export class AlertUtils { return request; } + public getAPIKeyRequest(ruleId: string) { + const request = this.supertestWithoutAuth.get( + `${getUrlPrefix(this.space.id)}/api/alerting/rule/${ruleId}/_get_api_key` + ); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + public getDisableRequest(alertId: string) { const request = this.supertestWithoutAuth .post(`${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/_disable`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts index a5c81a849d8f8..53b7e8e3fb2c0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/index.ts @@ -31,6 +31,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./get_alert_summary')); loadTestFile(require.resolve('./rule_types')); loadTestFile(require.resolve('./bulk_edit')); + loadTestFile(require.resolve('./retain_api_key')); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts new file mode 100644 index 0000000000000..d406bf0212383 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts @@ -0,0 +1,106 @@ +/* + * 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 expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../../scenarios'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { AlertUtils, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; + +// eslint-disable-next-line import/no-default-export +export default function retainAPIKeyTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('retain api key', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should retain the api key when a rule is disabled and then enabled', async () => { + const { body: createdConnector } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + actions: [ + { + id: createdConnector.id, + group: 'default', + params: {}, + }, + ], + }) + ) + .expect(200); + objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); + + const { + body: { apiKey, apiKeyOwner }, + } = await alertUtils.getAPIKeyRequest(createdRule.id); + + await alertUtils.getDisableRequest(createdRule.id); + + const { + body: { apiKey: apiKeyAfterDisable, apiKeyOwner: apiKeyOwnerAfterDisable }, + } = await alertUtils.getAPIKeyRequest(createdRule.id); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(apiKey).to.be(apiKeyAfterDisable); + expect(apiKeyOwner).to.be(apiKeyOwnerAfterDisable); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + + await alertUtils.getEnableRequest(createdRule.id); + + const { + body: { apiKey: apiKeyAfterEnable, apiKeyOwner: apiKeyOwnerAfterEnable }, + } = await alertUtils.getAPIKeyRequest(createdRule.id); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(apiKey).to.be(apiKeyAfterEnable); + expect(apiKeyOwner).to.be(apiKeyOwnerAfterEnable); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} From 0da7084a0d5e59abb5d593fd0303c180cccdb39e Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Mon, 16 May 2022 21:14:58 +0200 Subject: [PATCH 16/18] Update Auth docs --- docs/user/alerting/alerting-setup.asciidoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/user/alerting/alerting-setup.asciidoc b/docs/user/alerting/alerting-setup.asciidoc index 2b92e8caa7ef9..6643f8d0ec870 100644 --- a/docs/user/alerting/alerting-setup.asciidoc +++ b/docs/user/alerting/alerting-setup.asciidoc @@ -65,10 +65,9 @@ Rules and connectors are isolated to the {kib} space in which they were created. Rules are authorized using an <> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks like {es} queries and triggered actions. The following rule actions will re-generate the API key: * Creating a rule -* Enabling a disabled rule * Updating a rule [IMPORTANT] ============================================== -If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates, disables, or re-enables the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. +If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges. ============================================== From 521ef6e6549525dd46267ab58f29cd9ac982f3a5 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Tue, 17 May 2022 17:21:56 +0200 Subject: [PATCH 17/18] use alerts_fixture in _get_api_key ep path test apiKei is not null or undefined --- .../common/fixtures/plugins/alerts/server/routes.ts | 2 +- .../test/alerting_api_integration/common/lib/alert_utils.ts | 2 +- .../group1/tests/alerting/retain_api_key.ts | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index 55fcff63e3d28..4934e31b27f79 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -368,7 +368,7 @@ export function defineRoutes( router.get( { - path: '/api/alerting/rule/{id}/_get_api_key', + path: '/api/alerts_fixture/rule/{id}/_get_api_key', validate: { params: schema.object({ id: schema.string(), diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts index cc4c730743d46..2525e7fa50b7e 100644 --- a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -78,7 +78,7 @@ export class AlertUtils { public getAPIKeyRequest(ruleId: string) { const request = this.supertestWithoutAuth.get( - `${getUrlPrefix(this.space.id)}/api/alerting/rule/${ruleId}/_get_api_key` + `${getUrlPrefix(this.space.id)}/api/alerts_fixture/rule/${ruleId}/_get_api_key` ); if (this.user) { return request.auth(this.user.username, this.user.password); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts index d406bf0212383..2b7f67495c9dc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts @@ -53,11 +53,17 @@ export default function retainAPIKeyTests({ getService }: FtrProviderContext) { ) .expect(200); objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); + objectRemover.add(space.id, createdConnector.id, 'connector', 'alerting'); const { body: { apiKey, apiKeyOwner }, } = await alertUtils.getAPIKeyRequest(createdRule.id); + expect(apiKey).not.to.be(null); + expect(apiKey).not.to.be(undefined); + expect(apiKeyOwner).not.to.be(null); + expect(apiKeyOwner).not.to.be(undefined); + await alertUtils.getDisableRequest(createdRule.id); const { From 5e87f3e58b7e641be3ec4f492860b54536c6e658 Mon Sep 17 00:00:00 2001 From: Ersin Erdal Date: Tue, 17 May 2022 20:49:02 +0200 Subject: [PATCH 18/18] fix object remover --- .../group1/tests/alerting/retain_api_key.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts index 2b7f67495c9dc..51b50ae3dc6b3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/retain_api_key.ts @@ -26,7 +26,7 @@ export default function retainAPIKeyTests({ getService }: FtrProviderContext) { describe(scenario.id, () => { it('should retain the api key when a rule is disabled and then enabled', async () => { - const { body: createdConnector } = await supertest + const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/connector`) .set('kbn-xsrf', 'foo') .send({ @@ -44,7 +44,7 @@ export default function retainAPIKeyTests({ getService }: FtrProviderContext) { getTestRuleData({ actions: [ { - id: createdConnector.id, + id: createdAction.id, group: 'default', params: {}, }, @@ -53,8 +53,7 @@ export default function retainAPIKeyTests({ getService }: FtrProviderContext) { ) .expect(200); objectRemover.add(space.id, createdRule.id, 'rule', 'alerting'); - objectRemover.add(space.id, createdConnector.id, 'connector', 'alerting'); - + objectRemover.add(space.id, createdAction.id, 'action', 'actions'); const { body: { apiKey, apiKeyOwner }, } = await alertUtils.getAPIKeyRequest(createdRule.id);