From 8d539547e1070d63fc1b98516521e4fe4f252b3b Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Thu, 12 Nov 2020 12:13:30 -0800 Subject: [PATCH] Add EngineOverviewLogic --- .../engine_overview_logic.test.ts | 163 ++++++++++++++++++ .../engine_overview/engine_overview_logic.ts | 130 ++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts new file mode 100644 index 000000000000000..22d388d8b0d9473 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -0,0 +1,163 @@ +/* + * 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'; + +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'some-engine' } }, +})); + +import { EngineOverviewLogic } from './'; + +describe('EngineOverviewLogic', () => { + const mockEngineMetrics = { + apiLogsUnavailable: true, + documentCount: 10, + startDate: '1970-01-30', + endDate: '1970-01-31', + operationsPerDay: [0, 0, 0, 0, 0, 0, 0], + queriesPerDay: [0, 0, 0, 0, 0, 25, 50], + totalClicks: 50, + totalQueries: 75, + }; + + const DEFAULT_VALUES = { + dataLoading: true, + apiLogsUnavailable: false, + documentCount: 0, + startDate: '', + endDate: '', + operationsPerDay: [], + queriesPerDay: [], + totalClicks: 0, + totalQueries: 0, + timeoutId: null, + }; + + const mount = () => { + resetContext({}); + EngineOverviewLogic.mount(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(EngineOverviewLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('setPolledData', () => { + it('should set all received data as top-level values and set dataLoading to false', () => { + mount(); + EngineOverviewLogic.actions.setPolledData(mockEngineMetrics); + + expect(EngineOverviewLogic.values).toEqual({ + ...DEFAULT_VALUES, + ...mockEngineMetrics, + dataLoading: false, + }); + }); + }); + + describe('setTimeoutId', () => { + describe('timeoutId', () => { + it('should be set to the provided value', () => { + mount(); + EngineOverviewLogic.actions.setTimeoutId(123); + + expect(EngineOverviewLogic.values).toEqual({ + ...DEFAULT_VALUES, + timeoutId: 123, + }); + }); + }); + }); + + describe('pollForOverviewMetrics', () => { + it('fetches data and calls onPollingSuccess', async () => { + mount(); + jest.spyOn(EngineOverviewLogic.actions, 'onPollingSuccess'); + const promise = Promise.resolve(mockEngineMetrics); + http.get.mockReturnValueOnce(promise); + + EngineOverviewLogic.actions.pollForOverviewMetrics(); + await promise; + + expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/overview'); + expect(EngineOverviewLogic.actions.onPollingSuccess).toHaveBeenCalledWith( + mockEngineMetrics + ); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occurred'); + http.get.mockReturnValue(promise); + + try { + EngineOverviewLogic.actions.pollForOverviewMetrics(); + await promise; + } catch { + // Do nothing + } + expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); + }); + }); + + describe('onPollingSuccess', () => { + it('starts a polling timeout and sets data', async () => { + mount(); + jest.useFakeTimers(); + jest.spyOn(EngineOverviewLogic.actions, 'setTimeoutId'); + jest.spyOn(EngineOverviewLogic.actions, 'setPolledData'); + + EngineOverviewLogic.actions.onPollingSuccess(mockEngineMetrics); + + expect(setTimeout).toHaveBeenCalledWith( + EngineOverviewLogic.actions.pollForOverviewMetrics, + 5000 + ); + expect(EngineOverviewLogic.actions.setTimeoutId).toHaveBeenCalledWith(expect.any(Number)); + expect(EngineOverviewLogic.actions.setPolledData).toHaveBeenCalledWith(mockEngineMetrics); + }); + }); + }); + + describe('unmount', () => { + let unmount: Function; + + beforeEach(() => { + jest.useFakeTimers(); + resetContext({}); + unmount = EngineOverviewLogic.mount(); + }); + + it('clears existing polling timeouts on unmount', () => { + EngineOverviewLogic.actions.setTimeoutId(123); + unmount(); + expect(clearTimeout).toHaveBeenCalled(); + }); + + it("does not clear timeout if one hasn't been set on unmount", () => { + unmount(); + expect(clearTimeout).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts new file mode 100644 index 000000000000000..3fc7ce8083e03ef --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.ts @@ -0,0 +1,130 @@ +/* + * 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 { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { EngineLogic } from '../engine'; + +const POLLING_DURATION = 5000; + +interface EngineOverviewApiData { + apiLogsUnavailable: boolean; + documentCount: number; + startDate: string; + endDate: string; + operationsPerDay: number[]; + queriesPerDay: number[]; + totalClicks: number; + totalQueries: number; +} +interface EngineOverviewValues extends EngineOverviewApiData { + dataLoading: boolean; + timeoutId: number | null; +} + +interface EngineOverviewActions { + setPolledData(engineMetrics: EngineOverviewApiData): EngineOverviewApiData; + setTimeoutId(timeoutId: number): { timeoutId: number }; + pollForOverviewMetrics(): void; + onPollingSuccess(engineMetrics: EngineOverviewApiData): EngineOverviewApiData; +} + +export const EngineOverviewLogic = kea>({ + path: ['enterprise_search', 'app_search', 'engine_overview_logic'], + actions: () => ({ + setPolledData: (engineMetrics) => engineMetrics, + setTimeoutId: (timeoutId) => ({ timeoutId }), + pollForOverviewMetrics: true, + onPollingSuccess: (engineMetrics) => engineMetrics, + }), + reducers: () => ({ + dataLoading: [ + true, + { + setPolledData: () => false, + }, + ], + apiLogsUnavailable: [ + false, + { + setPolledData: (_, { apiLogsUnavailable }) => apiLogsUnavailable, + }, + ], + startDate: [ + '', + { + setPolledData: (_, { startDate }) => startDate, + }, + ], + endDate: [ + '', + { + setPolledData: (_, { endDate }) => endDate, + }, + ], + queriesPerDay: [ + [], + { + setPolledData: (_, { queriesPerDay }) => queriesPerDay, + }, + ], + operationsPerDay: [ + [], + { + setPolledData: (_, { operationsPerDay }) => operationsPerDay, + }, + ], + totalQueries: [ + 0, + { + setPolledData: (_, { totalQueries }) => totalQueries, + }, + ], + totalClicks: [ + 0, + { + setPolledData: (_, { totalClicks }) => totalClicks, + }, + ], + documentCount: [ + 0, + { + setPolledData: (_, { documentCount }) => documentCount, + }, + ], + timeoutId: [ + null, + { + setTimeoutId: (_, { timeoutId }) => timeoutId, + }, + ], + }), + listeners: ({ actions }) => ({ + pollForOverviewMetrics: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const response = await http.get(`/api/app_search/engines/${engineName}/overview`); + actions.onPollingSuccess(response); + } catch (e) { + flashAPIErrors(e); + } + }, + onPollingSuccess: (engineMetrics) => { + const timeoutId = window.setTimeout(actions.pollForOverviewMetrics, POLLING_DURATION); + actions.setTimeoutId(timeoutId); + actions.setPolledData(engineMetrics); + }, + }), + events: ({ values }) => ({ + beforeUnmount() { + if (values.timeoutId !== null) clearTimeout(values.timeoutId); + }, + }), +});