From e4c37f65c37a566a55c944ec25e2dc367ad1f586 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Mon, 9 Nov 2020 14:21:08 -0500 Subject: [PATCH] [Enterprise Search] Log retention settings logic (#82364) (#82983) --- .../log_retention/log_retention_logic.test.ts | 368 ++++++++++++++++++ .../log_retention/log_retention_logic.ts | 117 ++++++ .../settings/log_retention/types.ts | 42 ++ .../utils/convert_log_retention.test.ts | 64 +++ .../utils/convert_log_retention.ts | 45 +++ 5 files changed, 636 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts new file mode 100644 index 0000000000000..367c7b085123f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts @@ -0,0 +1,368 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { mockHttpValues } from '../../../../__mocks__'; +jest.mock('../../../../shared/http', () => ({ + HttpLogic: { values: mockHttpValues }, +})); +const { http } = mockHttpValues; + +jest.mock('../../../../shared/flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../../../../shared/flash_messages'; + +import { ELogRetentionOptions } from './types'; +import { LogRetentionLogic } from './log_retention_logic'; + +describe('LogRetentionLogic', () => { + const TYPICAL_SERVER_LOG_RETENTION = { + analytics: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + api: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + }; + + const TYPICAL_CLIENT_LOG_RETENTION = { + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }; + + const DEFAULT_VALUES = { + logRetention: null, + openedModal: null, + isLogRetentionUpdating: false, + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + log_retention_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + LogRetentionLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(LogRetentionLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setOpenedModal', () => { + describe('openedModal', () => { + it('should be set to the provided value', () => { + mount(); + + LogRetentionLogic.actions.setOpenedModal(ELogRetentionOptions.Analytics); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + openedModal: ELogRetentionOptions.Analytics, + }); + }); + }); + }); + + describe('closeModals', () => { + describe('openedModal', () => { + it('resets openedModal to null', () => { + mount({ + openedModal: 'analytics', + }); + + LogRetentionLogic.actions.closeModals(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + openedModal: null, + }); + }); + }); + + describe('isLogRetentionUpdating', () => { + it('resets isLogRetentionUpdating to false', () => { + mount({ + isLogRetentionUpdating: true, + }); + + LogRetentionLogic.actions.closeModals(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: false, + }); + }); + }); + }); + + describe('clearLogRetentionUpdating', () => { + describe('isLogRetentionUpdating', () => { + it('resets isLogRetentionUpdating to false', () => { + mount({ + isLogRetentionUpdating: true, + }); + + LogRetentionLogic.actions.clearLogRetentionUpdating(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: false, + }); + }); + }); + }); + + describe('updateLogRetention', () => { + describe('logRetention', () => { + it('updates the logRetention values that are passed', () => { + mount({ + logRetention: {}, + }); + + LogRetentionLogic.actions.updateLogRetention({ + api: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + }); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + logRetention: { + api: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + }, + }); + }); + }); + }); + + describe('saveLogRetention', () => { + beforeEach(() => { + mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); + }); + + describe('openedModal', () => { + it('should be reset to null', () => { + mount({ + openedModal: ELogRetentionOptions.Analytics, + }); + + LogRetentionLogic.actions.saveLogRetention(ELogRetentionOptions.Analytics, true); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + openedModal: null, + }); + }); + }); + + it('will call an API endpoint and update log retention', async () => { + jest.spyOn(LogRetentionLogic.actions, 'updateLogRetention'); + const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); + http.put.mockReturnValue(promise); + + LogRetentionLogic.actions.saveLogRetention(ELogRetentionOptions.Analytics, true); + + expect(http.put).toHaveBeenCalledWith('/api/app_search/log_settings', { + body: JSON.stringify({ + analytics: { + enabled: true, + }, + }), + }); + + await promise; + expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( + TYPICAL_CLIENT_LOG_RETENTION + ); + + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + const promise = Promise.reject('An error occured'); + http.put.mockReturnValue(promise); + + LogRetentionLogic.actions.saveLogRetention(ELogRetentionOptions.Analytics, true); + + try { + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + }); + + describe('toggleLogRetention', () => { + describe('isLogRetentionUpdating', () => { + it('sets isLogRetentionUpdating to true', () => { + mount({ + isLogRetentionUpdating: false, + }); + + LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.Analytics); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: true, + }); + }); + }); + + it('will call setOpenedModal if already enabled', () => { + mount({ + logRetention: { + [ELogRetentionOptions.Analytics]: { + enabled: true, + }, + }, + }); + jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); + + LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.Analytics); + + expect(LogRetentionLogic.actions.setOpenedModal).toHaveBeenCalledWith( + ELogRetentionOptions.Analytics + ); + }); + }); + + describe('fetchLogRetention', () => { + describe('isLogRetentionUpdating', () => { + it('sets isLogRetentionUpdating to true', () => { + mount({ + isLogRetentionUpdating: false, + }); + + LogRetentionLogic.actions.fetchLogRetention(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: true, + }); + }); + }); + + it('will call an API endpoint and update log retention', async () => { + mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); + jest + .spyOn(LogRetentionLogic.actions, 'updateLogRetention') + .mockImplementationOnce(() => {}); + + const promise = Promise.resolve(TYPICAL_SERVER_LOG_RETENTION); + http.get.mockReturnValue(promise); + + LogRetentionLogic.actions.fetchLogRetention(); + + expect(http.get).toHaveBeenCalledWith('/api/app_search/log_settings'); + await promise; + expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( + TYPICAL_CLIENT_LOG_RETENTION + ); + + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + mount(); + jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); + const promise = Promise.reject('An error occured'); + http.get.mockReturnValue(promise); + + LogRetentionLogic.actions.fetchLogRetention(); + + try { + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + }); + }); + + it('will call saveLogRetention if NOT already enabled', () => { + mount({ + logRetention: { + [ELogRetentionOptions.Analytics]: { + enabled: false, + }, + }, + }); + jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); + + LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.Analytics); + + expect(LogRetentionLogic.actions.saveLogRetention).toHaveBeenCalledWith( + ELogRetentionOptions.Analytics, + true + ); + }); + + it('will do nothing if logRetention option is not yet set', () => { + mount({ + logRetention: {}, + }); + jest.spyOn(LogRetentionLogic.actions, 'saveLogRetention'); + jest.spyOn(LogRetentionLogic.actions, 'setOpenedModal'); + + LogRetentionLogic.actions.toggleLogRetention(ELogRetentionOptions.API); + + expect(LogRetentionLogic.actions.saveLogRetention).not.toHaveBeenCalled(); + expect(LogRetentionLogic.actions.setOpenedModal).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts new file mode 100644 index 0000000000000..28830f2edb1d9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { ELogRetentionOptions, ILogRetention, ILogRetentionServer } from './types'; +import { HttpLogic } from '../../../../shared/http'; +import { flashAPIErrors } from '../../../../shared/flash_messages'; +import { convertLogRetentionFromServerToClient } from './utils/convert_log_retention'; + +interface ILogRetentionActions { + clearLogRetentionUpdating(): { value: boolean }; + closeModals(): { value: boolean }; + fetchLogRetention(): { value: boolean }; + saveLogRetention( + option: ELogRetentionOptions, + enabled: boolean + ): { option: ELogRetentionOptions; enabled: boolean }; + setOpenedModal(option: ELogRetentionOptions): { option: ELogRetentionOptions }; + toggleLogRetention(option: ELogRetentionOptions): { option: ELogRetentionOptions }; + updateLogRetention(logRetention: ILogRetention): { logRetention: ILogRetention }; +} + +interface ILogRetentionValues { + logRetention: ILogRetention | null; + isLogRetentionUpdating: boolean; + openedModal: ELogRetentionOptions | null; +} + +export const LogRetentionLogic = kea>({ + path: ['enterprise_search', 'app_search', 'log_retention_logic'], + actions: () => ({ + clearLogRetentionUpdating: true, + closeModals: true, + fetchLogRetention: true, + saveLogRetention: (option, enabled) => ({ enabled, option }), + setOpenedModal: (option) => ({ option }), + toggleLogRetention: (option) => ({ option }), + updateLogRetention: (logRetention) => ({ logRetention }), + }), + reducers: () => ({ + logRetention: [ + null, + { + updateLogRetention: (_, { logRetention }) => logRetention, + }, + ], + isLogRetentionUpdating: [ + false, + { + clearLogRetentionUpdating: () => false, + closeModals: () => false, + fetchLogRetention: () => true, + toggleLogRetention: () => true, + }, + ], + openedModal: [ + null, + { + closeModals: () => null, + saveLogRetention: () => null, + setOpenedModal: (_, { option }) => option, + }, + ], + }), + listeners: ({ actions, values }) => ({ + fetchLogRetention: async () => { + try { + const { http } = HttpLogic.values; + const response = await http.get('/api/app_search/log_settings'); + actions.updateLogRetention( + convertLogRetentionFromServerToClient(response as ILogRetentionServer) + ); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.clearLogRetentionUpdating(); + } + }, + saveLogRetention: async ({ enabled, option }) => { + const updateData = { [option]: { enabled } }; + + try { + const { http } = HttpLogic.values; + const response = await http.put('/api/app_search/log_settings', { + body: JSON.stringify(updateData), + }); + actions.updateLogRetention( + convertLogRetentionFromServerToClient(response as ILogRetentionServer) + ); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.clearLogRetentionUpdating(); + } + }, + toggleLogRetention: ({ option }) => { + const logRetention = values.logRetention?.[option]; + + // If the user has found a way to call this before we've retrieved + // log retention settings from the server, short circuit this and return early + if (!logRetention) { + return; + } + + const optionIsAlreadyEnabled = logRetention.enabled; + if (optionIsAlreadyEnabled) { + actions.setOpenedModal(option); + } else { + actions.saveLogRetention(option, true); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts new file mode 100644 index 0000000000000..7d4f30f88f38f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum ELogRetentionOptions { + Analytics = 'analytics', + API = 'api', +} + +export interface ILogRetention { + [ELogRetentionOptions.Analytics]: ILogRetentionSettings; + [ELogRetentionOptions.API]: ILogRetentionSettings; +} + +export interface ILogRetentionPolicy { + isDefault: boolean; + minAgeDays: number | null; +} + +export interface ILogRetentionSettings { + disabledAt?: string | null; + enabled?: boolean; + retentionPolicy?: ILogRetentionPolicy | null; +} + +export interface ILogRetentionServer { + [ELogRetentionOptions.Analytics]: ILogRetentionServerSettings; + [ELogRetentionOptions.API]: ILogRetentionServerSettings; +} + +export interface ILogRetentionServerPolicy { + is_default: boolean; + min_age_days: number | null; +} + +export interface ILogRetentionServerSettings { + disabled_at: string | null; + enabled: boolean; + retention_policy: ILogRetentionServerPolicy | null; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts new file mode 100644 index 0000000000000..b49b2afe50831 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { convertLogRetentionFromServerToClient } from './convert_log_retention'; + +describe('convertLogRetentionFromServerToClient', () => { + it('converts log retention from server to client', () => { + expect( + convertLogRetentionFromServerToClient({ + analytics: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + api: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, + }) + ).toEqual({ + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, + }); + }); + + it('handles null retention policies and null min_age_days', () => { + expect( + convertLogRetentionFromServerToClient({ + analytics: { + disabled_at: null, + enabled: true, + retention_policy: null, + }, + api: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: null }, + }, + }) + ).toEqual({ + analytics: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, + api: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: null }, + }, + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts new file mode 100644 index 0000000000000..1c0818fc286f2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ELogRetentionOptions, + ILogRetention, + ILogRetentionPolicy, + ILogRetentionServer, + ILogRetentionServerPolicy, + ILogRetentionServerSettings, + ILogRetentionSettings, +} from '../types'; + +export const convertLogRetentionFromServerToClient = ( + logRetention: ILogRetentionServer +): ILogRetention => ({ + [ELogRetentionOptions.Analytics]: convertLogRetentionSettingsFromServerToClient( + logRetention[ELogRetentionOptions.Analytics] + ), + [ELogRetentionOptions.API]: convertLogRetentionSettingsFromServerToClient( + logRetention[ELogRetentionOptions.API] + ), +}); + +const convertLogRetentionSettingsFromServerToClient = ({ + disabled_at: disabledAt, + enabled, + retention_policy: retentionPolicy, +}: ILogRetentionServerSettings): ILogRetentionSettings => ({ + disabledAt, + enabled, + retentionPolicy: + retentionPolicy === null ? null : convertLogRetentionPolicyFromServerToClient(retentionPolicy), +}); + +const convertLogRetentionPolicyFromServerToClient = ({ + min_age_days: minAgeDays, + is_default: isDefault, +}: ILogRetentionServerPolicy): ILogRetentionPolicy => ({ + isDefault, + minAgeDays, +});