From e54729caf2fcf6a1f917b27b195b049f62234d91 Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Thu, 9 Jul 2020 14:32:39 -0400 Subject: [PATCH 01/15] Dashboard add or update panel (#71130) Added a standard method for adding or replacing a panel on a dashboard. --- .../application/dashboard_app_controller.tsx | 5 +- .../embeddable/dashboard_container.tsx | 68 +++++++++++++------ 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index a321bc7959c5c8..8138e1c7f4dfdb 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -60,6 +60,7 @@ import { ViewMode, ContainerOutput, EmbeddableInput, + SavedObjectEmbeddableInput, } from '../../../embeddable/public'; import { NavAction, SavedDashboardPanel } from '../types'; @@ -431,7 +432,7 @@ export class DashboardAppController { .getIncomingEmbeddablePackage(); if (incomingState) { if ('id' in incomingState) { - container.addNewEmbeddable(incomingState.type, { + container.addOrUpdateEmbeddable(incomingState.type, { savedObjectId: incomingState.id, }); } else if ('input' in incomingState) { @@ -440,7 +441,7 @@ export class DashboardAppController { const explicitInput = { savedVis: input, }; - container.addNewEmbeddable(incomingState.type, explicitInput); + container.addOrUpdateEmbeddable(incomingState.type, explicitInput); } } } diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index f1ecd0f221926b..ff74580ba256be 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -46,7 +46,7 @@ import { } from '../../../../kibana_react/public'; import { PLACEHOLDER_EMBEDDABLE } from './placeholder'; import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement'; -import { EmbeddableStateTransfer } from '../../../../embeddable/public'; +import { EmbeddableStateTransfer, EmbeddableOutput } from '../../../../embeddable/public'; export interface DashboardContainerInput extends ContainerInput { viewMode: ViewMode; @@ -159,29 +159,55 @@ export class DashboardContainer extends Container) => { - const finalPanels = { ...this.input.panels }; - delete finalPanels[placeholderPanelState.explicitInput.id]; - const newPanelId = newPanelState.explicitInput?.id - ? newPanelState.explicitInput.id - : uuid.v4(); - finalPanels[newPanelId] = { - ...placeholderPanelState, - ...newPanelState, - gridData: { - ...placeholderPanelState.gridData, - i: newPanelId, - }, + newStateComplete.then((newPanelState: Partial) => + this.replacePanel(placeholderPanelState, newPanelState) + ); + } + + public replacePanel( + previousPanelState: DashboardPanelState, + newPanelState: Partial + ) { + // TODO: In the current infrastructure, embeddables in a container do not react properly to + // changes. Removing the existing embeddable, and adding a new one is a temporary workaround + // until the container logic is fixed. + const finalPanels = { ...this.input.panels }; + delete finalPanels[previousPanelState.explicitInput.id]; + const newPanelId = newPanelState.explicitInput?.id ? newPanelState.explicitInput.id : uuid.v4(); + finalPanels[newPanelId] = { + ...previousPanelState, + ...newPanelState, + gridData: { + ...previousPanelState.gridData, + i: newPanelId, + }, + explicitInput: { + ...newPanelState.explicitInput, + id: newPanelId, + }, + }; + this.updateInput({ + panels: finalPanels, + lastReloadRequestTime: new Date().getTime(), + }); + } + + public async addOrUpdateEmbeddable< + EEI extends EmbeddableInput = EmbeddableInput, + EEO extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable + >(type: string, explicitInput: Partial) { + if (explicitInput.id && this.input.panels[explicitInput.id]) { + this.replacePanel(this.input.panels[explicitInput.id], { + type, explicitInput: { - ...newPanelState.explicitInput, - id: newPanelId, + ...explicitInput, + id: uuid.v4(), }, - }; - this.updateInput({ - panels: finalPanels, - lastReloadRequestTime: new Date().getTime(), }); - }); + } else { + this.addNewEmbeddable(type, explicitInput); + } } public render(dom: HTMLElement) { From 4b4796ddbbc3a6aa02d1ed9967ca9f8eec28467b Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 9 Jul 2020 12:33:37 -0600 Subject: [PATCH 02/15] [SIEM][Detection Engine][Lists] Adds "wait_for" to all the create, update, patch, delete endpoints ## Summary * Adds "wait_for" to all the create, update, patch, and delete endpoints * Ran some quick tests against import and the performance still looks acceptable * Updates the unit tests to reflect the addition ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../lists/server/services/items/create_list_item.test.ts | 1 + x-pack/plugins/lists/server/services/items/create_list_item.ts | 1 + .../lists/server/services/items/create_list_items_bulk.test.ts | 2 ++ .../lists/server/services/items/create_list_items_bulk.ts | 1 + .../lists/server/services/items/delete_list_item.test.ts | 1 + x-pack/plugins/lists/server/services/items/delete_list_item.ts | 1 + .../server/services/items/delete_list_item_by_value.test.ts | 1 + .../lists/server/services/items/delete_list_item_by_value.ts | 1 + x-pack/plugins/lists/server/services/items/update_list_item.ts | 1 + x-pack/plugins/lists/server/services/lists/create_list.test.ts | 1 + x-pack/plugins/lists/server/services/lists/create_list.ts | 1 + x-pack/plugins/lists/server/services/lists/delete_list.test.ts | 2 ++ x-pack/plugins/lists/server/services/lists/delete_list.ts | 2 ++ x-pack/plugins/lists/server/services/lists/update_list.ts | 1 + 14 files changed, 17 insertions(+) diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index 7fbdc900fe2a4d..76bd47d217107e 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -36,6 +36,7 @@ describe('crete_list_item', () => { body, id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('index', expected); }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index 333f34946828a8..aa17fc00b25c66 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -71,6 +71,7 @@ export const createListItem = async ({ body, id, index: listItemIndex, + refresh: 'wait_for', }); return { diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index 4ab1bfb856846c..b2cc0da669e42d 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -33,6 +33,7 @@ describe('crete_list_item_bulk', () => { secondRecord, ], index: LIST_ITEM_INDEX, + refresh: 'wait_for', }); }); @@ -70,6 +71,7 @@ describe('crete_list_item_bulk', () => { }, ], index: '.items', + refresh: 'wait_for', }); }); }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 463b9735b25784..91e9587aa676a6 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -84,6 +84,7 @@ export const createListItemsBulk = async ({ await callCluster('bulk', { body, index: listItemIndex, + refresh: 'wait_for', }); } catch (error) { // TODO: Log out the error with return values from the bulk insert into another index or saved object diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts index ea338d9dd37917..b14bddb1268f86 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -47,6 +47,7 @@ describe('delete_list_item', () => { const deleteQuery = { id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('delete', deleteQuery); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index b006aed6f6dde3..baeced4b09995c 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -28,6 +28,7 @@ export const deleteListItem = async ({ await callCluster('delete', { id, index: listItemIndex, + refresh: 'wait_for', }); } return listItem; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index bf1608334ef24b..f658a51730d97f 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -52,6 +52,7 @@ describe('delete_list_item_by_value', () => { }, }, index: '.items', + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index 3551cb75dc5bcb..880402fca1bfa5 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -48,6 +48,7 @@ export const deleteListItemByValue = async ({ }, }, index: listItemIndex, + refresh: 'wait_for', }); return listItems; }; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index 24cd11cbb65e4d..eb20f1cfe3b305 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -62,6 +62,7 @@ export const updateListItem = async ({ }, id: listItem.id, index: listItemIndex, + refresh: 'wait_for', }); return { created_at: listItem.created_at, diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index 43af08bcaf7ffd..e328df710ebe10 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -52,6 +52,7 @@ describe('crete_list', () => { body, id: LIST_ID, index: LIST_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('index', expected); }); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index 3925fa5f0170c5..3d396cf4d5af9f 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -67,6 +67,7 @@ export const createList = async ({ body, id, index: listIndex, + refresh: 'wait_for', }); return { id: response._id, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index b9f1ec4d400be7..029b6226a7375d 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -47,6 +47,7 @@ describe('delete_list', () => { const deleteByQuery = { body: { query: { term: { list_id: LIST_ID } } }, index: LIST_ITEM_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); @@ -59,6 +60,7 @@ describe('delete_list', () => { const deleteQuery = { id: LIST_ID, index: LIST_INDEX, + refresh: 'wait_for', }; expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); }); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 64359b72732744..152048ca9cac6f 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -36,11 +36,13 @@ export const deleteList = async ({ }, }, index: listItemIndex, + refresh: 'wait_for', }); await callCluster('delete', { id, index: listIndex, + refresh: 'wait_for', }); return list; } diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index c7cc30aaae908c..f84ca787eaa7cd 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -55,6 +55,7 @@ export const updateList = async ({ body: { doc }, id, index: listIndex, + refresh: 'wait_for', }); return { created_at: list.created_at, From 733f33880055b1e3ce3926650459d787aebf8c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 9 Jul 2020 20:37:05 +0200 Subject: [PATCH 03/15] [ILM] Change "wait for snapshot" policy text field to EuiCombobox (#70627) * [ILM] Change "Wait for snapshot policy" text field to a dropdown in Delete phase * [ILM] Change "wait for snapshot" field to a EuiCombobox and update jest tests * [ILM] Update jest tests to check callouts * [ILM] Implement PR review suggestions * Update x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx Co-authored-by: Adam Locke * Update x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx Co-authored-by: Adam Locke * Update x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx Co-authored-by: Adam Locke * [ILM] Fix copy * [ILM] Fix copy * [ILM] Fix build error * [ILM] Delete periods in callout titles Co-authored-by: Elastic Machine Co-authored-by: Adam Locke --- .../edit_policy/constants.ts | 4 +- .../edit_policy/edit_policy.helpers.tsx | 28 +++- .../edit_policy/edit_policy.test.ts | 56 ++++++- .../helpers/http_requests.ts | 14 +- .../client_integration/helpers/index.ts | 7 +- .../components/delete_phase/delete_phase.js | 14 +- .../components/snapshot_policies/index.ts | 7 + .../snapshot_policies/snapshot_policies.tsx | 157 ++++++++++++++++++ .../application/services/{api.js => api.ts} | 40 +++-- .../public/application/services/http.ts | 12 +- .../routes/api/snapshot_policies/index.ts | 12 ++ .../snapshot_policies/register_fetch_route.ts | 42 +++++ .../server/routes/index.ts | 2 + 13 files changed, 355 insertions(+), 40 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx rename x-pack/plugins/index_lifecycle_management/public/application/services/{api.js => api.ts} (56%) create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 225432375dc757..e5037a6477aca4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -5,6 +5,8 @@ */ export const POLICY_NAME = 'my_policy'; +export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; +export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy'; export const DELETE_PHASE_POLICY = { version: 1, @@ -26,7 +28,7 @@ export const DELETE_PHASE_POLICY = { min_age: '0ms', actions: { wait_for_snapshot: { - policy: 'my_snapshot_policy', + policy: SNAPSHOT_POLICY_NAME, }, delete: { delete_searchable_snapshot: true, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index d6c955e0c08133..cba496ee0f2125 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; @@ -14,6 +15,25 @@ import { TestSubjects } from '../helpers'; import { EditPolicy } from '../../../public/application/sections/edit_policy'; import { indexLifecycleManagementStore } from '../../../public/application/store'; +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); + const testBedConfig: TestBedConfig = { store: () => indexLifecycleManagementStore(), memoryRouter: { @@ -34,9 +54,11 @@ export interface EditPolicyTestBed extends TestBed { export const setup = async (): Promise => { const testBed = await initTestBed(); - const setWaitForSnapshotPolicy = (snapshotPolicyName: string) => { - const { component, form } = testBed; - form.setInputValue('waitForSnapshotField', snapshotPolicyName, true); + const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => { + const { component } = testBed; + act(() => { + testBed.find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]); + }); component.update(); }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 8753f01376d42e..06829e6ef6f1e3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -7,11 +7,10 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../helpers/setup_environment'; - import { EditPolicyTestBed, setup } from './edit_policy.helpers'; -import { DELETE_PHASE_POLICY } from './constants'; import { API_BASE_PATH } from '../../../common/constants'; +import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME } from './constants'; window.scrollTo = jest.fn(); @@ -25,6 +24,10 @@ describe('', () => { describe('delete phase', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([ + SNAPSHOT_POLICY_NAME, + NEW_SNAPSHOT_POLICY_NAME, + ]); await act(async () => { testBed = await setup(); @@ -35,16 +38,18 @@ describe('', () => { }); test('wait for snapshot policy field should correctly display snapshot policy name', () => { - expect(testBed.find('waitForSnapshotField').props().value).toEqual( - DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy - ); + expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ + { + label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + value: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy, + }, + ]); }); test('wait for snapshot field should correctly update snapshot policy name', async () => { const { actions } = testBed; - const newPolicyName = 'my_new_snapshot_policy'; - actions.setWaitForSnapshotPolicy(newPolicyName); + await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME); await actions.savePolicy(); const expected = { @@ -56,7 +61,7 @@ describe('', () => { actions: { ...DELETE_PHASE_POLICY.policy.phases.delete.actions, wait_for_snapshot: { - policy: newPolicyName, + policy: NEW_SNAPSHOT_POLICY_NAME, }, }, }, @@ -69,6 +74,15 @@ describe('', () => { expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); + test('wait for snapshot field should display a callout when the input is not an existing policy', async () => { + const { actions } = testBed; + + await actions.setWaitForSnapshotPolicy('my_custom_policy'); + expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); + expect(testBed.find('customPolicyCallout').exists()).toBeTruthy(); + }); + test('wait for snapshot field should delete action if field is empty', async () => { const { actions } = testBed; @@ -92,5 +106,31 @@ describe('', () => { const latestRequest = server.requests[server.requests.length - 1]; expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); }); + + test('wait for snapshot field should display a callout when there are no snapshot policies', async () => { + // need to call setup on testBed again for it to use a newly defined snapshot policies response + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); + expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy(); + }); + + test('wait for snapshot field should display a callout when there is an error loading snapshot policies', async () => { + // need to call setup on testBed again for it to use a newly defined snapshot policies response + httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' }); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); + expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts index f41742fc104ff5..04f58f93939ca3 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SinonFakeServer, fakeServer } from 'sinon'; +import { fakeServer, SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; export const init = () => { @@ -27,7 +27,19 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadSnapshotPolicies = (response: any = [], error?: { status: number; body: any }) => { + const status = error ? error.status : 200; + const body = error ? error.body : response; + + server.respondWith('GET', `${API_BASE_PATH}/snapshot_policies`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + return { setLoadPolicies, + setLoadSnapshotPolicies, }; }; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 3cff2e3ab050f5..7b227f822fa97a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export type TestSubjects = 'waitForSnapshotField' | 'savePolicyButton'; +export type TestSubjects = + | 'snapshotPolicyCombobox' + | 'savePolicyButton' + | 'customPolicyCallout' + | 'noPoliciesCallout' + | 'policiesErrorCallout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js index 299bf28778ab43..34d1c0f8de2166 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js @@ -7,17 +7,12 @@ import React, { PureComponent, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiDescribedFormGroup, - EuiSwitch, - EuiFieldText, - EuiTextColor, - EuiFormRow, -} from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../../constants'; import { ActiveBadge, LearnMoreLink, OptionalLabel, PhaseErrorMessage } from '../../../components'; import { MinAgeInput } from '../min_age_input'; +import { SnapshotPolicies } from '../snapshot_policies'; export class DeletePhase extends PureComponent { static propTypes = { @@ -125,10 +120,9 @@ export class DeletePhase extends PureComponent { } > - setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, e.target.value)} + onChange={(value) => setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts new file mode 100644 index 00000000000000..f33ce81eb6157b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { SnapshotPolicies } from './snapshot_policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx new file mode 100644 index 00000000000000..76eae0f906d0c2 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx @@ -0,0 +1,157 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiComboBox, + EuiComboBoxOptionOption, + EuiSpacer, +} from '@elastic/eui'; + +import { useLoadSnapshotPolicies } from '../../../../services/api'; + +interface Props { + value: string; + onChange: (value: string) => void; +} +export const SnapshotPolicies: React.FunctionComponent = ({ value, onChange }) => { + const { error, isLoading, data, sendRequest } = useLoadSnapshotPolicies(); + + const policies = data.map((name: string) => ({ + label: name, + value: name, + })); + + const onComboChange = (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + onChange(options[0].label); + } else { + onChange(''); + } + }; + + const onCreateOption = (newValue: string) => { + onChange(newValue); + }; + + let calloutContent; + if (error) { + calloutContent = ( + + + + + + + + } + > + + + + ); + } else if (data.length === 0) { + calloutContent = ( + + + + } + > + + + + ); + } else if (value && !data.includes(value)) { + calloutContent = ( + + + + } + > + + + + ); + } + + return ( + + + {calloutContent} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts similarity index 56% rename from x-pack/plugins/index_lifecycle_management/public/application/services/api.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index 6b46d6e6ea7356..065fb3bcebca7b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { METRIC_TYPE } from '@kbn/analytics'; +import { trackUiMetric } from './ui_metric'; + import { UIM_POLICY_DELETE, UIM_POLICY_ATTACH_INDEX, @@ -12,14 +15,13 @@ import { UIM_INDEX_RETRY_STEP, } from '../constants'; -import { trackUiMetric } from './ui_metric'; -import { sendGet, sendPost, sendDelete } from './http'; +import { sendGet, sendPost, sendDelete, useRequest } from './http'; export async function loadNodes() { return await sendGet(`nodes/list`); } -export async function loadNodeDetails(selectedNodeAttrs) { +export async function loadNodeDetails(selectedNodeAttrs: string) { return await sendGet(`nodes/${selectedNodeAttrs}/details`); } @@ -27,45 +29,53 @@ export async function loadIndexTemplates() { return await sendGet(`templates`); } -export async function loadPolicies(withIndices) { +export async function loadPolicies(withIndices: boolean) { return await sendGet('policies', { withIndices }); } -export async function savePolicy(policy) { +export async function savePolicy(policy: any) { return await sendPost(`policies`, policy); } -export async function deletePolicy(policyName) { +export async function deletePolicy(policyName: string) { const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DELETE); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DELETE); return response; } -export const retryLifecycleForIndex = async (indexNames) => { +export const retryLifecycleForIndex = async (indexNames: string[]) => { const response = await sendPost(`index/retry`, { indexNames }); // Only track successful actions. - trackUiMetric('count', UIM_INDEX_RETRY_STEP); + trackUiMetric(METRIC_TYPE.COUNT, UIM_INDEX_RETRY_STEP); return response; }; -export const removeLifecycleForIndex = async (indexNames) => { +export const removeLifecycleForIndex = async (indexNames: string[]) => { const response = await sendPost(`index/remove`, { indexNames }); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_DETACH_INDEX); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DETACH_INDEX); return response; }; -export const addLifecyclePolicyToIndex = async (body) => { +export const addLifecyclePolicyToIndex = async (body: any) => { const response = await sendPost(`index/add`, body); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX); return response; }; -export const addLifecyclePolicyToTemplate = async (body) => { +export const addLifecyclePolicyToTemplate = async (body: any) => { const response = await sendPost(`template`, body); // Only track successful actions. - trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); + trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX_TEMPLATE); return response; }; + +export const useLoadSnapshotPolicies = () => { + return useRequest({ + path: `snapshot_policies`, + method: 'get', + initialData: [], + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts index 47e96ea28bb8cb..c54ee15fd69bf6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts @@ -4,6 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + UseRequestConfig, + useRequest as _useRequest, + Error, +} from '../../../../../../src/plugins/es_ui_shared/public'; + let _httpClient: any; export function init(httpClient: any): void { @@ -24,10 +30,14 @@ export function sendPost(path: string, payload: any): any { return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload) }); } -export function sendGet(path: string, query: any): any { +export function sendGet(path: string, query?: any): any { return _httpClient.get(getFullPath(path), { query }); } export function sendDelete(path: string): any { return _httpClient.delete(getFullPath(path)); } + +export const useRequest = (config: UseRequestConfig) => { + return _useRequest(_httpClient, { ...config, path: getFullPath(config.path) }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts new file mode 100644 index 00000000000000..19fbc45010ea2e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { registerFetchRoute } from './register_fetch_route'; + +export function registerSnapshotPoliciesRoutes(dependencies: RouteDependencies) { + registerFetchRoute(dependencies); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts new file mode 100644 index 00000000000000..7a52648e29ee8b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.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. + */ + +import { LegacyAPICaller } from 'src/core/server'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../../../services'; + +async function fetchSnapshotPolicies(callAsCurrentUser: LegacyAPICaller): Promise { + const params = { + method: 'GET', + path: '/_slm/policy', + }; + + return await callAsCurrentUser('transport.request', params); +} + +export function registerFetchRoute({ router, license, lib }: RouteDependencies) { + router.get( + { path: addBasePath('/snapshot_policies'), validate: false }, + license.guardApiRoute(async (context, request, response) => { + try { + const policiesByName = await fetchSnapshotPolicies( + context.core.elasticsearch.legacy.client.callAsCurrentUser + ); + return response.ok({ body: Object.keys(policiesByName) }); + } catch (e) { + if (lib.isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return response.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts index 35996721854c63..f7390debbe1773 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts @@ -10,10 +10,12 @@ import { registerIndexRoutes } from './api/index'; import { registerNodesRoutes } from './api/nodes'; import { registerPoliciesRoutes } from './api/policies'; import { registerTemplatesRoutes } from './api/templates'; +import { registerSnapshotPoliciesRoutes } from './api/snapshot_policies'; export function registerApiRoutes(dependencies: RouteDependencies) { registerIndexRoutes(dependencies); registerNodesRoutes(dependencies); registerPoliciesRoutes(dependencies); registerTemplatesRoutes(dependencies); + registerSnapshotPoliciesRoutes(dependencies); } From 0f09f6b140d271513d691f28eb1e29f6beac2590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 9 Jul 2020 19:41:36 +0100 Subject: [PATCH 04/15] [Observability] illustration for landing page (#71217) * changin illustration * renaming files Co-authored-by: Elastic Machine --- .../public/assets/illustration_dark.svg | 1 + .../public/assets/illustration_light.svg | 1 + .../public/assets/observability_overview.png | Bin 98273 -> 0 bytes .../public/pages/landing/index.tsx | 4 +++- 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/observability/public/assets/illustration_dark.svg create mode 100644 x-pack/plugins/observability/public/assets/illustration_light.svg delete mode 100644 x-pack/plugins/observability/public/assets/observability_overview.png diff --git a/x-pack/plugins/observability/public/assets/illustration_dark.svg b/x-pack/plugins/observability/public/assets/illustration_dark.svg new file mode 100644 index 00000000000000..44815a7455144d --- /dev/null +++ b/x-pack/plugins/observability/public/assets/illustration_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/assets/illustration_light.svg b/x-pack/plugins/observability/public/assets/illustration_light.svg new file mode 100644 index 00000000000000..1690c68fd595ab --- /dev/null +++ b/x-pack/plugins/observability/public/assets/illustration_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/observability/public/assets/observability_overview.png b/x-pack/plugins/observability/public/assets/observability_overview.png deleted file mode 100644 index 70be08af9745ad4d7ab427d9b3d4c1d2d2f1e75a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98273 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�s 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

{^Fs1wC!ys~qb#haBPfuxcrj$rVVBk_t#x* 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 NE2St|D{dJ4{{R5buN43Q diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index b6140956412508..512f4428d9bf2e 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -84,7 +84,9 @@ export const LandingPage = () => { size="xl" alt="observability overview image" url={core.http.basePath.prepend( - '/plugins/observability/assets/observability_overview.png' + `/plugins/observability/assets/illustration_${ + theme.darkMode ? 'dark' : 'light' + }.svg` )} /> From 69ff09ec387e73d48732b7e057a8703df282f45f Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 9 Jul 2020 14:55:48 -0400 Subject: [PATCH 05/15] Revert "[Ingest Manager ] prepend kibana asset ids with package name (#70502)" (#71271) This reverts commit 984ea0700ee8b84e69f626792e1dd913607307c9. --- .../services/epm/kibana/assets/install.ts | 119 ---------------- .../tests/__snapshots__/install.test.ts.snap | 133 ------------------ .../epm/kibana/assets/tests/dashboard.json | 129 ----------------- .../epm/kibana/assets/tests/install.test.ts | 35 ----- .../services/epm/packages/get_objects.ts | 32 +++++ .../server/services/epm/packages/index.ts | 2 +- .../server/services/epm/packages/install.ts | 58 +++++++- 7 files changed, 89 insertions(+), 419 deletions(-) delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts deleted file mode 100644 index ae6493d4716e81..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 { - SavedObject, - SavedObjectsBulkCreateObject, - SavedObjectsClientContract, -} from 'src/core/server'; -import * as Registry from '../../registry'; -import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; - -type SavedObjectToBe = Required & { type: AssetType }; -export type ArchiveAsset = Pick< - SavedObject, - 'id' | 'attributes' | 'migrationVersion' | 'references' -> & { - type: AssetType; -}; - -export async function getKibanaAsset(key: string) { - const buffer = Registry.getAsset(key); - - // cache values are buffers. convert to string / JSON - return JSON.parse(buffer.toString('utf8')); -} - -export function createSavedObjectKibanaAsset( - jsonAsset: ArchiveAsset, - pkgName: string -): SavedObjectToBe { - // convert that to an object - const asset = changeAssetIds(jsonAsset, pkgName); - - return { - type: asset.type, - id: asset.id, - attributes: asset.attributes, - references: asset.references || [], - migrationVersion: asset.migrationVersion || {}, - }; -} - -// modifies id property and the id property of references objects (not index-pattern) -// to be prepended with the package name to distinguish assets from Beats modules' assets -export const changeAssetIds = (asset: ArchiveAsset, pkgName: string): ArchiveAsset => { - const references = asset.references.map((ref) => { - if (ref.type === KibanaAssetType.indexPattern) return ref; - const id = getAssetId(ref.id, pkgName); - return { ...ref, id }; - }); - return { - ...asset, - id: getAssetId(asset.id, pkgName), - references, - }; -}; - -export const getAssetId = (id: string, pkgName: string) => { - return `${pkgName}-${id}`; -}; - -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case -export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - paths: string[]; -}) { - const { savedObjectsClient, paths, pkgName } = options; - - // Only install Kibana assets during package installation. - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map((assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths, pkgName }) - ); - - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array - return Promise.all(installationPromises).then((results) => results.flat()); -} - -async function installKibanaSavedObjects({ - savedObjectsClient, - assetType, - paths, - pkgName, -}: { - savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; - paths: string[]; - pkgName: string; -}) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); - const toBeSavedObjects = await Promise.all( - kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset, pkgName)) - ); - - if (toBeSavedObjects.length === 0) { - return []; - } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; - } -} - -function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; - - return reference; -} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap deleted file mode 100644 index 638ed4b6118c99..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`a kibana asset id and its reference ids are appended with package name changeAssetIds output matches snapshot: dashboard.json 1`] = ` -{ - "attributes": { - "description": "Overview dashboard for the Nginx integration in Metrics", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": { - "filter": [], - "highlightAll": true, - "query": { - "language": "kuery", - "query": "" - }, - "version": true - } - }, - "optionsJSON": { - "darkTheme": false, - "hidePanelTitles": false, - "useMargins": true - }, - "panelsJSON": [ - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "1", - "w": 24, - "x": 24, - "y": 0 - }, - "panelIndex": "1", - "panelRefName": "panel_0", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "2", - "w": 24, - "x": 24, - "y": 12 - }, - "panelIndex": "2", - "panelRefName": "panel_1", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "3", - "w": 24, - "x": 0, - "y": 12 - }, - "panelIndex": "3", - "panelRefName": "panel_2", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "4", - "w": 24, - "x": 0, - "y": 0 - }, - "panelIndex": "4", - "panelRefName": "panel_3", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "5", - "w": 48, - "x": 0, - "y": 24 - }, - "panelIndex": "5", - "panelRefName": "panel_4", - "version": "7.3.0" - } - ], - "timeRestore": false, - "title": "[Metrics Nginx] Overview ECS", - "version": 1 - }, - "id": "nginx-023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", - "migrationVersion": { - "dashboard": "7.3.0" - }, - "references": [ - { - "id": "metrics-*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern" - }, - { - "id": "nginx-555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_0", - "type": "search" - }, - { - "id": "nginx-a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_1", - "type": "map" - }, - { - "id": "nginx-d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_2", - "type": "dashboard" - }, - { - "id": "nginx-47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "nginx-dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_4", - "type": "visualization" - } - ], - "type": "dashboard" -} -`; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json deleted file mode 100644 index e28a61ae5e18c3..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "attributes": { - "description": "Overview dashboard for the Nginx integration in Metrics", - "hits": 0, - "kibanaSavedObjectMeta": { - "searchSourceJSON": { - "filter": [], - "highlightAll": true, - "query": { - "language": "kuery", - "query": "" - }, - "version": true - } - }, - "optionsJSON": { - "darkTheme": false, - "hidePanelTitles": false, - "useMargins": true - }, - "panelsJSON": [ - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "1", - "w": 24, - "x": 24, - "y": 0 - }, - "panelIndex": "1", - "panelRefName": "panel_0", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "2", - "w": 24, - "x": 24, - "y": 12 - }, - "panelIndex": "2", - "panelRefName": "panel_1", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "3", - "w": 24, - "x": 0, - "y": 12 - }, - "panelIndex": "3", - "panelRefName": "panel_2", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "4", - "w": 24, - "x": 0, - "y": 0 - }, - "panelIndex": "4", - "panelRefName": "panel_3", - "version": "7.3.0" - }, - { - "embeddableConfig": {}, - "gridData": { - "h": 12, - "i": "5", - "w": 48, - "x": 0, - "y": 24 - }, - "panelIndex": "5", - "panelRefName": "panel_4", - "version": "7.3.0" - } - ], - "timeRestore": false, - "title": "[Metrics Nginx] Overview ECS", - "version": 1 - }, - "id": "023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", - "migrationVersion": { - "dashboard": "7.3.0" - }, - "references": [ - { - "id": "metrics-*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern" - }, - { - "id": "555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_0", - "type": "search" - }, - { - "id": "a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_1", - "type": "map" - }, - { - "id": "d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_2", - "type": "dashboard" - }, - { - "id": "47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_3", - "type": "visualization" - }, - { - "id": "dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", - "name": "panel_4", - "type": "visualization" - } - ], - "type": "dashboard" -} \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts deleted file mode 100644 index f9bc4cdbf203fd..00000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { readFileSync } from 'fs'; -import path from 'path'; -import { getAssetId, changeAssetIds } from '../install'; - -expect.addSnapshotSerializer({ - print(val) { - return JSON.stringify(val, null, 2); - }, - - test(val) { - return val; - }, -}); - -describe('a kibana asset id and its reference ids are appended with package name', () => { - const assetPath = path.join(__dirname, './dashboard.json'); - const kibanaAsset = JSON.parse(readFileSync(assetPath, 'utf-8')); - const pkgName = 'nginx'; - const modifiedAssetObject = changeAssetIds(kibanaAsset, pkgName); - - test('changeAssetIds output matches snapshot', () => { - expect(modifiedAssetObject).toMatchSnapshot(path.basename(assetPath)); - }); - - test('getAssetId', () => { - const id = '47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs'; - expect(getAssetId(id, pkgName)).toBe(`${pkgName}-${id}`); - }); -}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts new file mode 100644 index 00000000000000..b623295c5e0604 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts @@ -0,0 +1,32 @@ +/* + * 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 { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; +import { AssetType } from '../../../types'; +import * as Registry from '../registry'; + +type ArchiveAsset = Pick; +type SavedObjectToBe = Required & { type: AssetType }; + +export async function getObject(key: string) { + const buffer = Registry.getAsset(key); + + // cache values are buffers. convert to string / JSON + const json = buffer.toString('utf8'); + // convert that to an object + const asset: ArchiveAsset = JSON.parse(json); + + const { type, file } = Registry.pathParts(key); + const savedObject: SavedObjectToBe = { + type, + id: file.replace('.json', ''), + attributes: asset.attributes, + references: asset.references || [], + migrationVersion: asset.migrationVersion || {}, + }; + + return savedObject; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 57c4f77432455d..4bb803dfaf9127 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -23,7 +23,7 @@ export { SearchParams, } from './get'; -export { installPackage, ensureInstalledPackage } from './install'; +export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 8f73bc9a027653..910283549abdfc 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, Installation, + KibanaAssetType, CallESAsCurrentUser, DefaultPackages, ElasticsearchAssetType, @@ -17,7 +18,7 @@ import { } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { installKibanaAssets } from '../kibana/assets/install'; +import { getObject } from './get_objects'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; @@ -120,6 +121,7 @@ export async function installPackage(options: { installKibanaAssets({ savedObjectsClient, pkgName, + pkgVersion, paths, }), installPipelines(registryPackageInfo, paths, callCluster), @@ -183,6 +185,27 @@ export async function installPackage(options: { }); } +// TODO: make it an exhaustive list +// e.g. switch statement with cases for each enum key returning `never` for default case +export async function installKibanaAssets(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + pkgVersion: string; + paths: string[]; +}) { + const { savedObjectsClient, paths } = options; + + // Only install Kibana assets during package installation. + const kibanaAssetTypes = Object.values(KibanaAssetType); + const installationPromises = kibanaAssetTypes.map(async (assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) + ); + + // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] + // call .flat to flatten into one dimensional array + return Promise.all(installationPromises).then((results) => results.flat()); +} + export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -217,3 +240,34 @@ export async function saveInstallationReferences(options: { return toSaveAssetRefs; } + +async function installKibanaSavedObjects({ + savedObjectsClient, + assetType, + paths, +}: { + savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; + paths: string[]; +}) { + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); + + if (toBeSavedObjects.length === 0) { + return []; + } else { + const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { + overwrite: true, + }); + const createdObjects = createResults.saved_objects; + const installed = createdObjects.map(toAssetReference); + return installed; + } +} + +function toAssetReference({ id, type }: SavedObject) { + const reference: AssetReference = { id, type: type as KibanaAssetType }; + + return reference; +} From 1b4804b986ba254ef5a73dbd10cfff490adb9c23 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 9 Jul 2020 20:57:05 +0200 Subject: [PATCH 06/15] [Uptime] Jest test adjust to use relative date (#70411) Co-authored-by: Elastic Machine --- .../__snapshots__/monitor_list.test.tsx.snap | 4 +- .../__tests__/monitor_list.test.tsx | 40 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 126b2eb9dfdf65..80726fd1ce7eec 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -1132,7 +1132,7 @@ exports[`MonitorList component renders the monitor list 1`] = `

@@ -1309,7 +1309,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
- 1896 Yr ago + 5m ago
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx index 3bba1e9e894c6d..2cd246bf7d5b6c 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx @@ -16,6 +16,7 @@ import { import { MonitorListComponent, noItemsMessage } from '../monitor_list'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; import * as redux from 'react-redux'; +import moment from 'moment'; const testFooPings: Ping[] = [ makePing({ @@ -96,11 +97,25 @@ const testBarSummary: MonitorSummary = { }, }; -// Failing: See https://github.com/elastic/kibana/issues/70386 -describe.skip('MonitorList component', () => { - let result: MonitorSummariesResult; +describe('MonitorList component', () => { let localStorageMock: any; + const getMonitorList = (timestamp?: string): MonitorSummariesResult => { + if (timestamp) { + testBarSummary.state.timestamp = timestamp; + testFooSummary.state.timestamp = timestamp; + } else { + testBarSummary.state.timestamp = '125'; + testFooSummary.state.timestamp = '123'; + } + return { + nextPagePagination: null, + prevPagePagination: null, + summaries: [testFooSummary, testBarSummary], + totalSummaryCount: 2, + }; + }; + beforeEach(() => { const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); useDispatchSpy.mockReturnValue(jest.fn()); @@ -113,20 +128,14 @@ describe.skip('MonitorList component', () => { setItem: jest.fn(), }; - // @ts-ignore replacing a call to localStorage we use for monitor list size + // @ts-expect-error replacing a call to localStorage we use for monitor list size global.localStorage = localStorageMock; - result = { - nextPagePagination: null, - prevPagePagination: null, - summaries: [testFooSummary, testBarSummary], - totalSummaryCount: 2, - }; }); it('shallow renders the monitor list', () => { const component = shallowWithRouter( @@ -157,7 +166,10 @@ describe.skip('MonitorList component', () => { it('renders the monitor list', () => { const component = renderWithRouter( @@ -169,7 +181,7 @@ describe.skip('MonitorList component', () => { it('renders error list', () => { const component = shallowWithRouter( @@ -181,7 +193,7 @@ describe.skip('MonitorList component', () => { it('renders loading state', () => { const component = shallowWithRouter( From 9bfdb1c523fbd95fbeeec34bcb6215e354f7fd9d Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Thu, 9 Jul 2020 15:25:44 -0400 Subject: [PATCH 07/15] [Uptime] Availability alert (#70284) * Add new fields to monitor status alert for availability. * Add UI code for availability threshold expression + translations/a11y fields. * Add availability selection to alert UI. * Disable expression popover button functionality when not enabled. * Add select box for monitor status alert, reorganize layout. * Add new runtime types for parsing in alert executor. * Add new enablement field for monitor status checks. * Add availability check query function and tests. Extract helper function from similar test file to generic file. * Add availability checking to status check alert type. * Change availability threshold to be number type. * Remove clearing of fields when disabled. * Change alert validation to require availability or status check. * Fix threshold input functionality. * Add tests and refine alert validation. * Add test for new validation logic. * Add any type temporarily. * Delete unused code, fix types. * Add filter capabilities to availability type. * Disable availability by default for old alerts. * Add filtering to availability query. * Clean up types and refresh test snapshots. * Change threshold storage value to string. Add bucket selector agg. * Update copy. * Add tests and improve should check flag evaluation. * Improve old alert detection code. * Fix issue with status check enablement. * Update unit tests to reflect changes to query. * Fix types. * Improve tests, refactor a function to clean up code. * Remove fields from aggregate key and retrieve them from top hits instead. * Add sort parameter to top_hits aggregation. * Update context message of monitor status alert, and add translations for availability message. * Modify default alert message. * Add a comment. * Fix outdated translations. * Revert unknown to any to simplify validation. * Improve readability of array manipulation for availability result description. * Add a flex item wrapper to fix layout problem. --- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- .../runtime_types/alerts/status_check.ts | 40 + .../__tests__/alert_monitor_status.test.tsx | 49 +- .../__tests__/old_alert_callout.test.tsx | 2 +- .../alerts/alert_expression_popover.tsx | 18 +- .../overview/alerts/alert_monitor_status.tsx | 51 +- .../alert_monitor_status.tsx | 10 +- .../down_number_select.test.tsx.snap | 6 +- .../time_expression_select.test.tsx.snap | 101 +-- .../availability_expression_select.tsx | 178 ++++ .../down_number_select.tsx | 3 + .../alerts/monitor_expressions/index.ts | 1 + .../status_expression_select.tsx | 58 ++ .../time_expression_select.tsx | 51 +- .../time_unit_selectable.tsx | 52 ++ .../monitor_expressions/translations.ts | 12 + .../overview/alerts/old_alert_call_out.tsx | 2 +- .../overview/alerts/translations.ts | 119 +++ .../__tests__/monitor_status.test.ts | 81 +- .../public/lib/alert_types/monitor_status.tsx | 25 +- .../public/lib/alert_types/translations.ts | 3 +- .../lib/alerts/__tests__/status_check.test.ts | 581 +++++++++++- .../uptime/server/lib/alerts/status_check.ts | 255 ++++-- .../get_monitor_availability.test.ts | 853 ++++++++++++++++++ .../__tests__/get_monitor_status.test.ts | 99 +- .../server/lib/requests/__tests__/helper.ts | 49 + .../lib/requests/get_monitor_availability.ts | 160 ++++ .../server/lib/requests/get_monitor_status.ts | 2 +- .../uptime/server/lib/requests/index.ts | 1 + .../server/lib/requests/uptime_requests.ts | 3 + 31 files changed, 2583 insertions(+), 286 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/availability_expression_select.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/status_expression_select.tsx create mode 100644 x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_unit_selectable.tsx create mode 100644 x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dee92c4fbad583..92285d8bf72f84 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16355,7 +16355,7 @@ "xpack.uptime.alerts.monitorStatus.addFilter.tag": "タグ", "xpack.uptime.alerts.monitorStatus.addFilter.type": "タイプ", "xpack.uptime.alerts.monitorStatus.clientName": "稼働状況の監視ステータス", - "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "{contextMessage}\n前回トリガー日時:{lastTriggered}\n{downMonitors}", + "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "{contextMessage}\n前回トリガー日時:{lastTriggered}\n", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "監視状態アラートのフィルター基準を許可するインプット", "xpack.uptime.alerts.monitorStatus.filters.anyLocation": "任意の場所", "xpack.uptime.alerts.monitorStatus.filters.anyPort": "任意のポート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ad3c699db03c82..457f65e89083d1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16362,7 +16362,7 @@ "xpack.uptime.alerts.monitorStatus.addFilter.tag": "标记", "xpack.uptime.alerts.monitorStatus.addFilter.type": "类型", "xpack.uptime.alerts.monitorStatus.clientName": "运行时间监测状态", - "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "{contextMessage}\n上次触发时间:{lastTriggered}\n{downMonitors}", + "xpack.uptime.alerts.monitorStatus.defaultActionMessage": "{contextMessage}\n上次触发时间:{lastTriggered}\n", "xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "允许对监测状态告警使用筛选条件的输入", "xpack.uptime.alerts.monitorStatus.filters.anyLocation": "任意位置", "xpack.uptime.alerts.monitorStatus.filters.anyPort": "任意端口", diff --git a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts index 74d53372566016..5a355dc576c0aa 100644 --- a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts +++ b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts @@ -24,6 +24,7 @@ export const AtomicStatusCheckParamsType = t.intersection([ t.partial({ search: t.string, filters: StatusCheckFiltersType, + shouldCheckStatus: t.boolean, }), ]); @@ -32,6 +33,7 @@ export type AtomicStatusCheckParams = t.TypeOf; + export type StatusCheckParams = t.TypeOf; + +export const GetMonitorAvailabilityParamsType = t.intersection([ + t.type({ + range: t.number, + rangeUnit: RangeUnitType, + threshold: t.string, + }), + t.partial({ + filters: t.string, + }), +]); + +export type GetMonitorAvailabilityParams = t.TypeOf; + +export const MonitorAvailabilityType = t.intersection([ + t.type({ + availability: GetMonitorAvailabilityParamsType, + shouldCheckAvailability: t.boolean, + }), + t.partial({ + filters: StatusCheckFiltersType, + search: t.string, + }), +]); + +export type MonitorAvailability = t.Type; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx index b955667ea74008..f3f3d583fd9382 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx @@ -59,21 +59,9 @@ describe('alert monitor status component', () => { - - - - { setAlertParams={[MockFunction]} shouldUpdateUrl={false} /> - + - + { size="s" title={ diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx index 00e8e451489855..0ae8c3a93da949 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_expression_popover.tsx @@ -12,15 +12,29 @@ interface AlertExpressionPopoverProps { content: React.ReactElement; description: string; 'data-test-subj': string; + isEnabled?: boolean; id: string; + isInvalid?: boolean; value: string; } +const getColor = ( + isOpen: boolean, + isEnabled?: boolean, + isInvalid?: boolean +): 'primary' | 'secondary' | 'subdued' | 'danger' => { + if (isInvalid === true) return 'danger'; + if (isEnabled === false) return 'subdued'; + return isOpen ? 'primary' : 'secondary'; +}; + export const AlertExpressionPopover: React.FC = ({ 'aria-label': ariaLabel, content, 'data-test-subj': dataTestSubj, description, + isEnabled, + isInvalid, id, value, }) => { @@ -32,11 +46,11 @@ export const AlertExpressionPopover: React.FC = ({ button={ setIsOpen(!isOpen)} + onClick={isEnabled ? () => setIsOpen(!isOpen) : undefined} value={value} /> } diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx index a1b4762627e7c3..b06b45f6fc9e78 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx @@ -5,17 +5,14 @@ */ import React, { useState } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer, EuiHorizontalRule } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { DataPublicPluginSetup } from 'src/plugins/data/public'; import * as labels from './translations'; -import { - DownNoExpressionSelect, - TimeExpressionSelect, - FiltersExpressionSelectContainer, -} from './monitor_expressions'; +import { FiltersExpressionSelectContainer, StatusExpressionSelect } from './monitor_expressions'; import { AddFilterButton } from './add_filter_btn'; import { OldAlertCallOut } from './old_alert_call_out'; +import { AvailabilityExpressionSelect } from './monitor_expressions/availability_expression_select'; import { KueryBar } from '..'; export interface AlertMonitorStatusProps { @@ -69,22 +66,14 @@ export const AlertMonitorStatusComponent: React.FC = (p - - - - - { + setNewFilters([...newFilters, newFilter]); + }} /> - - = (p shouldUpdateUrl={shouldUpdateUrl} /> - + - { - setNewFilters([...newFilters, newFilter]); - }} + - + + + + + = ({ }, [dispatch, esFilters]); const isOldAlert = React.useMemo( - () => !isRight(AtomicStatusCheckParamsType.decode(alertParams)), + () => + Object.entries(alertParams).length > 0 && + !isRight(AtomicStatusCheckParamsType.decode(alertParams)) && + !isRight(GetMonitorAvailabilityParamsType.decode(alertParams)), [alertParams] ); useEffect(() => { diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap index b761bc3e2368aa..bf56ebd0de2365 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/down_number_select.test.tsx.snap @@ -8,9 +8,9 @@ exports[`DownNoExpressionSelect component should renders against props 1`] = `
- +
`; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap index cbbaccbab34e4e..487d42cfdb6f2f 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/__tests__/__snapshots__/time_expression_select.test.tsx.snap @@ -14,9 +14,9 @@ exports[`TimeExpressionSelect component should renders against props 1`] = `
- +
@@ -44,9 +44,9 @@ exports[`TimeExpressionSelect component should renders against props 1`] = `
- +
@@ -93,62 +93,41 @@ exports[`TimeExpressionSelect component should shallow renders against props 1`] - -
- -
-
- - [Function] - - + "aria-label": "\\"Seconds\\" time range select item", + "data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption", + "key": "s", + "label": "seconds", + }, + Object { + "aria-label": "\\"Minutes\\" time range select item", + "checked": "on", + "data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption", + "key": "m", + "label": "minutes", + }, + Object { + "aria-label": "\\"Hours\\" time range select item", + "data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption", + "key": "h", + "label": "hours", + }, + Object { + "aria-label": "\\"Days\\" time range select item", + "data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption", + "key": "d", + "label": "days", + }, + ] + } + /> } data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" description="" diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/availability_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/availability_expression_select.tsx new file mode 100644 index 00000000000000..58a6bd910d669a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/availability_expression_select.tsx @@ -0,0 +1,178 @@ +/* + * 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 { EuiCheckbox, EuiFlexGroup, EuiFlexItem, EuiFieldText } from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { AlertExpressionPopover } from '../alert_expression_popover'; +import * as labels from '../translations'; +import { AlertFieldNumber } from '../alert_field_number'; +import { TimeRangeOption, TimeUnitSelectable } from './time_unit_selectable'; + +interface Props { + alertParams: { [param: string]: any }; + isOldAlert: boolean; + setAlertParams: (key: string, value: any) => void; +} + +const TimeRangeOptions: TimeRangeOption[] = [ + { + 'aria-label': labels.DAYS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.availability.timerangeUnit.daysOption', + key: 'd', + label: labels.DAYS, + }, + { + 'aria-label': labels.WEEKS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.availability.timerangeUnit.weeksOption', + key: 'w', + label: labels.WEEKS, + }, + { + 'aria-label': labels.MONTHS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.availability.timerangeUnit.monthsOption', + key: 'M', + label: labels.MONTHS, + }, + { + 'aria-label': labels.YEARS_TIME_RANGE, + 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.availability.timerangeUnit.yearsOption', + key: 'y', + label: labels.YEARS, + }, +]; + +const DEFAULT_RANGE = 30; +const DEFAULT_TIMERANGE_UNIT = 'd'; +const DEFAULT_THRESHOLD = '99'; + +const isThresholdInvalid = (n: number): boolean => isNaN(n) || n <= 0 || n > 100; + +export const AvailabilityExpressionSelect: React.FC = ({ + alertParams, + isOldAlert, + setAlertParams, +}) => { + const [range, setRange] = useState(alertParams?.availability?.range ?? DEFAULT_RANGE); + const [rangeUnit, setRangeUnit] = useState( + alertParams?.availability?.rangeUnit ?? DEFAULT_TIMERANGE_UNIT + ); + const [threshold, setThreshold] = useState( + alertParams?.availability?.threshold ?? DEFAULT_THRESHOLD + ); + const [isEnabled, setIsEnabled] = useState( + // if an older version of alert is displayed, this expression should default to disabled + alertParams?.shouldCheckAvailability ?? !isOldAlert + ); + const [timerangeUnitOptions, setTimerangeUnitOptions] = useState( + TimeRangeOptions.map((opt) => + opt.key === DEFAULT_TIMERANGE_UNIT ? { ...opt, checked: 'on' } : opt + ) + ); + + const thresholdIsInvalid = isThresholdInvalid(Number(threshold)); + + useEffect(() => { + if (thresholdIsInvalid) { + setAlertParams('availability', undefined); + setAlertParams('shouldCheckAvailability', false); + } else if (isEnabled) { + setAlertParams('shouldCheckAvailability', true); + setAlertParams('availability', { + range, + rangeUnit, + threshold, + }); + } else { + setAlertParams('shouldCheckAvailability', false); + } + }, [isEnabled, range, rangeUnit, setAlertParams, threshold, thresholdIsInvalid]); + + return ( + + + setIsEnabled(!isEnabled)} + /> + + + { + setThreshold(e.target.value); + }} + /> + } + data-test-subj="xpack.uptime.alerts.monitorStatus.availability.threshold" + description={labels.ENTER_AVAILABILITY_THRESHOLD_DESCRIPTION} + id="threshold" + isEnabled={isEnabled} + isInvalid={thresholdIsInvalid} + value={labels.ENTER_AVAILABILITY_THRESHOLD_VALUE(threshold)} + /> + + + + + + } + data-test-subj="xpack.uptime.alerts.monitorStatus.availability.timerangeExpression" + description={labels.ENTER_AVAILABILITY_RANGE_UNITS_DESCRIPTION} + id="range" + isEnabled={isEnabled} + value={`${range}`} + /> + + + { + // TODO: this should not be `any` + const checkedOption = newOptions.find(({ checked }: any) => checked === 'on'); + if (checkedOption) { + setTimerangeUnitOptions(newOptions); + setRangeUnit(checkedOption.key); + } + }} + timeRangeOptions={timerangeUnitOptions} + /> + } + data-test-subj="xpack.uptime.alerts.monitorStatus.availability.timerangeUnit" + description="" + id="availability-unit" + isEnabled={isEnabled} + value={ + timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? + '' + } + /> + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx index 0eb53eb044bc50..986d55cde74632 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/down_number_select.tsx @@ -10,6 +10,7 @@ import * as labels from '../translations'; import { AlertFieldNumber } from '../alert_field_number'; interface Props { + isEnabled?: boolean; defaultNumTimes?: number; hasFilters: boolean; setAlertParams: (key: string, value: any) => void; @@ -18,6 +19,7 @@ interface Props { export const DownNoExpressionSelect: React.FC = ({ defaultNumTimes, hasFilters, + isEnabled, setAlertParams, }) => { const [numTimes, setNumTimes] = useState(defaultNumTimes ?? 5); @@ -41,6 +43,7 @@ export const DownNoExpressionSelect: React.FC = ({ data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression" description={hasFilters ? labels.MATCHING_MONITORS_DOWN : labels.ANY_MONITOR_DOWN} id="ping-count" + isEnabled={isEnabled} value={`${numTimes} times`} /> ); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts index e6f47e744f5eaf..637d102df85d57 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/index.ts @@ -8,3 +8,4 @@ export { DownNoExpressionSelect } from './down_number_select'; export { FiltersExpressionsSelect } from './filters_expression_select'; export { FiltersExpressionSelectContainer } from './filters_expression_select_container'; export { TimeExpressionSelect } from './time_expression_select'; +export { StatusExpressionSelect } from './status_expression_select'; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/status_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/status_expression_select.tsx new file mode 100644 index 00000000000000..15c7d7a2ef32a1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/status_expression_select.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { DownNoExpressionSelect } from './down_number_select'; +import { TimeExpressionSelect } from './time_expression_select'; +import { statusExpLabels } from './translations'; + +interface Props { + alertParams: { [param: string]: any }; + hasFilters: boolean; + setAlertParams: (key: string, value: any) => void; +} + +export const StatusExpressionSelect: React.FC = ({ + alertParams, + hasFilters, + setAlertParams, +}) => { + const [isEnabled, setIsEnabled] = useState(alertParams.shouldCheckStatus ?? true); + + useEffect(() => { + setAlertParams('shouldCheckStatus', isEnabled); + }, [isEnabled, setAlertParams]); + + return ( + + + setIsEnabled(!isEnabled)} + /> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx index 44bfbff6817c44..48593e15be0178 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_expression_select.tsx @@ -5,22 +5,23 @@ */ import React, { useEffect, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { AlertExpressionPopover } from '../alert_expression_popover'; import * as labels from '../translations'; import { AlertFieldNumber } from '../alert_field_number'; import { timeExpLabels } from './translations'; +import { TimeUnitSelectable, TimeRangeOption } from './time_unit_selectable'; interface Props { defaultTimerangeCount?: number; defaultTimerangeUnit?: string; + isEnabled?: boolean; setAlertParams: (key: string, value: any) => void; } const DEFAULT_TIMERANGE_UNIT = 'm'; -const TimeRangeOptions = [ +const TimeRangeOptions: TimeRangeOption[] = [ { 'aria-label': labels.SECONDS_TIME_RANGE, 'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption', @@ -50,6 +51,7 @@ const TimeRangeOptions = [ export const TimeExpressionSelect: React.FC = ({ defaultTimerangeCount, defaultTimerangeUnit, + isEnabled, setAlertParams, }) => { const [numUnits, setNumUnits] = useState(defaultTimerangeCount ?? 15); @@ -81,45 +83,32 @@ export const TimeExpressionSelect: React.FC = ({ /> } data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression" - description="within" + description={labels.ENTER_NUMBER_OF_TIME_UNITS_DESCRIPTION} id="timerange" - value={`last ${numUnits}`} + isEnabled={isEnabled} + value={labels.ENTER_NUMBER_OF_TIME_UNITS_VALUE(numUnits)} /> - -
- -
-
- { - if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) { - setTimerangeUnitOptions(newOptions); - } - }} - singleSelection={true} - listProps={{ - showIcons: true, - }} - > - {(list) => list} - - + >) => { + if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) { + setTimerangeUnitOptions(newOptions); + } + }} + timeRangeOptions={timerangeUnitOptions} + /> } data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression" description="" id="timerange-unit" + isEnabled={isEnabled} value={ timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? '' } diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_unit_selectable.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_unit_selectable.tsx new file mode 100644 index 00000000000000..ed5842f9d36999 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/time_unit_selectable.tsx @@ -0,0 +1,52 @@ +/* + * 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 React from 'react'; +import { EuiTitle, EuiSelectable } from '@elastic/eui'; + +export interface TimeRangeOption { + 'aria-label': string; + 'data-test-subj': string; + key: string; + label: string; + checked?: 'on' | 'off'; +} + +interface Props { + 'aria-label': string; + 'data-test-subj': string; + headlineText: string; + onChange: (newOptions: Array>) => void; + timeRangeOptions: TimeRangeOption[]; +} + +export const TimeUnitSelectable: React.FC = ({ + 'aria-label': ariaLabel, + 'data-test-subj': dataTestSubj, + headlineText: headlineText, + onChange, + timeRangeOptions, +}) => { + return ( + <> + +
{headlineText}
+
+ + {(list) => list} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts index 5fefc9f3ae35b5..64c082dc513341 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/translations.ts @@ -56,6 +56,12 @@ export const alertFilterLabels = { }), }; +export const statusExpLabels = { + ENABLED_CHECKBOX: i18n.translate('xpack.uptime.alerts.monitorStatus.statusEnabledCheck.label', { + defaultMessage: 'Status check', + }), +}; + export const timeExpLabels = { OPEN_TIME_POPOVER: i18n.translate( 'xpack.uptime.alerts.monitorStatus.timerangeUnitExpression.ariaLabel', @@ -69,4 +75,10 @@ export const timeExpLabels = { defaultMessage: 'Selectable field for the time range units alerts should use', } ), + SELECT_TIME_RANGE_HEADLINE: i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeSelectionHeader', + { + defaultMessage: 'Select time range unit', + } + ), }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/old_alert_call_out.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/old_alert_call_out.tsx index eba66f7bfd5708..de9a7bae1d6702 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/old_alert_call_out.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/old_alert_call_out.tsx @@ -23,7 +23,7 @@ export const OldAlertCallOut: React.FC = ({ isOldAlert }) => { title={ } iconType="alert" diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts index 637fe0a108958b..b2f35ccc2c2017 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts +++ b/x-pack/plugins/uptime/public/components/overview/alerts/translations.ts @@ -50,6 +50,39 @@ export const DAYS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeO defaultMessage: 'days', }); +export const WEEKS_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.weeksOption.ariaLabel', + { + defaultMessage: '"Weeks" time range select item', + } +); + +export const WEEKS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.weeks', { + defaultMessage: 'weeks', +}); + +export const MONTHS_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.monthsOption.ariaLabel', + { + defaultMessage: '"Months" time range select item', + } +); + +export const MONTHS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.months', { + defaultMessage: 'months', +}); + +export const YEARS_TIME_RANGE = i18n.translate( + 'xpack.uptime.alerts.timerangeUnitSelectable.yearsOption.ariaLabel', + { + defaultMessage: '"Years" time range select item', + } +); + +export const YEARS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.years', { + defaultMessage: 'years', +}); + export const ALERT_KUERY_BAR_ARIA = i18n.translate( 'xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel', { @@ -99,6 +132,92 @@ export const ENTER_NUMBER_OF_TIME_UNITS = i18n.translate( } ); +export const ENTER_NUMBER_OF_TIME_UNITS_DESCRIPTION = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.timerangeValueField.expression', + { + defaultMessage: 'within', + } +); + +export const ENTER_NUMBER_OF_TIME_UNITS_VALUE = (value: number) => + i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeValueField.value', { + defaultMessage: 'last {value}', + values: { value }, + }); + +export const ENTER_AVAILABILITY_RANGE_ENABLED = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.isEnabledCheckbox.label', + { + defaultMessage: 'Availability', + } +); + +export const ENTER_AVAILABILITY_RANGE_POPOVER_ARIA_LABEL = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.timerangeValueField.popover.ariaLabel', + { + defaultMessage: 'Specify availability tracking time range', + } +); + +export const ENTER_AVAILABILITY_RANGE_UNITS_ARIA_LABEL = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.timerangeValueField.ariaLabel', + { + defaultMessage: `Enter the number of units for the alert's availability check.`, + } +); + +export const ENTER_AVAILABILITY_RANGE_UNITS_DESCRIPTION = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.timerangeValueField.expression', + { + defaultMessage: 'within the last', + } +); + +export const ENTER_AVAILABILITY_THRESHOLD_ARIA_LABEL = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.threshold.ariaLabel', + { + defaultMessage: 'Specify availability thresholds for this alert', + } +); + +export const ENTER_AVAILABILITY_THRESHOLD_INPUT_ARIA_LABEL = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.threshold.input.ariaLabel', + { + defaultMessage: 'Input an availability threshold to check for this alert', + } +); + +export const ENTER_AVAILABILITY_THRESHOLD_DESCRIPTION = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.threshold.description', + { + defaultMessage: 'matching monitors are up in', + description: + 'This fragment explains that an alert will fire for monitors matching user-specified criteria', + } +); + +export const ENTER_AVAILABILITY_THRESHOLD_VALUE = (value: string) => + i18n.translate('xpack.uptime.alerts.monitorStatus.availability.threshold.value', { + defaultMessage: '< {value}% of checks', + description: + 'This fragment specifies criteria that will cause an alert to fire for uptime monitors', + values: { value }, + }); + +export const ENTER_AVAILABILITY_RANGE_SELECT_ARIA = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.unit.selectable', + { + defaultMessage: 'Use this select to set the availability range units for this alert', + } +); + +export const ENTER_AVAILABILITY_RANGE_SELECT_HEADLINE = i18n.translate( + 'xpack.uptime.alerts.monitorStatus.availability.unit.headline', + { + defaultMessage: 'Select time range unit', + } +); + export const ADD_FILTER = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter', { defaultMessage: `Add filter`, }); diff --git a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts index 7ca5e7438d28a6..cfcb414f4815df 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts @@ -13,6 +13,7 @@ describe('monitor status alert type', () => { beforeEach(() => { params = { numTimes: 5, + shouldCheckStatus: true, timerangeCount: 15, timerangeUnit: 'm', }; @@ -24,9 +25,9 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", ], }, } @@ -43,6 +44,7 @@ describe('monitor status alert type', () => { to: 'now', }, filters: '{foo: "bar"}', + shouldCheckStatus: true, }) ).toMatchInlineSnapshot(` Object { @@ -51,6 +53,73 @@ describe('monitor status alert type', () => { `); }); + describe('should check flags', () => { + it('does not pass without one or more should check flags', () => { + params.shouldCheckStatus = false; + expect(validate(params)).toMatchInlineSnapshot(` + Object { + "errors": Object { + "noAlertSelected": "Alert must check for monitor status or monitor availability.", + }, + } + `); + }); + + it('does not pass when availability is defined, but both check flags are false', () => { + params.shouldCheckStatus = false; + params.shouldCheckAvailability = false; + params.availability = { + range: 3, + rangeUnit: 'w', + threshold: 98.3, + }; + expect(validate(params)).toMatchInlineSnapshot(` + Object { + "errors": Object { + "noAlertSelected": "Alert must check for monitor status or monitor availability.", + }, + } + `); + }); + + it('passes when status check is defined and flag is set to true', () => { + params.shouldCheckStatus = false; + params.shouldCheckAvailability = true; + params.availability = { + range: 3, + rangeUnit: 'w', + threshold: 98.3, + }; + expect(validate(params)).toMatchInlineSnapshot(` + Object { + "errors": Object {}, + } + `); + }); + + it('passes when status check and availability check flags are both true', () => { + params.shouldCheckAvailability = true; + params.availability = { + range: 3, + rangeUnit: 'w', + threshold: 98.3, + }; + expect(validate(params)).toMatchInlineSnapshot(` + Object { + "errors": Object {}, + } + `); + }); + + it('passes when availability check is defined and flag is set to true', () => { + expect(validate(params)).toMatchInlineSnapshot(` + Object { + "errors": Object {}, + } + `); + }); + }); + describe('timerange', () => { it('has invalid timerangeCount value', () => { expect(validate({ ...params, timerangeCount: 0 })).toMatchInlineSnapshot(` @@ -81,7 +150,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } @@ -94,7 +163,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array } }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } @@ -134,7 +203,7 @@ describe('monitor status alert type', () => { "alertParamsExpression": [Function], "defaultActionMessage": "{{context.message}} Last triggered at: {{state.lastTriggeredAt}} - {{context.downMonitorsWithGeo}}", + ", "iconClass": "uptimeApp", "id": "xpack.uptime.alerts.monitorStatus", "name": { +export const validate = (alertParams: any) => { const errors: Record = {}; const decoded = AtomicStatusCheckParamsType.decode(alertParams); const oldDecoded = StatusCheckParamsType.decode(alertParams); + const availabilityDecoded = MonitorAvailabilityType.decode(alertParams); - if (!isRight(decoded) && !isRight(oldDecoded)) { + if (!isRight(decoded) && !isRight(oldDecoded) && !isRight(availabilityDecoded)) { return { errors: { typeCheckFailure: 'Provided parameters do not conform to the expected type.', @@ -30,7 +35,19 @@ export const validate = (alertParams: unknown) => { }, }; } - if (isRight(decoded)) { + + if ( + !(alertParams.shouldCheckAvailability ?? false) && + !(alertParams.shouldCheckStatus ?? false) + ) { + return { + errors: { + noAlertSelected: 'Alert must check for monitor status or monitor availability.', + }, + }; + } + + if (isRight(decoded) && decoded.right.shouldCheckStatus) { const { numTimes, timerangeCount } = decoded.right; if (numTimes < 1) { errors.invalidNumTimes = 'Number of alert check down times must be an integer greater than 0'; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts index cdf3cd107b00f9..11fa70bc56f4a1 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/translations.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/translations.ts @@ -8,11 +8,10 @@ import { i18n } from '@kbn/i18n'; export const MonitorStatusTranslations = { defaultActionMessage: i18n.translate('xpack.uptime.alerts.monitorStatus.defaultActionMessage', { - defaultMessage: '{contextMessage}\nLast triggered at: {lastTriggered}\n{downMonitors}', + defaultMessage: '{contextMessage}\nLast triggered at: {lastTriggered}\n', values: { contextMessage: '{{context.message}}', lastTriggered: '{{state.lastTriggeredAt}}', - downMonitors: '{{context.downMonitorsWithGeo}}', }, }), name: i18n.translate('xpack.uptime.alerts.monitorStatus.clientName', { diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 6cd836525c0775..d85752768b47bb 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -7,10 +7,11 @@ import { contextMessage, fullListByIdAndLocation, - genFilterString, + generateFilterDSL, hasFilters, statusCheckAlertFactory, uniqueMonitorIds, + availabilityMessage, } from '../status_check'; import { GetMonitorStatusResult } from '../../requests'; import { AlertType } from '../../../../../alerts/server'; @@ -45,7 +46,12 @@ const bootstrapDependencies = (customRequests?: any) => { * @param state the state the alert maintains */ const mockOptions = ( - params = { numTimes: 5, locations: [], timerange: { from: 'now-15m', to: 'now' } }, + params: any = { + numTimes: 5, + locations: [], + timerange: { from: 'now-15m', to: 'now' }, + shouldCheckStatus: true, + }, services = alertsMock.createAlertServices(), state = {} ): any => { @@ -95,6 +101,7 @@ describe('status check alert', () => { }, "locations": Array [], "numTimes": 5, + "shouldCheckStatus": true, "timerange": Object { "from": "now-15m", "to": "now", @@ -140,6 +147,7 @@ describe('status check alert', () => { }, "locations": Array [], "numTimes": 5, + "shouldCheckStatus": true, "timerange": Object { "from": "now-15m", "to": "now", @@ -187,6 +195,443 @@ describe('status check alert', () => { ] `); }); + + it('supports 7.7 alert format', async () => { + toISOStringSpy.mockImplementation(() => '7.7 date'); + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([ + { + monitor_id: 'first', + location: 'harrisburg', + count: 234, + status: 'down', + }, + { + monitor_id: 'first', + location: 'fairbanks', + count: 234, + status: 'down', + }, + ]); + const { server, libs } = bootstrapDependencies({ + getMonitorStatus: mockGetter, + getIndexPattern: jest.fn(), + }); + const alert = statusCheckAlertFactory(server, libs); + const options = mockOptions({ + numTimes: 4, + timerange: { from: 'now-14h', to: 'now' }, + locations: ['fairbanks'], + filters: '', + }); + const alertServices: AlertServicesMock = options.services; + const state = await alert.executor(options); + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "currentTriggerStarted": "7.7 date", + "firstCheckedAt": "7.7 date", + "firstTriggeredAt": "7.7 date", + "isTriggered": true, + "lastCheckedAt": "7.7 date", + "lastResolvedAt": undefined, + "lastTriggeredAt": "7.7 date", + "monitors": Array [ + Object { + "count": 234, + "location": "fairbanks", + "monitor_id": "first", + "status": "down", + }, + Object { + "count": 234, + "location": "harrisburg", + "monitor_id": "first", + "status": "down", + }, + ], + }, + ] + `); + expect(state).toMatchInlineSnapshot(` + Object { + "currentTriggerStarted": "7.7 date", + "firstCheckedAt": "7.7 date", + "firstTriggeredAt": "7.7 date", + "isTriggered": true, + "lastCheckedAt": "7.7 date", + "lastResolvedAt": undefined, + "lastTriggeredAt": "7.7 date", + } + `); + }); + + it('supports 7.8 alert format', async () => { + expect.assertions(5); + toISOStringSpy.mockImplementation(() => 'foo date string'); + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([ + { + monitor_id: 'first', + location: 'harrisburg', + count: 234, + status: 'down', + }, + { + monitor_id: 'first', + location: 'fairbanks', + count: 234, + status: 'down', + }, + ]); + const { server, libs } = bootstrapDependencies({ + getMonitorStatus: mockGetter, + getIndexPattern: jest.fn(), + }); + const alert = statusCheckAlertFactory(server, libs); + const options = mockOptions({ + numTimes: 3, + timerangeUnit: 'm', + timerangeCount: 15, + search: 'monitor.ip : * ', + filters: { + 'url.port': ['12349', '5601', '443'], + 'observer.geo.name': ['harrisburg'], + 'monitor.type': ['http'], + tags: ['unsecured', 'containers', 'org:google'], + }, + }); + const alertServices: AlertServicesMock = options.services; + const state = await alert.executor(options); + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "callES": [MockFunction], + "dynamicSettings": Object { + "certAgeThreshold": 730, + "certExpirationThreshold": 30, + "heartbeatIndices": "heartbeat-8*", + }, + "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":12349}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":5601}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":443}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}]}}]}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"monitor.ip\\"}}],\\"minimum_should_match\\":1}}]}}", + "locations": Array [], + "numTimes": 3, + "timerange": Object { + "from": "now-15m", + "to": "now", + }, + }, + ] + `); + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "currentTriggerStarted": "foo date string", + "firstCheckedAt": "foo date string", + "firstTriggeredAt": "foo date string", + "isTriggered": true, + "lastCheckedAt": "foo date string", + "lastResolvedAt": undefined, + "lastTriggeredAt": "foo date string", + "monitors": Array [ + Object { + "count": 234, + "location": "fairbanks", + "monitor_id": "first", + "status": "down", + }, + Object { + "count": 234, + "location": "harrisburg", + "monitor_id": "first", + "status": "down", + }, + ], + }, + ] + `); + expect(state).toMatchInlineSnapshot(` + Object { + "currentTriggerStarted": "foo date string", + "firstCheckedAt": "foo date string", + "firstTriggeredAt": "foo date string", + "isTriggered": true, + "lastCheckedAt": "foo date string", + "lastResolvedAt": undefined, + "lastTriggeredAt": "foo date string", + } + `); + }); + + it('supports searches', async () => { + toISOStringSpy.mockImplementation(() => 'search test'); + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([]); + const { server, libs } = bootstrapDependencies({ + getIndexPattern: jest.fn(), + getMonitorStatus: mockGetter, + }); + const alert = statusCheckAlertFactory(server, libs); + const options = mockOptions({ + numTimes: 20, + timerangeCount: 30, + timerangeUnit: 'h', + filters: { + 'monitor.type': ['http'], + 'observer.geo.name': [], + tags: [], + 'url.port': [], + }, + search: 'url.full: *', + }); + await alert.executor(options); + expect(mockGetter).toHaveBeenCalledTimes(1); + expect(mockGetter.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "callES": [MockFunction], + "dynamicSettings": Object { + "certAgeThreshold": 730, + "certExpirationThreshold": 30, + "heartbeatIndices": "heartbeat-8*", + }, + "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"url.full\\"}}],\\"minimum_should_match\\":1}}]}}", + "locations": Array [], + "numTimes": 20, + "timerange": Object { + "from": "now-30h", + "to": "now", + }, + }, + ] + `); + }); + + it('supports availability checks', async () => { + expect.assertions(8); + toISOStringSpy.mockImplementation(() => 'availability test'); + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([ + { + monitor_id: 'first', + location: 'harrisburg', + count: 234, + status: 'down', + }, + { + monitor_id: 'first', + location: 'fairbanks', + count: 234, + status: 'down', + }, + ]); + const mockAvailability = jest.fn(); + mockAvailability.mockReturnValue([ + { + monitorId: 'foo', + location: 'harrisburg', + name: 'Foo', + url: 'https://foo.com', + up: 2341, + down: 17, + availabilityRatio: 0.992790500424088, + }, + { + monitorId: 'foo', + location: 'fairbanks', + name: 'Foo', + url: 'https://foo.com', + up: 2343, + down: 47, + availabilityRatio: 0.980334728033473, + }, + { + monitorId: 'unreliable', + location: 'fairbanks', + name: 'Unreliable', + url: 'https://unreliable.co', + up: 2134, + down: 213, + availabilityRatio: 0.909245845760545, + }, + { + monitorId: 'no-name', + location: 'fairbanks', + url: 'https://no-name.co', + up: 2134, + down: 213, + availabilityRatio: 0.909245845760545, + }, + ]); + const { server, libs } = bootstrapDependencies({ + getMonitorAvailability: mockAvailability, + getMonitorStatus: mockGetter, + getIndexPattern: jest.fn(), + }); + const alert = statusCheckAlertFactory(server, libs); + const options = mockOptions({ + availability: { + range: 35, + rangeUnit: 'd', + threshold: '99.34', + }, + filters: { + 'url.port': ['12349', '5601', '443'], + 'observer.geo.name': ['harrisburg'], + 'monitor.type': ['http'], + tags: ['unsecured', 'containers', 'org:google'], + }, + shouldCheckAvailability: true, + }); + const alertServices: AlertServicesMock = options.services; + const state = await alert.executor(options); + const [{ value: alertInstanceMock }] = alertServices.alertInstanceFactory.mock.results; + expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.replaceState.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "currentTriggerStarted": "availability test", + "firstCheckedAt": "availability test", + "firstTriggeredAt": "availability test", + "isTriggered": true, + "lastCheckedAt": "availability test", + "lastResolvedAt": undefined, + "lastTriggeredAt": "availability test", + "monitors": Array [], + }, + ] + `); + expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(1); + expect(alertInstanceMock.scheduleActions.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "xpack.uptime.alerts.actionGroups.monitorStatus", + Object { + "downMonitorsWithGeo": "", + "message": "Top 3 Monitors Below Availability Threshold (99.34 %): + Unreliable(https://unreliable.co): 90.925% + no-name(https://no-name.co): 90.925% + Foo(https://foo.com): 98.033% + ", + }, + ], + ] + `); + expect(mockGetter).not.toHaveBeenCalled(); + expect(mockAvailability).toHaveBeenCalledTimes(1); + expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "callES": [MockFunction], + "dynamicSettings": Object { + "certAgeThreshold": 730, + "certExpirationThreshold": 30, + "heartbeatIndices": "heartbeat-8*", + }, + "filters": "{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":12349}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":5601}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"url.port\\":443}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"observer.geo.name\\":\\"harrisburg\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"monitor.type\\":\\"http\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"unsecured\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match\\":{\\"tags\\":\\"containers\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"tags\\":\\"org:google\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}]}}]}}]}}", + "range": 35, + "rangeUnit": "d", + "threshold": "99.34", + }, + ] + `); + expect(state).toMatchInlineSnapshot(` + Object { + "currentTriggerStarted": undefined, + "firstCheckedAt": "availability test", + "firstTriggeredAt": undefined, + "isTriggered": false, + "lastCheckedAt": "availability test", + "lastResolvedAt": undefined, + "lastTriggeredAt": undefined, + } + `); + }); + + it('supports availability checks with search', async () => { + expect.assertions(2); + toISOStringSpy.mockImplementation(() => 'availability with search'); + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([]); + const mockAvailability = jest.fn(); + mockAvailability.mockReturnValue([]); + const { server, libs } = bootstrapDependencies({ + getMonitorAvailability: mockAvailability, + getIndexPattern: jest.fn(), + }); + const alert = statusCheckAlertFactory(server, libs); + const options = mockOptions({ + availability: { + range: 23, + rangeUnit: 'w', + threshold: '90', + }, + search: 'ur.port: *', + shouldCheckAvailability: true, + }); + await alert.executor(options); + expect(mockAvailability).toHaveBeenCalledTimes(1); + expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "callES": [MockFunction], + "dynamicSettings": Object { + "certAgeThreshold": 730, + "certExpirationThreshold": 30, + "heartbeatIndices": "heartbeat-8*", + }, + "filters": "{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"ur.port\\"}}],\\"minimum_should_match\\":1}}", + "range": 23, + "rangeUnit": "w", + "threshold": "90", + }, + ] + `); + }); + + it('supports availability checks with no filter or search', async () => { + expect.assertions(2); + toISOStringSpy.mockImplementation(() => 'availability with search'); + const mockGetter = jest.fn(); + mockGetter.mockReturnValue([]); + const mockAvailability = jest.fn(); + mockAvailability.mockReturnValue([]); + const { server, libs } = bootstrapDependencies({ + getMonitorAvailability: mockAvailability, + getIndexPattern: jest.fn(), + }); + const alert = statusCheckAlertFactory(server, libs); + const options = mockOptions({ + availability: { + range: 23, + rangeUnit: 'w', + threshold: '90', + }, + shouldCheckAvailability: true, + }); + await alert.executor(options); + expect(mockAvailability).toHaveBeenCalledTimes(1); + expect(mockAvailability.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "callES": [MockFunction], + "dynamicSettings": Object { + "certAgeThreshold": 730, + "certExpirationThreshold": 30, + "heartbeatIndices": "heartbeat-8*", + }, + "filters": undefined, + "range": 23, + "rangeUnit": "w", + "threshold": "90", + }, + ] + `); + }); }); describe('fullListByIdAndLocation', () => { @@ -311,13 +756,17 @@ describe('status check alert', () => { // @ts-ignore the `props` key here isn't described expect(Object.keys(alert.validate?.params?.props ?? {})).toMatchInlineSnapshot(` Array [ + "availability", "filters", "locations", "numTimes", "search", + "shouldCheckStatus", + "shouldCheckAvailability", "timerangeCount", "timerangeUnit", "timerange", + "version", ] `); }); @@ -370,11 +819,11 @@ describe('status check alert', () => { mockGetIndexPattern.mockReturnValue(undefined); it('returns `undefined` for no filters or search', async () => { - expect(await genFilterString(mockGetIndexPattern)).toBeUndefined(); + expect(await generateFilterDSL(mockGetIndexPattern)).toBeUndefined(); }); it('creates a filter string for filters only', async () => { - const res = await genFilterString(mockGetIndexPattern, { + const res = await generateFilterDSL(mockGetIndexPattern, { 'monitor.type': [], 'observer.geo.name': ['us-east', 'us-west'], tags: [], @@ -416,7 +865,7 @@ describe('status check alert', () => { }); it('creates a filter string for search only', async () => { - expect(await genFilterString(mockGetIndexPattern, undefined, 'monitor.id: "kibana-dev"')) + expect(await generateFilterDSL(mockGetIndexPattern, undefined, 'monitor.id: "kibana-dev"')) .toMatchInlineSnapshot(` Object { "bool": Object { @@ -434,7 +883,7 @@ describe('status check alert', () => { }); it('creates a filter string for filters and string', async () => { - const res = await genFilterString( + const res = await generateFilterDSL( mockGetIndexPattern, { 'monitor.type': [], @@ -617,25 +1066,137 @@ describe('status check alert', () => { }); it('creates a message with appropriate number of monitors', () => { - expect(contextMessage(ids, 3)).toMatchInlineSnapshot( + expect(contextMessage(ids, 3, [], '0', false, true)).toMatchInlineSnapshot( `"Down monitors: first, second, third... and 2 other monitors"` ); }); it('throws an error if `max` is less than 2', () => { - expect(() => contextMessage(ids, 1)).toThrowErrorMatchingInlineSnapshot( + expect(() => contextMessage(ids, 1, [], '0', false, true)).toThrowErrorMatchingInlineSnapshot( '"Maximum value must be greater than 2, received 1."' ); }); it('returns only the ids if length < max', () => { - expect(contextMessage(ids.slice(0, 2), 3)).toMatchInlineSnapshot( + expect(contextMessage(ids.slice(0, 2), 3, [], '0', false, true)).toMatchInlineSnapshot( `"Down monitors: first, second"` ); }); it('returns a default message when no monitors are provided', () => { - expect(contextMessage([], 3)).toMatchInlineSnapshot(`"No down monitor IDs received"`); + expect(contextMessage([], 3, [], '0', false, true)).toMatchInlineSnapshot( + `"No down monitor IDs received"` + ); + }); + }); + + describe('availabilityMessage', () => { + it('creates message for singular item', () => { + expect( + availabilityMessage( + [ + { + monitorId: 'test-node-service', + location: 'fairbanks', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 821.0, + down: 2450.0, + availabilityRatio: 0.25099357994497096, + }, + ], + '59' + ) + ).toMatchInlineSnapshot(` + "Monitor Below Availability Threshold (59 %): + Test Node Service(http://localhost:12349): 25.099% + " + `); + }); + + it('creates message for multiple items', () => { + expect( + availabilityMessage( + [ + { + monitorId: 'test-node-service', + location: 'fairbanks', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 821.0, + down: 2450.0, + availabilityRatio: 0.25099357994497096, + }, + { + monitorId: 'test-node-service', + location: 'harrisburg', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 3389.0, + down: 2450.0, + availabilityRatio: 0.5804076040417879, + }, + ], + '59' + ) + ).toMatchInlineSnapshot(` + "Top 2 Monitors Below Availability Threshold (59 %): + Test Node Service(http://localhost:12349): 25.099% + Test Node Service(http://localhost:12349): 58.041% + " + `); + }); + + it('caps message for multiple items', () => { + expect( + availabilityMessage( + [ + { + monitorId: 'test-node-service', + location: 'fairbanks', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 821.0, + down: 2450.0, + availabilityRatio: 0.250993579944971, + }, + { + monitorId: 'test-node-service', + location: 'harrisburg', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 3389.0, + down: 2450.0, + availabilityRatio: 0.58040760404178, + }, + { + monitorId: 'test-node-service', + location: 'berlin', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 3645.0, + down: 2982.0, + availabilityRatio: 0.550022634676324, + }, + { + monitorId: 'test-node-service', + location: 'st paul', + name: 'Test Node Service', + url: 'http://localhost:12349', + up: 3601.0, + down: 2681.0, + availabilityRatio: 0.573225087551735, + }, + ], + '59' + ) + ).toMatchInlineSnapshot(` + "Top 3 Monitors Below Availability Threshold (59 %): + Test Node Service(http://localhost:12349): 25.099% + Test Node Service(http://localhost:12349): 55.002% + Test Node Service(http://localhost:12349): 57.323% + " + `); }); }); }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index cd42082b42c843..2117ac4b7ed4ef 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -18,12 +18,16 @@ import { StatusCheckParams, StatusCheckFilters, AtomicStatusCheckParamsType, + MonitorAvailabilityType, + DynamicSettings, } from '../../../common/runtime_types'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; import { savedObjectsAdapter } from '../saved_objects'; import { updateState } from './common'; import { commonStateTranslations } from './translations'; import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; +import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability'; +import { UMServerLibs } from '../lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -37,53 +41,141 @@ export const uniqueMonitorIds = (items: GetMonitorStatusResult[]): Set = return acc; }, new Set()); +const sortAvailabilityResultByRatioAsc = ( + a: GetMonitorAvailabilityResult, + b: GetMonitorAvailabilityResult +): number => (a.availabilityRatio ?? 100) - (b.availabilityRatio ?? 100); + +/** + * Map an availability result object to a descriptive string. + */ +const mapAvailabilityResultToString = ({ + availabilityRatio, + name, + monitorId, + url, +}: GetMonitorAvailabilityResult) => + i18n.translate('xpack.uptime.alerts.availability.monitorSummary', { + defaultMessage: '{nameOrId}({url}): {availabilityRatio}%', + values: { + nameOrId: name || monitorId, + url, + availabilityRatio: ((availabilityRatio ?? 1.0) * 100).toPrecision(5), + }, + }); + +const reduceAvailabilityStringsToMessage = (threshold: string) => ( + prev: string, + cur: string, + _ind: number, + array: string[] +) => { + let prefix: string = ''; + if (prev !== '') { + prefix = prev; + } else if (array.length > 1) { + prefix = i18n.translate('xpack.uptime.alerts.availability.multiItemTitle', { + defaultMessage: `Top {monitorCount} Monitors Below Availability Threshold ({threshold} %):\n`, + values: { + monitorCount: Math.min(array.length, MESSAGE_AVAILABILITY_MAX), + threshold, + }, + }); + } else { + prefix = i18n.translate('xpack.uptime.alerts.availability.singleItemTitle', { + defaultMessage: `Monitor Below Availability Threshold ({threshold} %):\n`, + values: { threshold }, + }); + } + return prefix + `${cur}\n`; +}; + +const MESSAGE_AVAILABILITY_MAX = 3; + +/** + * Creates a summary message from a list of availability check result objects. + * @param availabilityResult the list of results + * @param threshold the threshold used by the check + */ +export const availabilityMessage = ( + availabilityResult: GetMonitorAvailabilityResult[], + threshold: string, + max: number = MESSAGE_AVAILABILITY_MAX +): string => { + return availabilityResult.length > 0 + ? // if there are results, map each item to a descriptive string, and reduce the list + availabilityResult + .sort(sortAvailabilityResultByRatioAsc) + .slice(0, max) + .map(mapAvailabilityResultToString) + .reduce(reduceAvailabilityStringsToMessage(threshold), '') + : // if there are no results, return an empty list default string + i18n.translate('xpack.uptime.alerts.availability.emptyMessage', { + defaultMessage: `No monitors were below Availability Threshold ({threshold} %)`, + values: { + threshold, + }, + }); +}; + /** * Generates a message to include in contexts of alerts. * @param monitors the list of monitors to include in the message * @param max the maximum number of items the summary should contain */ -export const contextMessage = (monitorIds: string[], max: number): string => { +export const contextMessage = ( + monitorIds: string[], + max: number, + availabilityResult: GetMonitorAvailabilityResult[], + availabilityThreshold: string, + availabilityWasChecked: boolean, + statusWasChecked: boolean +): string => { const MIN = 2; if (max < MIN) throw new Error(`Maximum value must be greater than ${MIN}, received ${max}.`); // generate the message - let message; - if (monitorIds.length === 1) { - message = i18n.translate('xpack.uptime.alerts.message.singularTitle', { - defaultMessage: 'Down monitor: ', - }); - } else if (monitorIds.length) { - message = i18n.translate('xpack.uptime.alerts.message.multipleTitle', { - defaultMessage: 'Down monitors: ', - }); - } - // this shouldn't happen because the function should only be called - // when > 0 monitors are down - else { - message = i18n.translate('xpack.uptime.alerts.message.emptyTitle', { - defaultMessage: 'No down monitor IDs received', - }); - } - - for (let i = 0; i < monitorIds.length; i++) { - const id = monitorIds[i]; - if (i === max) { - return ( - message + - i18n.translate('xpack.uptime.alerts.message.overflowBody', { - defaultMessage: `... and {overflowCount} other monitors`, - values: { - overflowCount: monitorIds.length - i, - }, - }) - ); - } else if (i === 0) { - message = message + id; + let message = ''; + if (statusWasChecked) { + if (monitorIds.length === 1) { + message = i18n.translate('xpack.uptime.alerts.message.singularTitle', { + defaultMessage: 'Down monitor: ', + }); + } else if (monitorIds.length) { + message = i18n.translate('xpack.uptime.alerts.message.multipleTitle', { + defaultMessage: 'Down monitors: ', + }); } else { - message = message + `, ${id}`; + message = i18n.translate('xpack.uptime.alerts.message.emptyTitle', { + defaultMessage: 'No down monitor IDs received', + }); + } + + for (let i = 0; i < monitorIds.length; i++) { + const id = monitorIds[i]; + if (i === max) { + message = + message + + i18n.translate('xpack.uptime.alerts.message.overflowBody', { + defaultMessage: `... and {overflowCount} other monitors`, + values: { + overflowCount: monitorIds.length - i, + }, + }); + break; + } else if (i === 0) { + message = message + id; + } else { + message = message + `, ${id}`; + } } } + if (availabilityWasChecked) { + const availabilityMsg = availabilityMessage(availabilityResult, availabilityThreshold); + return message ? message + '\n' + availabilityMsg : availabilityMsg; + } + return message; }; @@ -142,7 +234,7 @@ export const hasFilters = (filters?: StatusCheckFilters) => { return false; }; -export const genFilterString = async ( +export const generateFilterDSL = async ( getIndexPattern: () => Promise, filters?: StatusCheckFilters, search?: string @@ -170,6 +262,25 @@ export const genFilterString = async ( ); }; +const formatFilterString = async ( + libs: UMServerLibs, + dynamicSettings: DynamicSettings, + options: AlertExecutorOptions, + filters?: StatusCheckFilters, + search?: string +) => + JSON.stringify( + await generateFilterDSL( + () => + libs.requests.getIndexPattern({ + callES: options.services.callCluster, + dynamicSettings, + }), + filters, + search + ) + ); + export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ id: 'xpack.uptime.alerts.monitorStatus', name: i18n.translate('xpack.uptime.alerts.monitorStatus', { @@ -177,8 +288,16 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = }), validate: { params: schema.object({ + availability: schema.maybe( + schema.object({ + range: schema.number(), + rangeUnit: schema.string(), + threshold: schema.string(), + }) + ), filters: schema.maybe( schema.oneOf([ + // deprecated schema.object({ 'monitor.type': schema.maybe(schema.arrayOf(schema.string())), 'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())), @@ -188,17 +307,22 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = schema.string(), ]) ), + // deprecated locations: schema.maybe(schema.arrayOf(schema.string())), numTimes: schema.number(), search: schema.maybe(schema.string()), + shouldCheckStatus: schema.maybe(schema.boolean()), + shouldCheckAvailability: schema.maybe(schema.boolean()), timerangeCount: schema.maybe(schema.number()), timerangeUnit: schema.maybe(schema.string()), + // deprecated timerange: schema.maybe( schema.object({ from: schema.string(), to: schema.string(), }) ), + version: schema.maybe(schema.number()), }), }, defaultActionGroupId: MONITOR_STATUS.id, @@ -239,22 +363,14 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = options.services.savedObjectsClient ); const atomicDecoded = AtomicStatusCheckParamsType.decode(rawParams); + const availabilityDecoded = MonitorAvailabilityType.decode(rawParams); const decoded = StatusCheckParamsType.decode(rawParams); + let filterString: string = ''; let params: StatusCheckParams; if (isRight(atomicDecoded)) { const { filters, search, numTimes, timerangeCount, timerangeUnit } = atomicDecoded.right; const timerange = { from: `now-${String(timerangeCount) + timerangeUnit}`, to: 'now' }; - const filterString = JSON.stringify( - await genFilterString( - () => - libs.requests.getIndexPattern({ - callES: options.services.callCluster, - dynamicSettings, - }), - filters, - search - ) - ); + filterString = await formatFilterString(libs, dynamicSettings, options, filters, search); params = { timerange, numTimes, @@ -263,25 +379,49 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = }; } else if (isRight(decoded)) { params = decoded.right; - } else { + } else if (!isRight(availabilityDecoded)) { ThrowReporter.report(decoded); return { error: 'Alert param types do not conform to required shape.', }; } + let availabilityResults: GetMonitorAvailabilityResult[] = []; + if ( + isRight(availabilityDecoded) && + availabilityDecoded.right.shouldCheckAvailability === true + ) { + const { filters, search } = availabilityDecoded.right; + if (filterString === '' && (filters || search)) { + filterString = await formatFilterString(libs, dynamicSettings, options, filters, search); + } + + availabilityResults = await libs.requests.getMonitorAvailability({ + callES: options.services.callCluster, + dynamicSettings, + ...availabilityDecoded.right.availability, + filters: filterString || undefined, + }); + } + /* This is called `monitorsByLocation` but it's really * monitors by location by status. The query we run to generate this * filters on the status field, so effectively there should be one and only one * status represented in the result set. */ - const monitorsByLocation = await libs.requests.getMonitorStatus({ - callES: options.services.callCluster, - dynamicSettings, - ...params, - }); + let monitorsByLocation: GetMonitorStatusResult[] = []; + + // old alert versions are missing this field so it must default to true + const verifiedParams = StatusCheckParamsType.decode(params!); + if (isRight(verifiedParams) && (verifiedParams.right?.shouldCheckStatus ?? true)) { + monitorsByLocation = await libs.requests.getMonitorStatus({ + callES: options.services.callCluster, + dynamicSettings, + ...verifiedParams.right, + }); + } // if no monitors are down for our query, we don't need to trigger an alert - if (monitorsByLocation.length) { + if (monitorsByLocation.length || availabilityResults.length) { const uniqueIds = uniqueMonitorIds(monitorsByLocation); const alertInstance = options.services.alertInstanceFactory(MONITOR_STATUS.id); alertInstance.replaceState({ @@ -290,7 +430,14 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = (_server, libs) = ...updateState(options.state, true), }); alertInstance.scheduleActions(MONITOR_STATUS.id, { - message: contextMessage(Array.from(uniqueIds.keys()), DEFAULT_MAX_MESSAGE_ROWS), + message: contextMessage( + Array.from(uniqueIds.keys()), + DEFAULT_MAX_MESSAGE_ROWS, + availabilityResults, + isRight(availabilityDecoded) ? availabilityDecoded.right.availability.threshold : '100', + isRight(availabilityDecoded) && availabilityDecoded.right.shouldCheckAvailability, + rawParams?.shouldCheckStatus ?? false + ), downMonitorsWithGeo: fullListByIdAndLocation(monitorsByLocation), }); } diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts new file mode 100644 index 00000000000000..e014aa985a82d1 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_availability.test.ts @@ -0,0 +1,853 @@ +/* + * 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 { + formatBuckets, + GetMonitorAvailabilityResult, + AvailabilityKey, + getMonitorAvailability, +} from '../get_monitor_availability'; +import { setupMockEsCompositeQuery } from './helper'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; +import { GetMonitorAvailabilityParams } from '../../../../common/runtime_types'; +interface AvailabilityTopHit { + _source: { + monitor: { + name: string; + }; + url: { + full: string; + }; + }; +} + +interface AvailabilityDoc { + key: AvailabilityKey; + doc_count: number; + up_sum: { + value: number; + }; + down_sum: { + value: number; + }; + fields: { + hits: { + hits: AvailabilityTopHit[]; + }; + }; + ratio: { + value: number | null; + }; +} + +const genBucketItem = ({ + monitorId, + location, + name, + url, + up, + down, + availabilityRatio, +}: GetMonitorAvailabilityResult): AvailabilityDoc => ({ + key: { + monitorId, + location, + }, + doc_count: up + down, + fields: { + hits: { + hits: [ + { + _source: { + monitor: { + name, + }, + url: { + full: url, + }, + }, + }, + ], + }, + }, + up_sum: { + value: up, + }, + down_sum: { + value: down, + }, + ratio: { + value: availabilityRatio, + }, +}); + +describe('monitor availability', () => { + describe('getMonitorAvailability', () => { + it('applies bool filters to params', async () => { + const [callES, esMock] = setupMockEsCompositeQuery< + AvailabilityKey, + GetMonitorAvailabilityResult, + AvailabilityDoc + >([], genBucketItem); + const exampleFilter = `{ + "bool": { + "should": [ + { + "bool": { + "should": [ + { + "match_phrase": { + "monitor.id": "apm-dev" + } + } + ], + "minimum_should_match": 1 + } + }, + { + "bool": { + "should": [ + { + "match_phrase": { + "monitor.id": "auto-http-0X8D6082B94BBE3B8A" + } + } + ], + "minimum_should_match": 1 + } + } + ], + "minimum_should_match": 1 + } + }`; + await getMonitorAvailability({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + filters: exampleFilter, + range: 2, + rangeUnit: 'w', + threshold: '54', + }); + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); + const [method, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "down_sum": Object { + "sum": Object { + "field": "summary.down", + "missing": 0, + }, + }, + "fields": Object { + "top_hits": Object { + "_source": Array [ + "monitor.name", + "url.full", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "filtered": Object { + "bucket_selector": Object { + "buckets_path": Object { + "threshold": "ratio.value", + }, + "script": "params.threshold < 0.54", + }, + }, + "ratio": Object { + "bucket_script": Object { + "buckets_path": Object { + "downTotal": "down_sum", + "upTotal": "up_sum", + }, + "script": " + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;", + }, + }, + "up_sum": Object { + "sum": Object { + "field": "summary.up", + "missing": 0, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-2w", + "lte": "now", + }, + }, + }, + ], + "minimum_should_match": 1, + "should": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "apm-dev", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "monitor.id": "auto-http-0X8D6082B94BBE3B8A", + }, + }, + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + }); + + it('fetches a single page of results', async () => { + const [callES, esMock] = setupMockEsCompositeQuery< + AvailabilityKey, + GetMonitorAvailabilityResult, + AvailabilityDoc + >( + [ + { + bucketCriteria: [ + { + monitorId: 'foo', + location: 'harrisburg', + name: 'Foo', + url: 'http://foo.com', + up: 456, + down: 234, + availabilityRatio: 0.660869565217391, + }, + { + monitorId: 'foo', + location: 'faribanks', + name: 'Foo', + url: 'http://foo.com', + up: 450, + down: 240, + availabilityRatio: 0.652173913043478, + }, + { + monitorId: 'bar', + location: 'fairbanks', + name: 'Bar', + url: 'http://bar.com', + up: 468, + down: 212, + availabilityRatio: 0.688235294117647, + }, + ], + }, + ], + genBucketItem + ); + const clientParameters: GetMonitorAvailabilityParams = { + range: 23, + rangeUnit: 'd', + threshold: '69', + }; + const result = await getMonitorAvailability({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + ...clientParameters, + }); + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); + const [method, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "down_sum": Object { + "sum": Object { + "field": "summary.down", + "missing": 0, + }, + }, + "fields": Object { + "top_hits": Object { + "_source": Array [ + "monitor.name", + "url.full", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "filtered": Object { + "bucket_selector": Object { + "buckets_path": Object { + "threshold": "ratio.value", + }, + "script": "params.threshold < 0.69", + }, + }, + "ratio": Object { + "bucket_script": Object { + "buckets_path": Object { + "downTotal": "down_sum", + "upTotal": "up_sum", + }, + "script": " + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;", + }, + }, + "up_sum": Object { + "sum": Object { + "field": "summary.up", + "missing": 0, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-23d", + "lte": "now", + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "availabilityRatio": 0.660869565217391, + "down": 234, + "location": "harrisburg", + "monitorId": "foo", + "name": "Foo", + "up": 456, + "url": "http://foo.com", + }, + Object { + "availabilityRatio": 0.652173913043478, + "down": 240, + "location": "faribanks", + "monitorId": "foo", + "name": "Foo", + "up": 450, + "url": "http://foo.com", + }, + Object { + "availabilityRatio": 0.688235294117647, + "down": 212, + "location": "fairbanks", + "monitorId": "bar", + "name": "Bar", + "up": 468, + "url": "http://bar.com", + }, + ] + `); + }); + + it('fetches multiple pages', async () => { + const [callES, esMock] = setupMockEsCompositeQuery< + AvailabilityKey, + GetMonitorAvailabilityResult, + AvailabilityDoc + >( + [ + { + after_key: { + monitorId: 'baz', + location: 'harrisburg', + }, + bucketCriteria: [ + { + monitorId: 'foo', + location: 'harrisburg', + name: 'Foo', + url: 'http://foo.com', + up: 243, + down: 11, + availabilityRatio: 0.956692913385827, + }, + { + monitorId: 'foo', + location: 'fairbanks', + name: 'Foo', + url: 'http://foo.com', + up: 251, + down: 13, + availabilityRatio: 0.950757575757576, + }, + ], + }, + { + bucketCriteria: [ + { + monitorId: 'baz', + location: 'harrisburg', + name: 'Baz', + url: 'http://baz.com', + up: 341, + down: 3, + availabilityRatio: 0.991279069767442, + }, + { + monitorId: 'baz', + location: 'fairbanks', + name: 'Baz', + url: 'http://baz.com', + up: 365, + down: 5, + availabilityRatio: 0.986486486486486, + }, + ], + }, + ], + genBucketItem + ); + const result = await getMonitorAvailability({ + callES, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, + range: 3, + rangeUnit: 'M', + threshold: '98', + }); + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "availabilityRatio": 0.956692913385827, + "down": 11, + "location": "harrisburg", + "monitorId": "foo", + "name": "Foo", + "up": 243, + "url": "http://foo.com", + }, + Object { + "availabilityRatio": 0.950757575757576, + "down": 13, + "location": "fairbanks", + "monitorId": "foo", + "name": "Foo", + "up": 251, + "url": "http://foo.com", + }, + Object { + "availabilityRatio": 0.991279069767442, + "down": 3, + "location": "harrisburg", + "monitorId": "baz", + "name": "Baz", + "up": 341, + "url": "http://baz.com", + }, + Object { + "availabilityRatio": 0.986486486486486, + "down": 5, + "location": "fairbanks", + "monitorId": "baz", + "name": "Baz", + "up": 365, + "url": "http://baz.com", + }, + ] + `); + const [method, params] = esMock.callAsCurrentUser.mock.calls[0]; + expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(method).toEqual('search'); + expect(params).toMatchInlineSnapshot(` + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "down_sum": Object { + "sum": Object { + "field": "summary.down", + "missing": 0, + }, + }, + "fields": Object { + "top_hits": Object { + "_source": Array [ + "monitor.name", + "url.full", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "filtered": Object { + "bucket_selector": Object { + "buckets_path": Object { + "threshold": "ratio.value", + }, + "script": "params.threshold < 0.98", + }, + }, + "ratio": Object { + "bucket_script": Object { + "buckets_path": Object { + "downTotal": "down_sum", + "upTotal": "up_sum", + }, + "script": " + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;", + }, + }, + "up_sum": Object { + "sum": Object { + "field": "summary.up", + "missing": 0, + }, + }, + }, + "composite": Object { + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-3M", + "lte": "now", + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + } + `); + expect(esMock.callAsCurrentUser.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "search", + Object { + "body": Object { + "aggs": Object { + "monitors": Object { + "aggs": Object { + "down_sum": Object { + "sum": Object { + "field": "summary.down", + "missing": 0, + }, + }, + "fields": Object { + "top_hits": Object { + "_source": Array [ + "monitor.name", + "url.full", + ], + "size": 1, + "sort": Array [ + Object { + "@timestamp": Object { + "order": "desc", + }, + }, + ], + }, + }, + "filtered": Object { + "bucket_selector": Object { + "buckets_path": Object { + "threshold": "ratio.value", + }, + "script": "params.threshold < 0.98", + }, + }, + "ratio": Object { + "bucket_script": Object { + "buckets_path": Object { + "downTotal": "down_sum", + "upTotal": "up_sum", + }, + "script": " + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;", + }, + }, + "up_sum": Object { + "sum": Object { + "field": "summary.up", + "missing": 0, + }, + }, + }, + "composite": Object { + "after": Object { + "location": "harrisburg", + "monitorId": "baz", + }, + "size": 2000, + "sources": Array [ + Object { + "monitorId": Object { + "terms": Object { + "field": "monitor.id", + }, + }, + }, + Object { + "location": Object { + "terms": Object { + "field": "observer.geo.name", + "missing_bucket": true, + }, + }, + }, + ], + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "gte": "now-3M", + "lte": "now", + }, + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "heartbeat-8*", + }, + ] + `); + }); + }); + + describe('formatBuckets', () => { + let buckets: any[]; + + beforeEach(() => { + buckets = [ + { + key: { + monitorId: 'test-node-service', + location: 'fairbanks', + }, + doc_count: 3271, + fields: { + hits: { + hits: [ + { + _source: { + monitor: { + name: 'Test Node Service', + }, + url: { + full: 'http://localhost:12349', + }, + }, + }, + ], + }, + }, + up_sum: { + value: 821.0, + }, + down_sum: { + value: 2450.0, + }, + ratio: { + value: 0.25099357994497096, + }, + }, + { + key: { + monitorId: 'test-node-service', + location: 'harrisburg', + }, + fields: { + hits: { + hits: [ + { + _source: { + monitor: { + name: 'Test Node Service', + }, + url: { + full: 'http://localhost:12349', + }, + }, + }, + ], + }, + }, + doc_count: 5839, + up_sum: { + value: 3389.0, + }, + down_sum: { + value: 2450.0, + }, + ratio: { + value: 0.5804076040417879, + }, + }, + ]; + }); + + it('formats the buckets to the correct shape', async () => { + expect(await formatBuckets(buckets)).toMatchInlineSnapshot(` + Array [ + Object { + "availabilityRatio": 0.25099357994497096, + "down": 2450, + "location": "fairbanks", + "monitorId": "test-node-service", + "name": "Test Node Service", + "up": 821, + "url": "http://localhost:12349", + }, + Object { + "availabilityRatio": 0.5804076040417879, + "down": 2450, + "location": "harrisburg", + "monitorId": "test-node-service", + "name": "Test Node Service", + "up": 3389, + "url": "http://localhost:12349", + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index 2a1417b49dca41..1783cb3c28522d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { LegacyScopedClusterClient } from 'src/core/server'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; +import { setupMockEsCompositeQuery } from './helper'; -interface BucketItemCriteria { +export interface BucketItemCriteria { monitor_id: string; status: string; location: string; @@ -27,11 +26,6 @@ interface BucketItem { doc_count: number; } -interface MultiPageCriteria { - after_key?: BucketKey; - bucketCriteria: BucketItemCriteria[]; -} - const genBucketItem = ({ monitor_id, status, @@ -46,30 +40,12 @@ const genBucketItem = ({ doc_count, }); -type MockCallES = (method: any, params: any) => Promise; - -const setupMock = ( - criteria: MultiPageCriteria[] -): [MockCallES, jest.Mocked>] => { - const esMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); - - criteria.forEach(({ after_key, bucketCriteria }) => { - const mockResponse = { - aggregations: { - monitors: { - after_key, - buckets: bucketCriteria.map((item) => genBucketItem(item)), - }, - }, - }; - esMock.callAsCurrentUser.mockResolvedValueOnce(mockResponse); - }); - return [(method: any, params: any) => esMock.callAsCurrentUser(method, params), esMock]; -}; - describe('getMonitorStatus', () => { it('applies bool filters to params', async () => { - const [callES, esMock] = setupMock([]); + const [callES, esMock] = setupMockEsCompositeQuery( + [], + genBucketItem + ); const exampleFilter = `{ "bool": { "should": [ @@ -203,7 +179,10 @@ describe('getMonitorStatus', () => { }); it('applies locations to params', async () => { - const [callES, esMock] = setupMock([]); + const [callES, esMock] = setupMockEsCompositeQuery( + [], + genBucketItem + ); await getMonitorStatus({ callES, dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, @@ -294,30 +273,33 @@ describe('getMonitorStatus', () => { }); it('fetches single page of results', async () => { - const [callES, esMock] = setupMock([ - { - bucketCriteria: [ - { - monitor_id: 'foo', - status: 'down', - location: 'fairbanks', - doc_count: 43, - }, - { - monitor_id: 'bar', - status: 'down', - location: 'harrisburg', - doc_count: 53, - }, - { - monitor_id: 'foo', - status: 'down', - location: 'harrisburg', - doc_count: 44, - }, - ], - }, - ]); + const [callES, esMock] = setupMockEsCompositeQuery( + [ + { + bucketCriteria: [ + { + monitor_id: 'foo', + status: 'down', + location: 'fairbanks', + doc_count: 43, + }, + { + monitor_id: 'bar', + status: 'down', + location: 'harrisburg', + doc_count: 53, + }, + { + monitor_id: 'foo', + status: 'down', + location: 'harrisburg', + doc_count: 44, + }, + ], + }, + ], + genBucketItem + ); const clientParameters = { filters: undefined, locations: [], @@ -418,7 +400,7 @@ describe('getMonitorStatus', () => { `); }); - it('fetches multiple pages of results in the thing', async () => { + it('fetches multiple pages of ES results', async () => { const criteria = [ { after_key: { @@ -491,7 +473,10 @@ describe('getMonitorStatus', () => { ], }, ]; - const [callES] = setupMock(criteria); + const [callES] = setupMockEsCompositeQuery( + criteria, + genBucketItem + ); const result = await getMonitorStatus({ callES, dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts new file mode 100644 index 00000000000000..0eb46e17c63241 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/helper.ts @@ -0,0 +1,49 @@ +/* + * 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 { LegacyScopedClusterClient } from 'src/core/server'; +import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; + +export interface MultiPageCriteria { + after_key?: K; + bucketCriteria: T[]; +} + +export type MockCallES = (method: any, params: any) => Promise; + +/** + * This utility function will set up a mock ES client, and store subsequent calls. It is designed + * to let callers easily simulate an arbitrary series of chained composite aggregation calls by supplying + * custom after_key values. + * + * This function is used by supplying criteria, a flat collection of values, and a function that can map + * those values to the same document shape the tested code expects to receive from elasticsearch. + * @param criteria A series of objects with the fields of interest. + * @param genBucketItem A function that maps the criteria to the structure of a document. + * @template K The Key type of the mock after_key value for simulated composite aggregation queries. + * @template C The Criteria type that specifies the values of interest in the buckets returned by the mock ES. + * @template I The Item type that specifies the simulated documents that are generated by the mock. + */ +export const setupMockEsCompositeQuery = ( + criteria: Array>, + genBucketItem: (criteria: C) => I +): [MockCallES, jest.Mocked>] => { + const esMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); + + criteria.forEach(({ after_key, bucketCriteria }) => { + const mockResponse = { + aggregations: { + monitors: { + after_key, + buckets: bucketCriteria.map((item) => genBucketItem(item)), + }, + }, + }; + esMock.callAsCurrentUser.mockResolvedValueOnce(mockResponse); + }); + + return [(method: any, params: any) => esMock.callAsCurrentUser(method, params), esMock]; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts new file mode 100644 index 00000000000000..eafc0df431f770 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_availability.ts @@ -0,0 +1,160 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { GetMonitorAvailabilityParams } from '../../../common/runtime_types'; + +export interface AvailabilityKey { + monitorId: string; + location: string; +} + +export interface GetMonitorAvailabilityResult { + monitorId: string; + location: string; + name: string; + url: string; + up: number; + down: number; + availabilityRatio: number | null; +} + +export const formatBuckets = async (buckets: any[]): Promise => + buckets.map(({ key, fields, up_sum, down_sum, ratio }: any) => ({ + ...key, + name: fields?.hits?.hits?.[0]?._source?.monitor.name, + url: fields?.hits?.hits?.[0]?._source?.url.full, + up: up_sum.value, + down: down_sum.value, + availabilityRatio: ratio.value, + })); + +export const getMonitorAvailability: UMElasticsearchQueryFn< + GetMonitorAvailabilityParams, + GetMonitorAvailabilityResult[] +> = async ({ callES, dynamicSettings, range, rangeUnit, threshold: thresholdString, filters }) => { + const queryResults: Array> = []; + let afterKey: AvailabilityKey | undefined; + + const threshold = Number(thresholdString) / 100; + if (threshold <= 0 || threshold > 1.0) { + throw new Error( + `Invalid availability threshold value ${thresholdString}. The value must be between 0 and 100` + ); + } + + const gte = `now-${range}${rangeUnit}`; + + do { + const esParams: any = { + index: dynamicSettings.heartbeatIndices, + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte, + lte: 'now', + }, + }, + }, + ], + }, + }, + size: 0, + aggs: { + monitors: { + composite: { + size: 2000, + sources: [ + { + monitorId: { + terms: { + field: 'monitor.id', + }, + }, + }, + { + location: { + terms: { + field: 'observer.geo.name', + missing_bucket: true, + }, + }, + }, + ], + }, + aggs: { + fields: { + top_hits: { + size: 1, + _source: ['monitor.name', 'url.full'], + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }, + up_sum: { + sum: { + field: 'summary.up', + missing: 0, + }, + }, + down_sum: { + sum: { + field: 'summary.down', + missing: 0, + }, + }, + ratio: { + bucket_script: { + buckets_path: { + upTotal: 'up_sum', + downTotal: 'down_sum', + }, + script: ` + if (params.upTotal + params.downTotal > 0) { + return params.upTotal / (params.upTotal + params.downTotal); + } return null;`, + }, + }, + filtered: { + bucket_selector: { + buckets_path: { + threshold: 'ratio.value', + }, + script: `params.threshold < ${threshold}`, + }, + }, + }, + }, + }, + }, + }; + + if (filters) { + const parsedFilters = JSON.parse(filters); + esParams.body.query.bool = { ...esParams.body.query.bool, ...parsedFilters.bool }; + } + + if (afterKey) { + esParams.body.aggs.monitors.composite.after = afterKey; + } + + const result = await callES('search', esParams); + afterKey = result?.aggregations?.monitors?.after_key; + + queryResults.push(formatBuckets(result?.aggregations?.monitors?.buckets || [])); + } while (afterKey !== undefined); + + return (await Promise.all(queryResults)).reduce((acc, cur) => acc.concat(cur), []); +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index 8435240963ebfa..33f18b7a940694 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -54,10 +54,10 @@ export const getMonitorStatus: UMElasticsearchQueryFn< const queryResults: Array> = []; let afterKey: MonitorStatusKey | undefined; + const STATUS = 'down'; do { // today this value is hardcoded. In the future we may support // multiple status types for this alert, and this will become a parameter - const STATUS = 'down'; const esParams: any = { index: dynamicSettings.heartbeatIndices, body: { diff --git a/x-pack/plugins/uptime/server/lib/requests/index.ts b/x-pack/plugins/uptime/server/lib/requests/index.ts index 243bb089cc7b46..415b3d2f4b4a17 100644 --- a/x-pack/plugins/uptime/server/lib/requests/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/index.ts @@ -8,6 +8,7 @@ export { getCerts } from './get_certs'; export { getFilterBar, GetFilterBarParams } from './get_filter_bar'; export { getUptimeIndexPattern as getIndexPattern } from './get_index_pattern'; export { getLatestMonitor, GetLatestMonitorParams } from './get_latest_monitor'; +export { getMonitorAvailability } from './get_monitor_availability'; export { getMonitorDurationChart, GetMonitorChartsParams } from './get_monitor_duration'; export { getMonitorDetails, GetMonitorDetailsParams } from './get_monitor_details'; export { getMonitorLocations, GetMonitorLocationsParams } from './get_monitor_locations'; diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index ae3a729e41c706..2a9420a2755709 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -7,6 +7,7 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { OverviewFilters, + GetMonitorAvailabilityParams, MonitorDetails, MonitorLocations, Snapshot, @@ -34,6 +35,7 @@ import { } from '.'; import { GetSnapshotCountParams } from './get_snapshot_counts'; import { IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { GetMonitorAvailabilityResult } from './get_monitor_availability'; type ESQ = UMElasticsearchQueryFn; @@ -42,6 +44,7 @@ export interface UptimeRequests { getFilterBar: ESQ; getIndexPattern: ESQ<{}, IIndexPattern | undefined>; getLatestMonitor: ESQ; + getMonitorAvailability: ESQ; getMonitorDurationChart: ESQ; getMonitorDetails: ESQ; getMonitorLocations: ESQ; From d58f52de2bc793d61b9755961bccbf475ed1033f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Thu, 9 Jul 2020 21:31:53 +0200 Subject: [PATCH 08/15] [Composable template] Demo and PR review fixes (#71065) --- .../static/forms/components/form_row.tsx | 6 +- .../home/index_templates_tab.helpers.ts | 4 +- .../home/index_templates_tab.test.ts | 70 ++-- .../template_create.test.tsx | 2 +- .../template_edit.test.tsx | 2 +- .../common/lib/template_serialization.ts | 26 +- .../index_management/common/lib/utils.ts | 2 +- .../common/types/templates.ts | 18 +- .../component_template_create.test.tsx | 2 - .../component_template_edit.test.tsx | 5 - .../component_templates.tsx | 56 ++-- .../component_templates_selector.scss | 4 + .../component_templates_selector.tsx | 6 +- .../steps/step_logistics.tsx | 8 +- .../steps/step_logistics_schema.tsx | 2 +- .../configuration_form/configuration_form.tsx | 26 +- .../configuration_form_schema.tsx | 22 +- .../templates_form/templates_form.tsx | 14 +- .../components/mappings_editor/lib/utils.ts | 2 +- .../mappings_editor/mappings_editor.tsx | 6 +- .../mappings_editor/mappings_state.tsx | 13 +- .../template_form/steps/step_components.tsx | 19 +- .../template_form/steps/step_logistics.tsx | 221 +++++++++---- .../template_form/steps/step_review.tsx | 2 +- .../template_form/template_form.tsx | 68 ++-- .../template_form/template_form_schemas.tsx | 34 ++ .../home/template_list/components/index.ts | 2 + .../components/template_type_indicator.tsx | 37 +++ .../template_table/template_table.tsx | 95 +++--- .../template_details/tabs/tab_summary.tsx | 307 +++++++++--------- .../template_details_content.tsx | 17 +- .../home/template_list/template_list.tsx | 48 ++- .../template_table/template_table.tsx | 39 +-- .../sections/template_edit/template_edit.tsx | 4 +- .../index_management/public/shared_imports.ts | 3 + .../routes/api/templates/validate_schemas.ts | 4 +- .../test/fixtures/template.ts | 52 +-- .../index_management/templates.helpers.js | 8 +- 38 files changed, 756 insertions(+), 500 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx diff --git a/src/plugins/es_ui_shared/static/forms/components/form_row.tsx b/src/plugins/es_ui_shared/static/forms/components/form_row.tsx index ad5a517e40cfbd..d38e6c4f5fd95c 100644 --- a/src/plugins/es_ui_shared/static/forms/components/form_row.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/form_row.tsx @@ -57,13 +57,9 @@ export const FormRow = ({ titleWrapped = title; } - if (!children && !field) { - throw new Error('You need to provide either children or a field to the FormRow'); - } - return ( - {children ? children : } + {children ? children : field ? : null} ); }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 0047e4c0294cb9..a3974190533517 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -95,9 +95,9 @@ const createActions = (testBed: TestBed) => { find('closeDetailsButton').simulate('click'); }; - const toggleViewItem = (view: 'composable' | 'system') => { + const toggleViewItem = (view: 'managed' | 'cloudManaged' | 'system') => { const { find, component } = testBed; - const views = ['composable', 'system']; + const views = ['managed', 'cloudManaged', 'system']; // First open the pop over act(() => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index 1ec29f1c5b894b..276101486aa619 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -63,7 +63,6 @@ describe('Index Templates tab', () => { }, }, }); - (template1 as any).hasSettings = true; const template2 = fixtures.getTemplate({ name: `b${getRandomString()}`, @@ -73,6 +72,7 @@ describe('Index Templates tab', () => { const template3 = fixtures.getTemplate({ name: `.c${getRandomString()}`, // mock system template indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'], + type: 'system', }); const template4 = fixtures.getTemplate({ @@ -101,6 +101,7 @@ describe('Index Templates tab', () => { name: `.c${getRandomString()}`, // mock system template indexPatterns: ['template6Pattern1*', 'template6Pattern2', 'template6Pattern3'], isLegacy: true, + type: 'system', }); const templates = [template1, template2, template3]; @@ -124,44 +125,49 @@ describe('Index Templates tab', () => { // Test composable table content tableCellsValues.forEach((row, i) => { const indexTemplate = templates[i]; - const { name, indexPatterns, priority, ilmPolicy, composedOf, template } = indexTemplate; + const { name, indexPatterns, ilmPolicy, composedOf, template } = indexTemplate; const hasContent = !!template.settings || !!template.mappings || !!template.aliases; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; const composedOfString = composedOf ? composedOf.join(',') : ''; - const priorityFormatted = priority ? priority.toString() : ''; - - expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ - '', // Checkbox to select row - name, - indexPatterns.join(', '), - ilmPolicyName, - composedOfString, - priorityFormatted, - hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges - '', // Column of actions - ]); + + try { + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', // Checkbox to select row + name, + indexPatterns.join(', '), + ilmPolicyName, + composedOfString, + hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges + '', // Column of actions + ]); + } catch (e) { + console.error(`Error in index template at row ${i}`); // eslint-disable-line no-console + throw e; + } }); // Test legacy table content legacyTableCellsValues.forEach((row, i) => { - const template = legacyTemplates[i]; - const { name, indexPatterns, order, ilmPolicy } = template; + const legacyIndexTemplate = legacyTemplates[i]; + const { name, indexPatterns, ilmPolicy, template } = legacyIndexTemplate; + const hasContent = !!template.settings || !!template.mappings || !!template.aliases; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; - const orderFormatted = order ? order.toString() : order; - - expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ - '', - name, - indexPatterns.join(', '), - ilmPolicyName, - orderFormatted, - '', - '', - '', - '', - ]); + + try { + expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', + name, + indexPatterns.join(', '), + ilmPolicyName, + hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges + '', // Column of actions + ]); + } catch (e) { + console.error(`Error in legacy template at row ${i}`); // eslint-disable-line no-console + throw e; + } }); }); @@ -211,7 +217,7 @@ describe('Index Templates tab', () => { await actions.clickTemplateAt(0); expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(templates[0].name); + expect(find('templateDetails.title').text().trim()).toBe(templates[0].name); // Close flyout await act(async () => { @@ -223,7 +229,7 @@ describe('Index Templates tab', () => { expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); - expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); + expect(find('templateDetails.title').text().trim()).toBe(legacyTemplates[0].name); }); describe('table row actions', () => { @@ -460,7 +466,7 @@ describe('Index Templates tab', () => { const { find } = testBed; const [{ name }] = templates; - expect(find('templateDetails.title').text()).toEqual(name); + expect(find('templateDetails.title').text().trim()).toEqual(name); }); it('should have a close button and be able to close flyout', async () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx index 69d7a13edfcfbe..76b6c34f999d5b 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx @@ -368,8 +368,8 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { + type: 'default', isLegacy: false, - isManaged: false, }, }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx index 9f0e81454f0af0..de66013241236b 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx @@ -213,7 +213,7 @@ describe.skip('', () => { aliases: ALIASES, }, _kbnMeta: { - isManaged: false, + type: 'default', isLegacy: templateToEdit._kbnMeta.isLegacy, }, }; diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 5c55860bda81b2..069d6ac29fbca7 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -8,18 +8,28 @@ import { LegacyTemplateSerialized, TemplateSerialized, TemplateListItem, + TemplateType, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized { - const { version, priority, indexPatterns, template, composedOf, _meta } = templateDeserialized; + const { + version, + priority, + indexPatterns, + template, + composedOf, + dataStream, + _meta, + } = templateDeserialized; return { version, priority, template, index_patterns: indexPatterns, + data_stream: dataStream, composed_of: composedOf, _meta, }; @@ -41,6 +51,15 @@ export function deserializeTemplate( } = templateEs; const { settings } = template; + let type: TemplateType = 'default'; + if (Boolean(cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix))) { + type = 'cloudManaged'; + } else if (name.startsWith('.')) { + type = 'system'; + } else if (Boolean(_meta?.managed === true)) { + type = 'managed'; + } + const deserializedTemplate: TemplateDeserialized = { name, version, @@ -52,10 +71,7 @@ export function deserializeTemplate( dataStream, _meta, _kbnMeta: { - isManaged: Boolean(_meta?.managed === true), - isCloudManaged: Boolean( - cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix) - ), + type, hasDatastream: Boolean(dataStream), }, }; diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts index 5a7db8ef50ab43..1dc6f4a486a2c1 100644 --- a/x-pack/plugins/index_management/common/lib/utils.ts +++ b/x-pack/plugins/index_management/common/lib/utils.ts @@ -23,5 +23,5 @@ export const getTemplateParameter = ( ) => { return isLegacyTemplate(template) ? (template as LegacyTemplateSerialized)[setting] - : (template as TemplateSerialized).template[setting]; + : (template as TemplateSerialized).template?.[setting]; }; diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index fdcac40ca596ff..32e254e490b2aa 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -38,23 +38,24 @@ export interface TemplateDeserialized { aliases?: Aliases; mappings?: Mappings; }; - composedOf?: string[]; // Used on composable index template + composedOf?: string[]; // Composable template only version?: number; - priority?: number; - order?: number; // Used on legacy index template + priority?: number; // Composable template only + order?: number; // Legacy template only ilmPolicy?: { name: string; }; - _meta?: { [key: string]: any }; - dataStream?: { timestamp_field: string }; + _meta?: { [key: string]: any }; // Composable template only + dataStream?: { timestamp_field: string }; // Composable template only _kbnMeta: { - isManaged: boolean; - isCloudManaged: boolean; + type: TemplateType; hasDatastream: boolean; isLegacy?: boolean; }; } +export type TemplateType = 'default' | 'managed' | 'cloudManaged' | 'system'; + export interface TemplateFromEs { name: string; index_template: TemplateSerialized; @@ -78,8 +79,7 @@ export interface TemplateListItem { name: string; }; _kbnMeta: { - isManaged: boolean; - isCloudManaged: boolean; + type: TemplateType; hasDatastream: boolean; isLegacy?: boolean; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx index 6c8da4684f019a..75eb419d56a5c9 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx @@ -177,8 +177,6 @@ describe('', () => { template: { settings: SETTINGS, mappings: { - _source: {}, - _meta: {}, properties: { [BOOLEAN_MAPPING_FIELD.name]: { type: BOOLEAN_MAPPING_FIELD.type, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx index f237605756d5c3..115fdf032da8f0 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -109,11 +109,6 @@ describe('', () => { ...COMPONENT_TEMPLATE_TO_EDIT, template: { ...COMPONENT_TEMPLATE_TO_EDIT.template, - mappings: { - _meta: {}, - _source: {}, - properties: {}, - }, }, }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx index 64c7cd400ba0d6..ea5632ac861924 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx @@ -26,8 +26,15 @@ interface Filters { [key: string]: { name: string; checked: 'on' | 'off' }; } +/** + * Copied from https://stackoverflow.com/a/9310752 + */ +function escapeRegExp(text: string) { + return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +} + function fuzzyMatch(searchValue: string, text: string) { - const pattern = `.*${searchValue.split('').join('.*')}.*`; + const pattern = `.*${searchValue.split('').map(escapeRegExp).join('.*')}.*`; const regex = new RegExp(pattern); return regex.test(text); } @@ -48,7 +55,7 @@ const i18nTexts = { searchBoxPlaceholder: i18n.translate( 'xpack.idxMgmt.componentTemplatesSelector.searchBox.placeholder', { - defaultMessage: 'Search components', + defaultMessage: 'Search component templates', } ), }; @@ -78,24 +85,33 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro return []; } - return components.filter((component) => { - if (filters.settings.checked === 'on' && !component.hasSettings) { - return false; - } - if (filters.mappings.checked === 'on' && !component.hasMappings) { - return false; - } - if (filters.aliases.checked === 'on' && !component.hasAliases) { - return false; - } - - if (searchValue.trim() === '') { - return true; - } - - const match = fuzzyMatch(searchValue, component.name); - return match; - }); + return components + .filter((component) => { + if (filters.settings.checked === 'on' && !component.hasSettings) { + return false; + } + if (filters.mappings.checked === 'on' && !component.hasMappings) { + return false; + } + if (filters.aliases.checked === 'on' && !component.hasAliases) { + return false; + } + + if (searchValue.trim() === '') { + return true; + } + + const match = fuzzyMatch(searchValue, component.name); + return match; + }) + .sort((a, b) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } + return 0; + }); }, [isLoading, components, searchValue, filters]); const isSearchResultEmpty = filteredComponents.length === 0 && components.length > 0; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss index 6abbbe65790e73..61d5512da2cd9f 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss @@ -32,5 +32,9 @@ font-weight: 600; } } + + &__content { + mask-image: none; + } } } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx index af48c3c79379ad..8795c08fd2bee8 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx @@ -96,7 +96,7 @@ export const ComponentTemplatesSelector = ({ ); @@ -136,7 +136,7 @@ export const ComponentTemplatesSelector = ({ }} /> -
+
)} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx index 8762eae9d2297c..18988fa125a066 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx @@ -200,7 +200,7 @@ export const StepLogistics: React.FunctionComponent = React.memo( } > - {isMetaVisible ? ( + {isMetaVisible && ( = React.memo( 'aria-label': i18n.translate( 'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaAriaLabel', { - defaultMessage: 'Metadata JSON editor', + defaultMessage: '_meta field data editor', } ), }, }} /> - ) : ( - // requires children or a field - // For now, we return an empty
if the editor is not visible -
)} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx index 0c52037abde459..c5779573394879 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx @@ -65,7 +65,7 @@ export const logisticsFormSchema: FormSchema = { }, _meta: { label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', { - defaultMessage: 'Metadata (optional)', + defaultMessage: '_meta field data (optional)', }), helpText: ( - Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; - const formSerializer: SerializerFunc = (formData) => { const { dynamicMapping: { @@ -40,22 +37,17 @@ const formSerializer: SerializerFunc = (formData) => { const dynamic = dynamicMappingsEnabled ? true : throwErrorsForUnmappedFields ? 'strict' : false; - let parsedMeta; - try { - parsedMeta = JSON.parse(metaField); - } catch { - parsedMeta = {}; - } - - return { + const serialized = { dynamic, numeric_detection, date_detection, dynamic_date_formats, - _source: { ...sourceField }, - _meta: parsedMeta, + _source: sourceField, + _meta: metaField, _routing, }; + + return serialized; }; const formDeserializer = (formData: GenericObject) => { @@ -64,7 +56,11 @@ const formDeserializer = (formData: GenericObject) => { numeric_detection, date_detection, dynamic_date_formats, - _source: { enabled, includes, excludes }, + _source: { enabled, includes, excludes } = {} as { + enabled?: boolean; + includes?: string[]; + excludes?: string[]; + }, _meta, _routing, } = formData; @@ -82,7 +78,7 @@ const formDeserializer = (formData: GenericObject) => { includes, excludes, }, - metaField: stringifyJson(_meta), + metaField: _meta ?? {}, _routing, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx index c06340fd9ae14b..6e80f8b813ec22 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx @@ -48,10 +48,30 @@ export const configurationFormSchema: FormSchema = { validator: isJsonField( i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.metaFieldEditorJsonError', { defaultMessage: 'The _meta field JSON is not valid.', - }) + }), + { allowEmptyString: true } ), }, ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value, null, 2); + }, + serializer: (value: string) => { + try { + const parsed = JSON.parse(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(parsed).length) { + return undefined; + } + return parsed; + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, }, sourceField: { enabled: { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx index 80937e7da11922..79685d46b6bddb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx @@ -22,7 +22,7 @@ interface Props { const stringifyJson = (json: { [key: string]: any }) => Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; -const formSerializer: SerializerFunc = (formData) => { +const formSerializer: SerializerFunc = (formData) => { const { dynamicTemplates } = formData; let parsedTemplates; @@ -34,12 +34,14 @@ const formSerializer: SerializerFunc = (formData) => { parsedTemplates = [parsedTemplates]; } } catch { - parsedTemplates = []; + // Silently swallow errors } - return { - dynamic_templates: parsedTemplates, - }; + return Array.isArray(parsedTemplates) && parsedTemplates.length > 0 + ? { + dynamic_templates: parsedTemplates, + } + : undefined; }; const formDeserializer = (formData: { [key: string]: any }) => { @@ -53,7 +55,7 @@ const formDeserializer = (formData: { [key: string]: any }) => { export const TemplatesForm = React.memo(({ value }: Props) => { const isMounted = useRef(undefined); - const { form } = useForm({ + const { form } = useForm({ schema: templatesFormSchema, serializer: formSerializer, deserializer: formDeserializer, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts index 9fa4a7981c047d..8b3ff600053054 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts @@ -199,7 +199,7 @@ export const getTypeMetaFromSource = ( * * @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types) */ -export const normalize = (fieldsToNormalize: Fields): NormalizedFields => { +export const normalize = (fieldsToNormalize: Fields = {}): NormalizedFields => { let maxNestedDepth = 0; const normalizeFields = ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx index 46dc1176f62b4b..e8fda907377088 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx @@ -39,14 +39,14 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr } const { - _source = {}, - _meta = {}, + _source, + _meta, _routing, dynamic, numeric_detection, date_detection, dynamic_date_formats, - properties = {}, + properties, dynamic_templates, } = mappingsDefinition; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx index fb4bfae9740005..ad5056fa73ce16 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx @@ -19,7 +19,7 @@ import { normalize, deNormalize, stripUndefinedValues } from './lib'; type Mappings = MappingsTemplates & MappingsConfiguration & { - properties: MappingsFields; + properties?: MappingsFields; }; export interface Types { @@ -31,7 +31,7 @@ export interface Types { export interface OnUpdateHandlerArg { isValid?: boolean; - getData: () => Mappings; + getData: () => Mappings | undefined; validate: () => Promise; } @@ -114,13 +114,18 @@ export const MappingsState = React.memo(({ children, onChange, value }: Props) = const configurationData = state.configuration.data.format(); const templatesData = state.templates.data.format(); - return { + const output = { ...stripUndefinedValues({ ...configurationData, ...templatesData, }), - properties: fields, }; + + if (fields && Object.keys(fields).length > 0) { + output.properties = fields; + } + + return Object.keys(output).length > 0 ? (output as Mappings) : undefined; }, validate: async () => { const configurationFormValidator = diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx index 01771f40f89eaf..df0cc791384fe5 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx @@ -25,6 +25,12 @@ interface Props { } const i18nTexts = { + title: ( + + ), description: ( { - onChange({ isValid: true, validate: async () => true, getData: () => components }); + onChange({ + isValid: true, + validate: async () => true, + getData: () => (components.length > 0 ? components : undefined), + }); }, [onChange] ); @@ -63,12 +73,7 @@ export const StepComponents = ({ defaultValue = [], onChange, esDocsBase }: Prop -

- -

+

{i18nTexts.title}

diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index 44ec4db0873f38..27779411754295 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiLink, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -16,6 +23,7 @@ import { Field, Forms, JsonEditorField, + FormDataProvider, } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -24,70 +32,125 @@ import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_f const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); -const fieldsMeta = { - name: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameTitle', { - defaultMessage: 'Name', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameDescription', { - defaultMessage: 'A unique identifier for this template.', - }), - testSubject: 'nameField', - }, - indexPatterns: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle', { - defaultMessage: 'Index patterns', - }), - description: i18n.translate( - 'xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription', - { - defaultMessage: 'The index patterns to apply to the template.', - } - ), - testSubject: 'indexPatternsField', - }, - order: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', { - defaultMessage: 'Merge order', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderDescription', { - defaultMessage: 'The merge order when multiple templates match an index.', - }), - testSubject: 'orderField', - }, - priority: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', { - defaultMessage: 'Merge priority', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', { - defaultMessage: 'The merge priority when multiple templates match an index.', - }), - testSubject: 'priorityField', - }, - version: { - title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', { - defaultMessage: 'Version', - }), - description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionDescription', { - defaultMessage: 'A number that identifies the template to external management systems.', - }), - testSubject: 'versionField', - }, -}; +function getFieldsMeta(esDocsBase: string) { + return { + name: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameTitle', { + defaultMessage: 'Name', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameDescription', { + defaultMessage: 'A unique identifier for this template.', + }), + testSubject: 'nameField', + }, + indexPatterns: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle', { + defaultMessage: 'Index patterns', + }), + description: i18n.translate( + 'xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription', + { + defaultMessage: 'The index patterns to apply to the template.', + } + ), + testSubject: 'indexPatternsField', + }, + dataStream: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.dataStreamTitle', { + defaultMessage: 'Data stream', + }), + description: ( + +
+ + {i18n.translate( + 'xpack.idxMgmt.templateForm.stepLogistics.dataStreamDocumentionLink', + { + defaultMessage: 'Learn more about data streams.', + } + )} + + + ), + }} + /> + ), + testSubject: 'dataStreamField', + }, + order: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', { + defaultMessage: 'Merge order', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderDescription', { + defaultMessage: 'The merge order when multiple templates match an index.', + }), + testSubject: 'orderField', + }, + priority: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', { + defaultMessage: 'Priority', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', { + defaultMessage: 'Only the highest priority template will be applied.', + }), + testSubject: 'priorityField', + }, + version: { + title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', { + defaultMessage: 'Version', + }), + description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionDescription', { + defaultMessage: 'A number that identifies the template to external management systems.', + }), + testSubject: 'versionField', + }, + }; +} + +interface LogisticsForm { + [key: string]: any; +} + +interface LogisticsFormInternal extends LogisticsForm { + __internal__: { + addMeta: boolean; + }; +} interface Props { - defaultValue: { [key: string]: any }; + defaultValue: LogisticsForm; onChange: (content: Forms.Content) => void; isEditing?: boolean; isLegacy?: boolean; } +function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { + return { + ...formData, + __internal__: { + addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), + }, + }; +} + +function formSerializer(formData: LogisticsFormInternal): LogisticsForm { + const { __internal__, ...rest } = formData; + return rest; +} + export const StepLogistics: React.FunctionComponent = React.memo( ({ defaultValue, isEditing = false, onChange, isLegacy = false }) => { const { form } = useForm({ schema: schemas.logistics, defaultValue, options: { stripEmptyFields: false }, + serializer: formSerializer, + deserializer: formDeserializer, }); /** @@ -117,7 +180,9 @@ export const StepLogistics: React.FunctionComponent = React.memo( return subscription.unsubscribe; }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps - const { name, indexPatterns, order, priority, version } = fieldsMeta; + const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( + documentationService.getEsDocsBase() + ); return ( <> @@ -180,6 +245,16 @@ export const StepLogistics: React.FunctionComponent = React.memo( /> + {/* Create data stream */} + {isLegacy !== true && ( + + + + )} + {/* Order */} {isLegacy && ( @@ -226,25 +301,35 @@ export const StepLogistics: React.FunctionComponent = React.memo( id="xpack.idxMgmt.templateForm.stepLogistics.metaFieldDescription" defaultMessage="Use the _meta field to store any metadata you want." /> + + } > - + {({ '__internal__.addMeta': addMeta }) => { + return ( + addMeta && ( + + ) + ); }} - /> + )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx index 880c7fbd7f23c8..0f4b9de4f6cfa3 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx @@ -168,7 +168,7 @@ export const StepReview: React.FunctionComponent = React.memo( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 6310ac09488e5d..f5c9be9292cd05 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -50,7 +50,7 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { components: { id: 'components', label: i18n.translate('xpack.idxMgmt.templateForm.steps.componentsStepName', { - defaultMessage: 'Components', + defaultMessage: 'Component templates', }), }, settings: { @@ -91,15 +91,9 @@ export const TemplateForm = ({ const indexTemplate = defaultValue ?? { name: '', indexPatterns: [], - composedOf: [], - template: { - settings: {}, - mappings: {}, - aliases: {}, - }, + template: {}, _kbnMeta: { - isManaged: false, - isCloudManaged: false, + type: 'default', hasDatastream: false, isLegacy, }, @@ -150,18 +144,50 @@ export const TemplateForm = ({ ) : null; - const buildTemplateObject = (initialTemplate: TemplateDeserialized) => ( - wizardData: WizardContent - ): TemplateDeserialized => ({ - ...initialTemplate, - ...wizardData.logistics, - composedOf: wizardData.components, - template: { - settings: wizardData.settings, - mappings: wizardData.mappings, - aliases: wizardData.aliases, + /** + * If no mappings, settings or aliases are defined, it is better to not send empty + * object for those values. + * This method takes care of that and other cleanup of empty fields. + * @param template The template object to clean up + */ + const cleanupTemplateObject = (template: TemplateDeserialized) => { + const outputTemplate = { ...template }; + + if (outputTemplate.template.settings === undefined) { + delete outputTemplate.template.settings; + } + if (outputTemplate.template.mappings === undefined) { + delete outputTemplate.template.mappings; + } + if (outputTemplate.template.aliases === undefined) { + delete outputTemplate.template.aliases; + } + if (Object.keys(outputTemplate.template).length === 0) { + delete outputTemplate.template; + } + + return outputTemplate; + }; + + const buildTemplateObject = useCallback( + (initialTemplate: TemplateDeserialized) => ( + wizardData: WizardContent + ): TemplateDeserialized => { + const outputTemplate = { + ...initialTemplate, + ...wizardData.logistics, + composedOf: wizardData.components, + template: { + settings: wizardData.settings, + mappings: wizardData.mappings, + aliases: wizardData.aliases, + }, + }; + + return cleanupTemplateObject(outputTemplate); }, - }); + [] + ); const onSaveTemplate = useCallback( async (wizardData: WizardContent) => { @@ -177,7 +203,7 @@ export const TemplateForm = ({ clearSaveError(); }, - [indexTemplate, onSave, clearSaveError] + [indexTemplate, buildTemplateObject, onSave, clearSaveError] ); return ( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 5af3b4dd00c4ff..d8c3ad8c259fcf 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -128,6 +128,32 @@ export const schemas: Record = { }, ], }, + dataStream: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.datastreamLabel', { + defaultMessage: 'Create data stream', + }), + defaultValue: false, + serializer: (value) => { + if (value === true) { + return { + timestamp_field: '@timestamp', + }; + } + }, + deserializer: (value) => { + if (typeof value === 'boolean') { + return value; + } + + /** + * For now, it is enough to have a "data_stream" declared on the index template + * to assume that the template creates a data stream. In the future, this condition + * might change + */ + return value !== undefined; + }, + }, order: { type: FIELD_TYPES.NUMBER, label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldOrderLabel', { @@ -187,5 +213,13 @@ export const schemas: Record = { } }, }, + __internal__: { + addMeta: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { + defaultMessage: 'Add metadata', + }), + }, + }, }, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts index 156d792c26f1db..3954ce04ca0b53 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts @@ -5,3 +5,5 @@ */ export * from './filter_list_button'; + +export * from './template_type_indicator'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx new file mode 100644 index 00000000000000..c6b0e21ebfdc11 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx @@ -0,0 +1,37 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiBadge } from '@elastic/eui'; + +import { TemplateType } from '../../../../../../common'; + +interface Props { + templateType: TemplateType; +} + +const i18nTexts = { + managed: i18n.translate('xpack.idxMgmt.templateBadgeType.managed', { + defaultMessage: 'Managed', + }), + cloudManaged: i18n.translate('xpack.idxMgmt.templateBadgeType.cloudManaged', { + defaultMessage: 'Cloud-managed', + }), + system: i18n.translate('xpack.idxMgmt.templateBadgeType.system', { defaultMessage: 'System' }), +}; + +export const TemplateTypeIndicator = ({ templateType }: Props) => { + if (templateType === 'default') { + return null; + } + + return ( + + {i18nTexts[templateType]} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index b470bcfd7660e4..9203e76fce7873 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -7,7 +7,7 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiInMemoryTable, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { SendRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports'; import { TemplateListItem } from '../../../../../../../common'; @@ -15,6 +15,8 @@ import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/con import { TemplateDeleteModal } from '../../../../../components'; import { encodePathForReactRouter } from '../../../../../services/routing'; import { useServices } from '../../../../../app_context'; +import { TemplateContentIndicator } from '../../../../../components/shared'; +import { TemplateTypeIndicator } from '../../components'; interface Props { templates: TemplateListItem[]; @@ -47,20 +49,23 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ sortable: true, render: (name: TemplateListItem['name'], item: TemplateListItem) => { return ( - /* eslint-disable-next-line @elastic/eui/href-or-on-click */ - uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) - )} - data-test-subj="templateDetailsLink" - > - {name} - + <> + uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) + )} + data-test-subj="templateDetailsLink" + > + {name} + +   + + ); }, }, @@ -98,44 +103,30 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ ) : null, }, { - field: 'order', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.orderColumnTitle', { - defaultMessage: 'Order', - }), - truncateText: true, - sortable: true, - }, - { - field: 'hasMappings', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.mappingsColumnTitle', { - defaultMessage: 'Mappings', + name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.contentColumnTitle', { + defaultMessage: 'Content', }), - truncateText: true, - sortable: true, - render: (hasMappings: boolean) => (hasMappings ? : null), - }, - { - field: 'hasSettings', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.settingsColumnTitle', { - defaultMessage: 'Settings', - }), - truncateText: true, - sortable: true, - render: (hasSettings: boolean) => (hasSettings ? : null), - }, - { - field: 'hasAliases', - name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.aliasesColumnTitle', { - defaultMessage: 'Aliases', - }), - truncateText: true, - sortable: true, - render: (hasAliases: boolean) => (hasAliases ? : null), + width: '120px', + render: (item: TemplateListItem) => ( + + {i18n.translate('xpack.idxMgmt.templateList.table.noneDescriptionText', { + defaultMessage: 'None', + })} + + } + /> + ), }, { name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionColumnTitle', { defaultMessage: 'Actions', }), + width: '120px', actions: [ { name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionEditText', { @@ -153,7 +144,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name, true); }, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, { type: 'icon', @@ -188,7 +179,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ setTemplatesToDelete([{ name, isLegacy }]); }, isPrimary: true, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, ], }, @@ -208,13 +199,13 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ const selectionConfig = { onSelectionChange: setSelection, - selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + selectable: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( - 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + 'xpack.idxMgmt.templateList.legacyTable.deleteCloudManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', + defaultMessage: 'You cannot delete a cloud-managed template.', } ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index de2fc29ec85435..0c403e69d2e765 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -17,6 +17,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiCodeBlock, + EuiSpacer, } from '@elastic/eui'; import { useAppContext } from '../../../../../app_context'; import { TemplateDeserialized } from '../../../../../../../common'; @@ -57,163 +58,169 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails }) } = useAppContext(); return ( - - - - {/* Index patterns */} - - - - - {numIndexPatterns > 1 ? ( - -
    - {indexPatterns.map((indexName: string, i: number) => { - return ( -
  • - - {indexName} - -
  • - ); - })} -
-
+ <> + + + + {/* Index patterns */} + + + + + {numIndexPatterns > 1 ? ( + +
    + {indexPatterns.map((indexName: string, i: number) => { + return ( +
  • + + {indexName} + +
  • + ); + })} +
+
+ ) : ( + indexPatterns.toString() + )} +
+ + {/* Priority / Order */} + {isLegacy !== true ? ( + <> + + + + + {priority || priority === 0 ? priority : i18nTexts.none} + + ) : ( - indexPatterns.toString() + <> + + + + + {order || order === 0 ? order : i18nTexts.none} + + )} -
- - {/* Priority / Order */} - {isLegacy !== true ? ( - <> - - - - - {priority || priority === 0 ? priority : i18nTexts.none} - - - ) : ( - <> - - - - - {order || order === 0 ? order : i18nTexts.none} - - - )} - {/* Components */} - {isLegacy !== true && ( - <> - - - - - {composedOf && composedOf.length > 0 ? ( -
    - {composedOf.map((component) => ( -
  • - - {component} - -
  • - ))} -
- ) : ( - i18nTexts.none - )} -
- - )} -
-
+ {/* Components */} + {isLegacy !== true && ( + <> + + + + + {composedOf && composedOf.length > 0 ? ( +
    + {composedOf.map((component) => ( +
  • + + {component} + +
  • + ))} +
+ ) : ( + i18nTexts.none + )} +
+ + )} + +
- - - {/* ILM Policy (only for legacy as composable template could have ILM policy + + + {/* ILM Policy (only for legacy as composable template could have ILM policy inside one of their components) */} - {isLegacy && ( - <> - - - - - {ilmPolicy && ilmPolicy.name ? ( - - {ilmPolicy.name} - - ) : ( - i18nTexts.none - )} - - - )} + {isLegacy && ( + <> + + + + + {ilmPolicy && ilmPolicy.name ? ( + + {ilmPolicy.name} + + ) : ( + i18nTexts.none + )} + + + )} + + {/* Has data stream? (only for composable template) */} + {isLegacy !== true && ( + <> + + + + + {hasDatastream ? i18nTexts.yes : i18nTexts.no} + + + )} - {/* Has data stream? (only for composable template) */} - {isLegacy !== true && ( - <> - - - - - {hasDatastream ? i18nTexts.yes : i18nTexts.no} - - - )} + {/* Version */} + + + + + {version || version === 0 ? version : i18nTexts.none} + + + +
- {/* Version */} - - - - - {version || version === 0 ? version : i18nTexts.none} - + - {/* Metadata (optional) */} - {isLegacy !== true && _meta && ( - <> - - - - - {JSON.stringify(_meta, null, 2)} - - - )} - - - + + {/* Metadata (optional) */} + {isLegacy !== true && _meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )} + + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index 34e90aef51701a..5b726013a1d922 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -36,6 +36,7 @@ import { useLoadIndexTemplate } from '../../../../services/api'; import { decodePathFromReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; +import { TemplateTypeIndicator } from '../components'; import { TabSummary } from './tabs'; const SUMMARY_TAB_ID = 'summary'; @@ -98,7 +99,7 @@ export const TemplateDetailsContent = ({ decodedTemplateName, isLegacy ); - const isCloudManaged = templateDetails?._kbnMeta.isCloudManaged ?? false; + const isCloudManaged = templateDetails?._kbnMeta.type === 'cloudManaged'; const [templateToDelete, setTemplateToDelete] = useState< Array<{ name: string; isLegacy?: boolean }> >([]); @@ -111,6 +112,12 @@ export const TemplateDetailsContent = ({

{decodedTemplateName} + {templateDetails && ( + <> +   + + + )}

@@ -163,16 +170,16 @@ export const TemplateDetailsContent = ({ } color="primary" size="s" > diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index 18a65407ee20db..f421bc5d87a54e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -37,13 +37,19 @@ import { TemplateDetails } from './template_details'; import { LegacyTemplateTable } from './legacy_templates/template_table'; import { FilterListButton, Filters } from './components'; -type FilterName = 'composable' | 'system'; +type FilterName = 'managed' | 'cloudManaged' | 'system'; interface MatchParams { templateName?: string; } -const stripOutSystemTemplates = (templates: TemplateListItem[]): TemplateListItem[] => - templates.filter((template) => !template.name.startsWith('.')); +function filterTemplates(templates: TemplateListItem[], types: string[]): TemplateListItem[] { + return templates.filter((template) => { + if (template._kbnMeta.type === 'default') { + return true; + } + return types.includes(template._kbnMeta.type); + }); +} export const TemplateList: React.FunctionComponent> = ({ match: { @@ -56,12 +62,18 @@ export const TemplateList: React.FunctionComponent>({ - composable: { - name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewComposableTemplateLabel', { - defaultMessage: 'Composable templates', + managed: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewManagedTemplateLabel', { + defaultMessage: 'Managed templates', }), checked: 'on', }, + cloudManaged: { + name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewCloudManagedTemplateLabel', { + defaultMessage: 'Cloud-managed templates', + }), + checked: 'off', + }, system: { name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewSystemTemplateLabel', { defaultMessage: 'System templates', @@ -72,18 +84,19 @@ export const TemplateList: React.FunctionComponent { if (!allTemplates) { + // If templates are not fetched, return empty arrays. return { templates: [], legacyTemplates: [] }; } - return filters.system.checked === 'on' - ? allTemplates - : { - templates: stripOutSystemTemplates(allTemplates.templates), - legacyTemplates: stripOutSystemTemplates(allTemplates.legacyTemplates), - }; - }, [allTemplates, filters.system.checked]); + const visibleTemplateTypes = Object.entries(filters) + .filter(([name, _filter]) => _filter.checked === 'on') + .map(([name]) => name); - const showComposableTemplateTable = filters.composable.checked === 'on'; + return { + templates: filterTemplates(allTemplates.templates, visibleTemplateTypes), + legacyTemplates: filterTemplates(allTemplates.legacyTemplates, visibleTemplateTypes), + }; + }, [allTemplates, filters]); const selectedTemplate = Boolean(templateName) ? { @@ -154,8 +167,8 @@ export const TemplateList: React.FunctionComponent ); - const renderTemplatesTable = () => - showComposableTemplateTable ? ( + const renderTemplatesTable = () => { + return ( <> - ) : null; + ); + }; const renderLegacyTemplatesTable = () => ( <> diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx index 55a777363d06fe..3dffdcde160f16 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx @@ -7,14 +7,7 @@ import React, { useState, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiInMemoryTable, - EuiBasicTableColumn, - EuiButton, - EuiLink, - EuiBadge, - EuiIcon, -} from '@elastic/eui'; +import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink, EuiIcon } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { TemplateListItem } from '../../../../../../common'; @@ -24,6 +17,7 @@ import { encodePathForReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TemplateDeleteModal } from '../../../../components'; import { TemplateContentIndicator } from '../../../../components/shared'; +import { TemplateTypeIndicator } from '../components'; interface Props { templates: TemplateListItem[]; @@ -70,13 +64,7 @@ export const TemplateTable: React.FunctionComponent = ({ {name}   - {item._kbnMeta.isManaged ? ( - - Managed - - ) : ( - '' - )} + ); }, @@ -99,14 +87,6 @@ export const TemplateTable: React.FunctionComponent = ({ sortable: true, render: (composedOf: string[] = []) => {composedOf.join(', ')}, }, - { - field: 'priority', - name: i18n.translate('xpack.idxMgmt.templateList.table.priorityColumnTitle', { - defaultMessage: 'Priority', - }), - truncateText: true, - sortable: true, - }, { name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', { defaultMessage: 'Data stream', @@ -119,7 +99,7 @@ export const TemplateTable: React.FunctionComponent = ({ name: i18n.translate('xpack.idxMgmt.templateList.table.contentColumnTitle', { defaultMessage: 'Content', }), - truncateText: true, + width: '120px', render: (item: TemplateListItem) => ( = ({ name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', { defaultMessage: 'Actions', }), + width: '120px', actions: [ { name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', { @@ -153,7 +134,7 @@ export const TemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name); }, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, { type: 'icon', @@ -182,7 +163,7 @@ export const TemplateTable: React.FunctionComponent = ({ setTemplatesToDelete([{ name, isLegacy }]); }, isPrimary: true, - enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', }, ], }, @@ -202,13 +183,13 @@ export const TemplateTable: React.FunctionComponent = ({ const selectionConfig = { onSelectionChange: setSelection, - selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + selectable: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged', selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( - 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + 'xpack.idxMgmt.templateList.table.deleteCloudManagedTemplateTooltip', { - defaultMessage: 'You cannot delete a managed template.', + defaultMessage: 'You cannot delete a cloud-managed template.', } ); } diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 6ecefe18b1a61e..29fd2e02120fc1 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -85,11 +85,11 @@ export const TemplateEdit: React.FunctionComponent): boolean => { + return obj === undefined || Object.keys(obj).length === 0 ? false : true; +}; export const getTemplate = ({ name = getRandomString(), @@ -13,31 +17,35 @@ export const getTemplate = ({ order = getRandomNumber(), indexPatterns = [], template: { settings, aliases, mappings } = {}, - isManaged = false, - isCloudManaged = false, hasDatastream = false, isLegacy = false, + type = 'default', }: Partial< TemplateDeserialized & { isLegacy?: boolean; - isManaged: boolean; - isCloudManaged: boolean; + type?: TemplateType; hasDatastream: boolean; } -> = {}): TemplateDeserialized => ({ - name, - version, - order, - indexPatterns, - template: { - aliases, - mappings, - settings, - }, - _kbnMeta: { - isManaged, - isCloudManaged, - hasDatastream, - isLegacy, - }, -}); +> = {}): TemplateDeserialized & TemplateListItem => { + const indexTemplate = { + name, + version, + order, + indexPatterns, + template: { + aliases, + mappings, + settings, + }, + hasSettings: objHasProperties(settings), + hasMappings: objHasProperties(mappings), + hasAliases: objHasProperties(aliases), + _kbnMeta: { + type, + hasDatastream, + isLegacy, + }, + }; + + return indexTemplate; +}; diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js index a563b956df3445..d24a856399f10b 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.helpers.js @@ -14,7 +14,12 @@ export const registerHelpers = ({ supertest }) => { const getOneTemplate = (name, isLegacy = false) => supertest.get(`${API_BASE_PATH}/index_templates/${name}?legacy=${isLegacy}`); - const getTemplatePayload = (name, indexPatterns = INDEX_PATTERNS, isLegacy = false) => { + const getTemplatePayload = ( + name, + indexPatterns = INDEX_PATTERNS, + isLegacy = false, + type = 'default' + ) => { const baseTemplate = { name, indexPatterns, @@ -48,6 +53,7 @@ export const registerHelpers = ({ supertest }) => { }, _kbnMeta: { isLegacy, + type, }, }; From 09da11047df7348e05a8eb75da82bd3fd27a294c Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:33:44 -0400 Subject: [PATCH 09/15] [SECURITY_SOLUTION] adjust policy onboarding view, check for Ingest permissions (#70536) * adjust policy onboarding view * correct test subj * fix tests * re-enable tests * add no permissions view * adjust onbording look * adjust text * use ingest hook, add tests * adjust text * address comments * beta badges * fix test * correct timeline flyout Co-authored-by: Elastic Machine --- .../utils/timeline/use_show_timeline.tsx | 2 +- .../components/management_empty_state.tsx | 324 ++++++++++-------- .../pages/endpoint_hosts/view/index.tsx | 28 +- .../public/management/pages/index.test.tsx | 36 ++ .../public/management/pages/index.tsx | 42 +++ .../pages/policy/view/policy_list.test.tsx | 4 +- .../pages/policy/view/policy_list.tsx | 26 +- .../public/overview/pages/overview.test.tsx | 25 ++ .../public/overview/pages/overview.tsx | 4 +- 9 files changed, 327 insertions(+), 164 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index a9c6660ba9c68b..14c38c5d6dab69 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -7,7 +7,7 @@ import { useState, useEffect } from 'react'; import { useRouteSpy } from '../route/use_route_spy'; -const hideTimelineForRoutes = [`/cases/configure`, '/management']; +const hideTimelineForRoutes = [`/cases/configure`, '/administration']; export const useShowTimeline = () => { const [{ pageName, pathName }] = useRouteSpy(); diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index c3d6cb48e4dae7..6486b1f3be6d12 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -16,6 +16,7 @@ import { EuiSelectable, EuiSelectableMessage, EuiSelectableProps, + EuiIcon, EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -35,71 +36,121 @@ const PolicyEmptyState = React.memo<{ onActionClick: (event: MouseEvent) => void; actionDisabled?: boolean; }>(({ loading, onActionClick, actionDisabled }) => { - const policySteps = useMemo( - () => [ - { - title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', { - defaultMessage: 'Head over to Ingest Manager.', - }), - children: ( - - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', { - defaultMessage: 'We’ll create a recommended security policy for you.', - }), - children: ( - - - - ), - }, - { - title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', { - defaultMessage: 'Enroll your agents through Fleet.', - }), - children: ( - - - - ), - }, - ], - [] - ); - return ( - - } - bodyComponent={ - - } - /> +
+ {loading ? ( + + + + + + ) : ( + + + +

+ +

+
+ + + + + + + + + + + + + + + + + +

+ +

+
+
+
+ + + + +
+ + + + + + + +

+ +

+
+
+
+ + + + +
+
+ + + + + + + + + + + + +
+ + + +
+ )} +
); }); @@ -114,17 +165,17 @@ const HostsEmptyState = React.memo<{ () => [ { title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepOneTitle', { - defaultMessage: 'Select a policy you created from the list below.', + defaultMessage: 'Select the policy you want to use to protect your hosts', }), children: ( <> - + - + - - + + + + + + + + + + + + ), }, ], - [selectionOptions, handleSelectableOnChange, loading] + [selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick] ); return ( } bodyComponent={ } /> @@ -198,80 +265,45 @@ const HostsEmptyState = React.memo<{ const ManagementEmptyState = React.memo<{ loading: boolean; - onActionClick?: (event: MouseEvent) => void; - actionDisabled?: boolean; - actionButton?: JSX.Element; dataTestSubj: string; steps?: ManagementStep[]; headerComponent: JSX.Element; bodyComponent: JSX.Element; -}>( - ({ - loading, - onActionClick, - actionDisabled, - dataTestSubj, - steps, - actionButton, - headerComponent, - bodyComponent, - }) => { - return ( -
- {loading ? ( - - - - - - ) : ( - <> - - -

{headerComponent}

-
- - - {bodyComponent} - - - {steps && ( - - - - - - )} +}>(({ loading, dataTestSubj, steps, headerComponent, bodyComponent }) => { + return ( +
+ {loading ? ( + + + + + + ) : ( + <> + + +

{headerComponent}

+
+ + + {bodyComponent} + + + {steps && ( - <> - {actionButton ? ( - actionButton - ) : ( - - - - )} - + - - )} -
- ); - } -); + )} + + )} +
+ ); +}); PolicyEmptyState.displayName = 'PolicyEmptyState'; HostsEmptyState.displayName = 'HostsEmptyState'; ManagementEmptyState.displayName = 'ManagementEmptyState'; -export { PolicyEmptyState, HostsEmptyState, ManagementEmptyState }; +export { PolicyEmptyState, HostsEmptyState }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 492c75607a2555..8edeab15d6a091 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -16,6 +16,9 @@ import { EuiHealth, EuiToolTip, EuiSelectableProps, + EuiBetaBadge, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; @@ -374,14 +377,25 @@ export const HostList = () => { data-test-subj="hostPage" headerLeft={ <> - -

- + + +

+ +

+
+
+ + -

-
+ +

diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx new file mode 100644 index 00000000000000..5ec42671ec3d21 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -0,0 +1,36 @@ +/* + * 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 React from 'react'; + +import { ManagementContainer } from './index'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; + +jest.mock('../../common/hooks/endpoint/ingest_enabled'); + +describe('when in the Admistration tab', () => { + let render: () => ReturnType; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + render = () => mockedContext.render(); + }); + + it('should display the No Permissions view when Ingest is OFF', async () => { + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + const renderResult = render(); + const noIngestPermissions = await renderResult.findByTestId('noIngestPermissions'); + expect(noIngestPermissions).not.toBeNull(); + }); + + it('should display the Management view when Ingest is ON', async () => { + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + const renderResult = render(); + const hostPage = await renderResult.findByTestId('hostPage'); + expect(hostPage).not.toBeNull(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 2cf07b9b4382eb..30800234ab24c3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -7,6 +7,8 @@ import React, { memo } from 'react'; import { useHistory, Route, Switch } from 'react-router-dom'; +import { EuiText, EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { PolicyContainer } from './policy'; import { MANAGEMENT_ROUTING_HOSTS_PATH, @@ -16,9 +18,49 @@ import { import { NotFoundPage } from '../../app/404'; import { HostsContainer } from './endpoint_hosts'; import { getHostListPath } from '../common/routing'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../app/types'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; + +const NoPermissions = memo(() => { + return ( + <> + + } + body={ +

+ + + +

+ } + /> + + + ); +}); +NoPermissions.displayName = 'NoPermissions'; export const ManagementContainer = memo(() => { const history = useHistory(); + const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); + + if (!isIngestEnabled) { + return ; + } + return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index db622ceb87b631..047aa6918736e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -37,9 +37,9 @@ describe('when on the policies page', () => { expect(table).not.toBeNull(); }); - it('should display the onboarding steps', async () => { + it('should display the instructions', async () => { const renderResult = render(); - const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); + const onboardingSteps = await renderResult.findByTestId('policyOnboardingInstructions'); expect(onboardingSteps).not.toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index fc120d9782e674..8a77264c354ad4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -23,6 +23,7 @@ import { EuiConfirmModal, EuiCallOut, EuiButton, + EuiBetaBadge, EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -395,14 +396,25 @@ export const PolicyList = React.memo(() => { data-test-subj="policyListPage" headerLeft={ <> - -

- + + +

+ +

+
+
+ + -

-
+ +

diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 6f13f64ca1bffa..43d8fb10508b7c 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -104,6 +104,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -128,6 +129,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -152,6 +154,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -171,6 +174,7 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -190,6 +194,27 @@ describe('Overview', () => { const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); + + test('it does NOT render the Endpoint banner when Ingest is NOT available', () => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 2a522d3ea8fde1..6563f3c2b824da 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -29,6 +29,7 @@ import { SecurityPageName } from '../../app/types'; import { EndpointNotice } from '../components/endpoint_notice'; import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -64,6 +65,7 @@ const OverviewComponent: React.FC = ({ setDismissMessage(true); addMessage('management', 'dismissEndpointNotice'); }, [addMessage]); + const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); return ( <> @@ -74,7 +76,7 @@ const OverviewComponent: React.FC = ({ - {!dismissMessage && !metadataIndexExists && ( + {!dismissMessage && !metadataIndexExists && isIngestEnabled && ( <> From 9037018ed828afce831f1fc21c30df2a727d6841 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 9 Jul 2020 12:54:32 -0700 Subject: [PATCH 10/15] [Ingest Manager] Integrate beta messaging with Add Data (#71147) * Add methods to register directory notices and header links in tutorials, and use registered components when rendering tutorial directory * Add methods to register module notices components in tutorial pages, and use registered components when rendering tutorial page * Add `moduleName` field to server tutorial schema and test fixure * Surface `moduleName` field from built in tutorials and registered apm tutorial * Export component types * Add KibanaContextProvider to home plugin app render * Move setHttpClient to ingest manager plugin setup() method; add home as optional plugin dep; register tutorial module notice * Fix key prop warnings * Add dismissable tutorial directory notice and corresponding ingest manager global setting field * Add tutorial directory header link and tie it to the state of the dismissible directory notice via observable * Put spacing inside module notice component itself * Check if ingest manager is available in current space Co-authored-by: Elastic Machine --- src/plugins/apm_oss/server/tutorial/index.ts | 2 + .../home/public/application/application.tsx | 16 +- .../components/tutorial/tutorial.js | 18 ++ .../components/tutorial/tutorial.test.js | 3 + .../components/tutorial_directory.js | 60 ++++++- src/plugins/home/public/index.ts | 3 + src/plugins/home/public/plugin.ts | 2 +- .../home/public/services/tutorials/index.ts | 9 +- .../tutorials/tutorial_service.mock.ts | 6 + .../tutorials/tutorial_service.test.ts | 55 ------- .../tutorials/tutorial_service.test.tsx | 151 +++++++++++++++++ .../services/tutorials/tutorial_service.ts | 62 +++++++ .../services/tutorials/lib/tutorial_schema.ts | 1 + .../tutorials/lib/tutorials_registry_types.ts | 1 + .../tutorials/tutorials_registry.test.ts | 1 + .../server/tutorials/activemq_logs/index.ts | 1 + .../tutorials/activemq_metrics/index.ts | 1 + .../tutorials/aerospike_metrics/index.ts | 1 + .../server/tutorials/apache_logs/index.ts | 1 + .../server/tutorials/apache_metrics/index.ts | 1 + .../home/server/tutorials/auditbeat/index.ts | 2 + .../home/server/tutorials/aws_logs/index.ts | 1 + .../server/tutorials/aws_metrics/index.ts | 1 + .../home/server/tutorials/azure_logs/index.ts | 1 + .../server/tutorials/azure_metrics/index.ts | 1 + .../server/tutorials/ceph_metrics/index.ts | 1 + .../home/server/tutorials/cisco_logs/index.ts | 1 + .../server/tutorials/cloudwatch_logs/index.ts | 2 + .../tutorials/cockroachdb_metrics/index.ts | 1 + .../server/tutorials/consul_metrics/index.ts | 1 + .../server/tutorials/coredns_logs/index.ts | 1 + .../server/tutorials/coredns_metrics/index.ts | 1 + .../tutorials/couchbase_metrics/index.ts | 1 + .../server/tutorials/couchdb_metrics/index.ts | 1 + .../server/tutorials/docker_metrics/index.ts | 1 + .../tutorials/dropwizard_metrics/index.ts | 1 + .../tutorials/elasticsearch_logs/index.ts | 1 + .../tutorials/elasticsearch_metrics/index.ts | 1 + .../server/tutorials/envoyproxy_logs/index.ts | 1 + .../tutorials/envoyproxy_metrics/index.ts | 1 + .../server/tutorials/etcd_metrics/index.ts | 1 + .../server/tutorials/golang_metrics/index.ts | 1 + .../tutorials/googlecloud_metrics/index.ts | 1 + .../server/tutorials/haproxy_metrics/index.ts | 1 + .../home/server/tutorials/ibmmq_logs/index.ts | 1 + .../server/tutorials/ibmmq_metrics/index.ts | 1 + .../home/server/tutorials/iis_logs/index.ts | 1 + .../server/tutorials/iis_metrics/index.ts | 1 + .../server/tutorials/iptables_logs/index.ts | 1 + .../home/server/tutorials/kafka_logs/index.ts | 1 + .../server/tutorials/kafka_metrics/index.ts | 1 + .../server/tutorials/kibana_metrics/index.ts | 1 + .../tutorials/kubernetes_metrics/index.ts | 1 + .../server/tutorials/logstash_logs/index.ts | 1 + .../tutorials/logstash_metrics/index.ts | 1 + .../tutorials/memcached_metrics/index.ts | 1 + .../server/tutorials/mongodb_metrics/index.ts | 1 + .../server/tutorials/mssql_metrics/index.ts | 1 + .../server/tutorials/munin_metrics/index.ts | 1 + .../home/server/tutorials/mysql_logs/index.ts | 1 + .../server/tutorials/mysql_metrics/index.ts | 1 + .../home/server/tutorials/nats_logs/index.ts | 1 + .../server/tutorials/nats_metrics/index.ts | 1 + .../home/server/tutorials/netflow/index.ts | 2 + .../home/server/tutorials/nginx_logs/index.ts | 1 + .../server/tutorials/nginx_metrics/index.ts | 1 + .../tutorials/openmetrics_metrics/index.ts | 1 + .../server/tutorials/oracle_metrics/index.ts | 1 + .../server/tutorials/osquery_logs/index.ts | 1 + .../server/tutorials/php_fpm_metrics/index.ts | 1 + .../server/tutorials/postgresql_logs/index.ts | 1 + .../tutorials/postgresql_metrics/index.ts | 1 + .../tutorials/prometheus_metrics/index.ts | 1 + .../tutorials/rabbitmq_metrics/index.ts | 1 + .../home/server/tutorials/redis_logs/index.ts | 1 + .../server/tutorials/redis_metrics/index.ts | 1 + .../redisenterprise_metrics/index.ts | 1 + .../server/tutorials/stan_metrics/index.ts | 1 + .../server/tutorials/statsd_metrics/index.ts | 1 + .../server/tutorials/suricata_logs/index.ts | 1 + .../server/tutorials/system_logs/index.ts | 1 + .../server/tutorials/system_metrics/index.ts | 1 + .../server/tutorials/traefik_logs/index.ts | 1 + .../server/tutorials/traefik_metrics/index.ts | 1 + .../server/tutorials/uptime_monitors/index.ts | 2 + .../server/tutorials/uwsgi_metrics/index.ts | 1 + .../server/tutorials/vsphere_metrics/index.ts | 1 + .../tutorials/windows_event_logs/index.ts | 2 + .../server/tutorials/windows_metrics/index.ts | 1 + .../home/server/tutorials/zeek_logs/index.ts | 1 + .../tutorials/zookeeper_metrics/index.ts | 1 + .../common/types/models/settings.ts | 1 + x-pack/plugins/ingest_manager/kibana.json | 2 +- .../components/home_integration/index.ts | 7 + .../tutorial_directory_notice.tsx | 154 ++++++++++++++++++ .../tutorial_module_notice.tsx | 74 +++++++++ .../applications/ingest_manager/index.tsx | 3 +- .../plugins/ingest_manager/public/plugin.ts | 19 +++ .../server/saved_objects/index.ts | 1 + .../server/types/rest_spec/settings.ts | 1 + 100 files changed, 663 insertions(+), 70 deletions(-) delete mode 100644 src/plugins/home/public/services/tutorials/tutorial_service.test.ts create mode 100644 src/plugins/home/public/services/tutorials/tutorial_service.test.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx diff --git a/src/plugins/apm_oss/server/tutorial/index.ts b/src/plugins/apm_oss/server/tutorial/index.ts index aa775d007de309..42609f7d759174 100644 --- a/src/plugins/apm_oss/server/tutorial/index.ts +++ b/src/plugins/apm_oss/server/tutorial/index.ts @@ -26,6 +26,7 @@ import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constant const apmIntro = i18n.translate('apmOss.tutorial.introduction', { defaultMessage: 'Collect in-depth performance metrics and errors from inside your applications.', }); +const moduleName = 'apm'; export const tutorialProvider = ({ indexPatternTitle, @@ -68,6 +69,7 @@ export const tutorialProvider = ({ name: i18n.translate('apmOss.tutorial.specProvider.name', { defaultMessage: 'APM', }), + moduleName, category: TutorialsCategory.OTHER, shortDescription: apmIntro, longDescription: i18n.translate('apmOss.tutorial.specProvider.longDescription', { diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 3729e4e2aa089d..627bd10d7c2c83 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -20,14 +20,19 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { ScopedHistory } from 'kibana/public'; +import { ScopedHistory, CoreStart } from 'kibana/public'; +import { KibanaContextProvider } from '../../../kibana_react/public'; // @ts-ignore import { HomeApp } from './components/home_app'; import { getServices } from './kibana_services'; import './index.scss'; -export const renderApp = async (element: HTMLElement, history: ScopedHistory) => { +export const renderApp = async ( + element: HTMLElement, + coreStart: CoreStart, + history: ScopedHistory +) => { const homeTitle = i18n.translate('home.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); const { featureCatalogue, chrome } = getServices(); @@ -36,7 +41,12 @@ export const renderApp = async (element: HTMLElement, history: ScopedHistory) => chrome.setBreadcrumbs([{ text: homeTitle }]); - render(, element); + render( + + + , + element + ); // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 576f732278b8e4..8139bc6d38ab1d 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -334,6 +334,23 @@ class TutorialUi extends React.Component { } }; + renderModuleNotices() { + const notices = getServices().tutorialService.getModuleNotices(); + if (notices.length && this.state.tutorial.moduleName) { + return ( + + {notices.map((ModuleNotice, index) => ( + + + + ))} + + ); + } else { + return null; + } + } + render() { let content; if (this.state.notFound) { @@ -382,6 +399,7 @@ class TutorialUi extends React.Component { isBeta={this.state.tutorial.isBeta} /> + {this.renderModuleNotices()}

{this.renderInstructionSetsToggle()}
diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js index 23b0dc50018c18..9944ac4848bc61 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js @@ -28,6 +28,9 @@ jest.mock('../../kibana_services', () => ({ chrome: { setBreadcrumbs: () => {}, }, + tutorialService: { + getModuleNotices: () => [], + }, }), })); jest.mock('../../../../../kibana_react/public', () => { diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 774b23af11ac85..948024ae85dda4 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -30,6 +30,7 @@ import { EuiTab, EuiFlexItem, EuiFlexGrid, + EuiFlexGroup, EuiSpacer, EuiTitle, EuiPageBody, @@ -102,6 +103,7 @@ class TutorialDirectoryUi extends React.Component { this.state = { selectedTabId: openTab, tutorialCards: [], + notices: getServices().tutorialService.getDirectoryNotices(), }; } @@ -227,18 +229,62 @@ class TutorialDirectoryUi extends React.Component { ); }; + renderNotices = () => { + const notices = getServices().tutorialService.getDirectoryNotices(); + return notices.length ? ( + + {notices.map((DirectoryNotice, index) => ( + + + + ))} + + ) : null; + }; + + renderHeaderLinks = () => { + const headerLinks = getServices().tutorialService.getDirectoryHeaderLinks(); + return headerLinks.length ? ( + + {headerLinks.map((HeaderLink, index) => ( + + + + ))} + + ) : null; + }; + + renderHeader = () => { + const notices = this.renderNotices(); + const headerLinks = this.renderHeaderLinks(); + + return ( + <> + + + +

+ +

+
+
+ {headerLinks ? {headerLinks} : null} +
+ {notices} + + ); + }; + render() { return ( - -

- -

-
- + {this.renderHeader()} - {this.renderTabs()} {this.renderTabContent()} diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 587dbe886d505b..dc48332e052de0 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -30,6 +30,9 @@ export { FeatureCatalogueCategory, Environment, TutorialVariables, + TutorialDirectoryNoticeComponent, + TutorialDirectoryHeaderLinkComponent, + TutorialModuleNoticeComponent, } from './services'; export * from '../common/instruction_variant'; import { HomePublicPlugin } from './plugin'; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index d05fce652bd406..6859d916a61afd 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -104,7 +104,7 @@ export class HomePublicPlugin i18n.translate('home.pageTitle', { defaultMessage: 'Home' }) ); const { renderApp } = await import('./application'); - return await renderApp(params.element, params.history); + return await renderApp(params.element, coreStart, params.history); }, }); kibanaLegacy.forwardApp('home', 'home'); diff --git a/src/plugins/home/public/services/tutorials/index.ts b/src/plugins/home/public/services/tutorials/index.ts index 3de1e67204d966..44f0badd531b7c 100644 --- a/src/plugins/home/public/services/tutorials/index.ts +++ b/src/plugins/home/public/services/tutorials/index.ts @@ -17,4 +17,11 @@ * under the License. */ -export { TutorialService, TutorialVariables, TutorialServiceSetup } from './tutorial_service'; +export { + TutorialService, + TutorialVariables, + TutorialServiceSetup, + TutorialDirectoryNoticeComponent, + TutorialDirectoryHeaderLinkComponent, + TutorialModuleNoticeComponent, +} from './tutorial_service'; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index bd604fb231dee2..667730e25a2e3e 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -22,6 +22,9 @@ import { TutorialService, TutorialServiceSetup } from './tutorial_service'; const createSetupMock = (): jest.Mocked => { const setup = { setVariable: jest.fn(), + registerDirectoryNotice: jest.fn(), + registerDirectoryHeaderLink: jest.fn(), + registerModuleNotice: jest.fn(), }; return setup; }; @@ -30,6 +33,9 @@ const createMock = (): jest.Mocked> => { const service = { setup: jest.fn(), getVariables: jest.fn(() => ({})), + getDirectoryNotices: jest.fn(() => []), + getDirectoryHeaderLinks: jest.fn(() => []), + getModuleNotices: jest.fn(() => []), }; service.setup.mockImplementation(createSetupMock); return service; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.ts b/src/plugins/home/public/services/tutorials/tutorial_service.test.ts deleted file mode 100644 index f4bcd71a39e8ae..00000000000000 --- a/src/plugins/home/public/services/tutorials/tutorial_service.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { TutorialService } from './tutorial_service'; - -describe('TutorialService', () => { - describe('setup', () => { - test('allows multiple set calls', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.setVariable('abc', 123); - setup.setVariable('def', 456); - }).not.toThrow(); - }); - - test('throws when same variable is set twice', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.setVariable('abc', 123); - setup.setVariable('abc', 456); - }).toThrow(); - }); - }); - - describe('getVariables', () => { - test('returns empty object', () => { - const service = new TutorialService(); - expect(service.getVariables()).toEqual({}); - }); - - test('returns last state of update calls', () => { - const service = new TutorialService(); - const setup = service.setup(); - setup.setVariable('abc', 123); - setup.setVariable('def', { subKey: 456 }); - expect(service.getVariables()).toEqual({ abc: 123, def: { subKey: 456 } }); - }); - }); -}); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx new file mode 100644 index 00000000000000..2a60550e39d90b --- /dev/null +++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { TutorialService } from './tutorial_service'; + +describe('TutorialService', () => { + describe('setup', () => { + test('allows multiple set variable calls', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.setVariable('abc', 123); + setup.setVariable('def', 456); + }).not.toThrow(); + }); + + test('throws when same variable is set twice', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.setVariable('abc', 123); + setup.setVariable('abc', 456); + }).toThrow(); + }); + + test('allows multiple register directory notice calls', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.registerDirectoryNotice('abc', () =>
); + setup.registerDirectoryNotice('def', () => ); + }).not.toThrow(); + }); + + test('throws when same directory notice is registered twice', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.registerDirectoryNotice('abc', () =>
); + setup.registerDirectoryNotice('abc', () => ); + }).toThrow(); + }); + + test('allows multiple register directory header link calls', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.registerDirectoryHeaderLink('abc', () => 123); + setup.registerDirectoryHeaderLink('def', () => 456); + }).not.toThrow(); + }); + + test('throws when same directory header link is registered twice', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.registerDirectoryHeaderLink('abc', () => 123); + setup.registerDirectoryHeaderLink('abc', () => 456); + }).toThrow(); + }); + + test('allows multiple register module notice calls', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.registerModuleNotice('abc', () =>
); + setup.registerModuleNotice('def', () => ); + }).not.toThrow(); + }); + + test('throws when same module notice is registered twice', () => { + const setup = new TutorialService().setup(); + expect(() => { + setup.registerModuleNotice('abc', () =>
); + setup.registerModuleNotice('abc', () => ); + }).toThrow(); + }); + }); + + describe('getVariables', () => { + test('returns empty object', () => { + const service = new TutorialService(); + expect(service.getVariables()).toEqual({}); + }); + + test('returns last state of update calls', () => { + const service = new TutorialService(); + const setup = service.setup(); + setup.setVariable('abc', 123); + setup.setVariable('def', { subKey: 456 }); + expect(service.getVariables()).toEqual({ abc: 123, def: { subKey: 456 } }); + }); + }); + + describe('getDirectoryNotices', () => { + test('returns empty array', () => { + const service = new TutorialService(); + expect(service.getDirectoryNotices()).toEqual([]); + }); + + test('returns last state of register calls', () => { + const service = new TutorialService(); + const setup = service.setup(); + const notices = [() =>
, () => ]; + setup.registerDirectoryNotice('abc', notices[0]); + setup.registerDirectoryNotice('def', notices[1]); + expect(service.getDirectoryNotices()).toEqual(notices); + }); + }); + + describe('getDirectoryHeaderLinks', () => { + test('returns empty array', () => { + const service = new TutorialService(); + expect(service.getDirectoryHeaderLinks()).toEqual([]); + }); + + test('returns last state of register calls', () => { + const service = new TutorialService(); + const setup = service.setup(); + const links = [() => 123, () => 456]; + setup.registerDirectoryHeaderLink('abc', links[0]); + setup.registerDirectoryHeaderLink('def', links[1]); + expect(service.getDirectoryHeaderLinks()).toEqual(links); + }); + }); + + describe('getModuleNotices', () => { + test('returns empty array', () => { + const service = new TutorialService(); + expect(service.getModuleNotices()).toEqual([]); + }); + + test('returns last state of register calls', () => { + const service = new TutorialService(); + const setup = service.setup(); + const notices = [() =>
, () => ]; + setup.registerModuleNotice('abc', notices[0]); + setup.registerModuleNotice('def', notices[1]); + expect(service.getModuleNotices()).toEqual(notices); + }); + }); +}); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index 38297a64373152..538cea1c704581 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -16,12 +16,29 @@ * specific language governing permissions and limitations * under the License. */ +import React from 'react'; /** @public */ export type TutorialVariables = Partial>; +/** @public */ +export type TutorialDirectoryNoticeComponent = React.FC; + +/** @public */ +export type TutorialDirectoryHeaderLinkComponent = React.FC; + +/** @public */ +export type TutorialModuleNoticeComponent = React.FC<{ + moduleName: string; +}>; + export class TutorialService { private tutorialVariables: TutorialVariables = {}; + private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {}; + private tutorialDirectoryHeaderLinks: { + [key: string]: TutorialDirectoryHeaderLinkComponent; + } = {}; + private tutorialModuleNotices: { [key: string]: TutorialModuleNoticeComponent } = {}; public setup() { return { @@ -34,12 +51,57 @@ export class TutorialService { } this.tutorialVariables[key] = value; }, + + /** + * Registers a component that will be rendered at the top of tutorial directory page. + */ + registerDirectoryNotice: (id: string, component: TutorialDirectoryNoticeComponent) => { + if (this.tutorialDirectoryNotices[id]) { + throw new Error(`directory notice ${id} already set`); + } + this.tutorialDirectoryNotices[id] = component; + }, + + /** + * Registers a component that will be rendered next to tutorial directory title/header area. + */ + registerDirectoryHeaderLink: ( + id: string, + component: TutorialDirectoryHeaderLinkComponent + ) => { + if (this.tutorialDirectoryHeaderLinks[id]) { + throw new Error(`directory header link ${id} already set`); + } + this.tutorialDirectoryHeaderLinks[id] = component; + }, + + /** + * Registers a component that will be rendered in the description of a tutorial that is associated with a module. + */ + registerModuleNotice: (id: string, component: TutorialModuleNoticeComponent) => { + if (this.tutorialModuleNotices[id]) { + throw new Error(`module notice ${id} already set`); + } + this.tutorialModuleNotices[id] = component; + }, }; } public getVariables() { return this.tutorialVariables; } + + public getDirectoryNotices() { + return Object.values(this.tutorialDirectoryNotices); + } + + public getDirectoryHeaderLinks() { + return Object.values(this.tutorialDirectoryHeaderLinks); + } + + public getModuleNotices() { + return Object.values(this.tutorialModuleNotices); + } } export type TutorialServiceSetup = ReturnType; diff --git a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts index 32e5483b8b0703..bf28212624a4d7 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts @@ -110,6 +110,7 @@ export const tutorialSchema = { .required(), category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(), name: Joi.string().required(), + moduleName: Joi.string(), isBeta: Joi.boolean().default(false), shortDescription: Joi.string().required(), euiIconType: Joi.string(), // EUI icon type string, one of https://elastic.github.io/eui/#/icons diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts index 210d563696667b..a6b70cd70c02d7 100644 --- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts +++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts @@ -80,6 +80,7 @@ export interface TutorialSchema { id: string; category: TutorialsCategory; name: string; + moduleName?: string; isBeta?: boolean; shortDescription: string; euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/display/icons; diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts index 8144fef2d92e46..b91a265da7d18f 100644 --- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts +++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts @@ -54,6 +54,7 @@ const VALID_TUTORIAL: TutorialSchema = { id: 'test', category: 'logging' as TutorialsCategory, name: 'new tutorial provider', + moduleName: 'test', isBeta: false, shortDescription: 'short description', euiIconType: 'alert', diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index e85100996d4a16..c11c070637ae1b 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -37,6 +37,7 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.activemqLogs.nameTitle', { defaultMessage: 'ActiveMQ logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.activemqLogs.shortDescription', { defaultMessage: 'Collect ActiveMQ logs with Filebeat.', diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts index 088c5db4c6137a..e00ffb4773bea8 100644 --- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts @@ -36,6 +36,7 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS name: i18n.translate('home.tutorials.activemqMetrics.nameTitle', { defaultMessage: 'ActiveMQ metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.activemqMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.', diff --git a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts index 58ab2dcf0986f9..c65022c1875c4c 100644 --- a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts @@ -36,6 +36,7 @@ export function aerospikeMetricsSpecProvider(context: TutorialContext): Tutorial name: i18n.translate('home.tutorials.aerospikeMetrics.nameTitle', { defaultMessage: 'Aerospike metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.aerospikeMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts index 434f0b0b83f98b..94fa9ad1258ec0 100644 --- a/src/plugins/home/server/tutorials/apache_logs/index.ts +++ b/src/plugins/home/server/tutorials/apache_logs/index.ts @@ -37,6 +37,7 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.apacheLogs.nameTitle', { defaultMessage: 'Apache logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.apacheLogs.shortDescription', { defaultMessage: 'Collect and parse access and error logs created by the Apache HTTP server.', diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts index 1521c9820c400e..91de90b9f6c6b2 100644 --- a/src/plugins/home/server/tutorials/apache_metrics/index.ts +++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts @@ -36,6 +36,7 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.apacheMetrics.nameTitle', { defaultMessage: 'Apache metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.apacheMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from the Apache 2 HTTP server.', diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts index 214fda5a7cc538..44a97bfce6cef5 100644 --- a/src/plugins/home/server/tutorials/auditbeat/index.ts +++ b/src/plugins/home/server/tutorials/auditbeat/index.ts @@ -31,11 +31,13 @@ import { export function auditbeatSpecProvider(context: TutorialContext): TutorialSchema { const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + const moduleName = 'auditbeat'; return { id: 'auditbeat', name: i18n.translate('home.tutorials.auditbeat.nameTitle', { defaultMessage: 'Auditbeat', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.auditbeat.shortDescription', { defaultMessage: 'Collect audit data from your hosts.', diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts index 2fa22fa2c2d700..b875d93952c7a4 100644 --- a/src/plugins/home/server/tutorials/aws_logs/index.ts +++ b/src/plugins/home/server/tutorials/aws_logs/index.ts @@ -37,6 +37,7 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema { name: i18n.translate('home.tutorials.awsLogs.nameTitle', { defaultMessage: 'AWS S3 based logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.awsLogs.shortDescription', { defaultMessage: 'Collect AWS logs from S3 bucket with Filebeat.', diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts index c52620e150b5fe..549e98280bef22 100644 --- a/src/plugins/home/server/tutorials/aws_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts @@ -36,6 +36,7 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.awsMetrics.nameTitle', { defaultMessage: 'AWS metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.awsMetrics.shortDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/azure_logs/index.ts b/src/plugins/home/server/tutorials/azure_logs/index.ts index 06aef411775f1a..3624bea96b465c 100644 --- a/src/plugins/home/server/tutorials/azure_logs/index.ts +++ b/src/plugins/home/server/tutorials/azure_logs/index.ts @@ -37,6 +37,7 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.azureLogs.nameTitle', { defaultMessage: 'Azure logs', }), + moduleName, isBeta: true, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.azureLogs.shortDescription', { diff --git a/src/plugins/home/server/tutorials/azure_metrics/index.ts b/src/plugins/home/server/tutorials/azure_metrics/index.ts index c11b3ac0139bae..ac92d70fc64f5f 100644 --- a/src/plugins/home/server/tutorials/azure_metrics/index.ts +++ b/src/plugins/home/server/tutorials/azure_metrics/index.ts @@ -36,6 +36,7 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.azureMetrics.nameTitle', { defaultMessage: 'Azure metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.azureMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/ceph_metrics/index.ts b/src/plugins/home/server/tutorials/ceph_metrics/index.ts index 968a0a3f66b0a3..71e540454bc3a7 100644 --- a/src/plugins/home/server/tutorials/ceph_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ceph_metrics/index.ts @@ -36,6 +36,7 @@ export function cephMetricsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.cephMetrics.nameTitle', { defaultMessage: 'Ceph metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.cephMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index 2322f503b80ce5..b771744a069c3d 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -37,6 +37,7 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.ciscoLogs.nameTitle', { defaultMessage: 'Cisco', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.ciscoLogs.shortDescription', { defaultMessage: 'Collect and parse logs received from Cisco ASA firewalls.', diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index 9d33d9bf786d01..fb7b07c5dc1af2 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -30,11 +30,13 @@ import { } from '../../services/tutorials/lib/tutorials_registry_types'; export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'aws'; return { id: 'cloudwatchLogs', name: i18n.translate('home.tutorials.cloudwatchLogs.nameTitle', { defaultMessage: 'AWS Cloudwatch logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.cloudwatchLogs.shortDescription', { defaultMessage: 'Collect Cloudwatch logs with Functionbeat.', diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts index 96c02f24e347ae..1cb318c83bd34b 100644 --- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts @@ -36,6 +36,7 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori name: i18n.translate('home.tutorials.cockroachdbMetrics.nameTitle', { defaultMessage: 'CockroachDB metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.cockroachdbMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from the CockroachDB server.', diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts index 8bf4333cb018f5..e389db502a769f 100644 --- a/src/plugins/home/server/tutorials/consul_metrics/index.ts +++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts @@ -36,6 +36,7 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.consulMetrics.nameTitle', { defaultMessage: 'Consul metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.consulMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from the Consul server.', diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index 4304fb7acb9079..7fc8a2402d2167 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -37,6 +37,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.corednsLogs.nameTitle', { defaultMessage: 'CoreDNS logs', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.corednsLogs.shortDescription', { defaultMessage: 'Collect the logs created by Coredns.', diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts index 44bd0cb3999f65..c6589715ba9ce4 100644 --- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts +++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts @@ -36,6 +36,7 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.corednsMetrics.nameTitle', { defaultMessage: 'CoreDNS metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.corednsMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from the CoreDNS server.', diff --git a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts index efd59029c9c502..370541c9324d8e 100644 --- a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts @@ -36,6 +36,7 @@ export function couchbaseMetricsSpecProvider(context: TutorialContext): Tutorial name: i18n.translate('home.tutorials.couchbaseMetrics.nameTitle', { defaultMessage: 'Couchbase metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.couchbaseMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts index 1fbaa448172262..8d70fcf2a6cd72 100644 --- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts @@ -36,6 +36,7 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.couchdbMetrics.nameTitle', { defaultMessage: 'CouchDB metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.couchdbMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from the CouchdB server.', diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts index 8c603697c47136..2e0c3ccb642dd8 100644 --- a/src/plugins/home/server/tutorials/docker_metrics/index.ts +++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts @@ -36,6 +36,7 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.dockerMetrics.nameTitle', { defaultMessage: 'Docker metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.dockerMetrics.shortDescription', { defaultMessage: 'Fetch metrics about your Docker containers.', diff --git a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts index 008a7a9b3a6970..d74db4b2ad9580 100644 --- a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts +++ b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts @@ -36,6 +36,7 @@ export function dropwizardMetricsSpecProvider(context: TutorialContext): Tutoria name: i18n.translate('home.tutorials.dropwizardMetrics.nameTitle', { defaultMessage: 'Dropwizard metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.dropwizardMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts index 515b06ea82a5ef..f6c280d29f67fc 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts @@ -37,6 +37,7 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria name: i18n.translate('home.tutorials.elasticsearchLogs.nameTitle', { defaultMessage: 'Elasticsearch logs', }), + moduleName, category: TutorialsCategory.LOGGING, isBeta: true, shortDescription: i18n.translate('home.tutorials.elasticsearchLogs.shortDescription', { diff --git a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts index ea6dcf86d23e21..38713056e0640a 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts @@ -36,6 +36,7 @@ export function elasticsearchMetricsSpecProvider(context: TutorialContext): Tuto name: i18n.translate('home.tutorials.elasticsearchMetrics.nameTitle', { defaultMessage: 'Elasticsearch metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.elasticsearchMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index a9b9c33d61bdf1..0cf032e6b90c15 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -37,6 +37,7 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.envoyproxyLogs.nameTitle', { defaultMessage: 'Envoyproxy', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.envoyproxyLogs.shortDescription', { defaultMessage: 'Collect and parse logs received from the Envoy proxy.', diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts index adc7a494200c17..9b453370fb8026 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts @@ -36,6 +36,7 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria name: i18n.translate('home.tutorials.envoyproxyMetrics.nameTitle', { defaultMessage: 'Envoy Proxy metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.envoyproxyMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from Envoy Proxy.', diff --git a/src/plugins/home/server/tutorials/etcd_metrics/index.ts b/src/plugins/home/server/tutorials/etcd_metrics/index.ts index 2956473b6643bf..48bdba5abb4b34 100644 --- a/src/plugins/home/server/tutorials/etcd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/etcd_metrics/index.ts @@ -36,6 +36,7 @@ export function etcdMetricsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.etcdMetrics.nameTitle', { defaultMessage: 'Etcd metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.etcdMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/golang_metrics/index.ts b/src/plugins/home/server/tutorials/golang_metrics/index.ts index c53f8b2bba2817..e5ecbb9eb583b3 100644 --- a/src/plugins/home/server/tutorials/golang_metrics/index.ts +++ b/src/plugins/home/server/tutorials/golang_metrics/index.ts @@ -36,6 +36,7 @@ export function golangMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.golangMetrics.nameTitle', { defaultMessage: 'Golang metrics', }), + moduleName, isBeta: true, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.golangMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts b/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts index 504ede04c12d8e..42dc0720c10e0d 100644 --- a/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts +++ b/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts @@ -36,6 +36,7 @@ export function googlecloudMetricsSpecProvider(context: TutorialContext): Tutori name: i18n.translate('home.tutorials.googlecloudMetrics.nameTitle', { defaultMessage: 'Google Cloud metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.googlecloudMetrics.shortDescription', { defaultMessage: diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts index f06dfaa93063c0..49e2ec4390db9f 100644 --- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts @@ -36,6 +36,7 @@ export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.haproxyMetrics.nameTitle', { defaultMessage: 'HAProxy metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.haproxyMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts index 5739c03954def0..8f67b88c3fcf2b 100644 --- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts @@ -37,6 +37,7 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.ibmmqLogs.nameTitle', { defaultMessage: 'IBM MQ logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.ibmmqLogs.shortDescription', { defaultMessage: 'Collect IBM MQ logs with Filebeat.', diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts index 4f20b2d0684fc9..dc941233b02333 100644 --- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts @@ -36,6 +36,7 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.ibmmqMetrics.nameTitle', { defaultMessage: 'IBM MQ metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.ibmmqMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from IBM MQ instances.', diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts index fee8d036db757e..12411fc792e64e 100644 --- a/src/plugins/home/server/tutorials/iis_logs/index.ts +++ b/src/plugins/home/server/tutorials/iis_logs/index.ts @@ -37,6 +37,7 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { name: i18n.translate('home.tutorials.iisLogs.nameTitle', { defaultMessage: 'IIS logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.iisLogs.shortDescription', { defaultMessage: 'Collect and parse access and error logs created by the IIS HTTP server.', diff --git a/src/plugins/home/server/tutorials/iis_metrics/index.ts b/src/plugins/home/server/tutorials/iis_metrics/index.ts index 46621677a67ceb..d6dc5a2e33704d 100644 --- a/src/plugins/home/server/tutorials/iis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/iis_metrics/index.ts @@ -36,6 +36,7 @@ export function iisMetricsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.iisMetrics.nameTitle', { defaultMessage: 'IIS Metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.iisMetrics.shortDescription', { defaultMessage: 'Collect IIS server related metrics.', diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index fd84894dae8508..b3be1337674476 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -37,6 +37,7 @@ export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.iptablesLogs.nameTitle', { defaultMessage: 'Iptables / Ubiquiti', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.iptablesLogs.shortDescription', { defaultMessage: 'Collect and parse iptables and ip6tables logs or from Ubiqiti firewalls.', diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts index 746e65b71008c1..aac172520829c3 100644 --- a/src/plugins/home/server/tutorials/kafka_logs/index.ts +++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts @@ -37,6 +37,7 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.kafkaLogs.nameTitle', { defaultMessage: 'Kafka logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.kafkaLogs.shortDescription', { defaultMessage: 'Collect and parse logs created by Kafka.', diff --git a/src/plugins/home/server/tutorials/kafka_metrics/index.ts b/src/plugins/home/server/tutorials/kafka_metrics/index.ts index 55860a3ab649a4..1b0ce44db65503 100644 --- a/src/plugins/home/server/tutorials/kafka_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kafka_metrics/index.ts @@ -36,6 +36,7 @@ export function kafkaMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.kafkaMetrics.nameTitle', { defaultMessage: 'Kafka metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kafkaMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/kibana_metrics/index.ts b/src/plugins/home/server/tutorials/kibana_metrics/index.ts index fa966ac724a734..d595859959aca3 100644 --- a/src/plugins/home/server/tutorials/kibana_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kibana_metrics/index.ts @@ -36,6 +36,7 @@ export function kibanaMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.kibanaMetrics.nameTitle', { defaultMessage: 'Kibana metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kibanaMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts index bcea7f1221e1f1..a4ce9cfab5f627 100644 --- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts @@ -36,6 +36,7 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria name: i18n.translate('home.tutorials.kubernetesMetrics.nameTitle', { defaultMessage: 'Kubernetes metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.kubernetesMetrics.shortDescription', { defaultMessage: 'Fetch metrics from your Kubernetes installation.', diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 69e498ac59459d..32982cd1055a4c 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -37,6 +37,7 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.logstashLogs.nameTitle', { defaultMessage: 'Logstash logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.logstashLogs.shortDescription', { defaultMessage: 'Collect and parse debug and slow logs created by Logstash itself.', diff --git a/src/plugins/home/server/tutorials/logstash_metrics/index.ts b/src/plugins/home/server/tutorials/logstash_metrics/index.ts index 383273a8c365cc..11272b7ceef6bd 100644 --- a/src/plugins/home/server/tutorials/logstash_metrics/index.ts +++ b/src/plugins/home/server/tutorials/logstash_metrics/index.ts @@ -36,6 +36,7 @@ export function logstashMetricsSpecProvider(context: TutorialContext): TutorialS name: i18n.translate('home.tutorials.logstashMetrics.nameTitle', { defaultMessage: 'Logstash metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.logstashMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/memcached_metrics/index.ts b/src/plugins/home/server/tutorials/memcached_metrics/index.ts index 94451556ad34c6..c724b790f84a6c 100644 --- a/src/plugins/home/server/tutorials/memcached_metrics/index.ts +++ b/src/plugins/home/server/tutorials/memcached_metrics/index.ts @@ -36,6 +36,7 @@ export function memcachedMetricsSpecProvider(context: TutorialContext): Tutorial name: i18n.translate('home.tutorials.memcachedMetrics.nameTitle', { defaultMessage: 'Memcached metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.memcachedMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts index f02695e207dd32..2f39a048f2f154 100644 --- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts @@ -36,6 +36,7 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.mongodbMetrics.nameTitle', { defaultMessage: 'MongoDB metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mongodbMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from MongoDB.', diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts index 4b418587f78b2c..1a1f047a128481 100644 --- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts @@ -36,6 +36,7 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.mssqlMetrics.nameTitle', { defaultMessage: 'Microsoft SQL Server Metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mssqlMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from a Microsoft SQL Server instance', diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts index 3dbb34cb22031e..8434d916daa1f6 100644 --- a/src/plugins/home/server/tutorials/munin_metrics/index.ts +++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts @@ -36,6 +36,7 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.muninMetrics.nameTitle', { defaultMessage: 'Munin metrics', }), + moduleName, euiIconType: '/plugins/home/assets/logos/munin.svg', isBeta: true, category: TutorialsCategory.METRICS, diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts index 178a371f9212ed..37bbf409b91c5b 100644 --- a/src/plugins/home/server/tutorials/mysql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts @@ -37,6 +37,7 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.mysqlLogs.nameTitle', { defaultMessage: 'MySQL logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.mysqlLogs.shortDescription', { defaultMessage: 'Collect and parse error and slow logs created by MySQL.', diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts index 1148caeb441f88..89f5edf22a7b65 100644 --- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts @@ -36,6 +36,7 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.mysqlMetrics.nameTitle', { defaultMessage: 'MySQL metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.mysqlMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from MySQL.', diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts index 17c37755b6bc37..f00ddd6ca88793 100644 --- a/src/plugins/home/server/tutorials/nats_logs/index.ts +++ b/src/plugins/home/server/tutorials/nats_logs/index.ts @@ -37,6 +37,7 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { name: i18n.translate('home.tutorials.natsLogs.nameTitle', { defaultMessage: 'NATS logs', }), + moduleName, category: TutorialsCategory.LOGGING, isBeta: true, shortDescription: i18n.translate('home.tutorials.natsLogs.shortDescription', { diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts index bce08e85c6977e..cda011297d2c67 100644 --- a/src/plugins/home/server/tutorials/nats_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts @@ -36,6 +36,7 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.natsMetrics.nameTitle', { defaultMessage: 'NATS metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.natsMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from the Nats server.', diff --git a/src/plugins/home/server/tutorials/netflow/index.ts b/src/plugins/home/server/tutorials/netflow/index.ts index ec0aa8953b146f..5be30bbb152b73 100644 --- a/src/plugins/home/server/tutorials/netflow/index.ts +++ b/src/plugins/home/server/tutorials/netflow/index.ts @@ -25,9 +25,11 @@ import { createElasticCloudInstructions } from './elastic_cloud'; import { createOnPremElasticCloudInstructions } from './on_prem_elastic_cloud'; export function netflowSpecProvider() { + const moduleName = 'netflow'; return { id: 'netflow', name: 'Netflow', + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.netflow.tutorialShortDescription', { defaultMessage: 'Collect Netflow records sent by a Netflow exporter.', diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts index 37d0cc106bfe58..f357e77fc25ca3 100644 --- a/src/plugins/home/server/tutorials/nginx_logs/index.ts +++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts @@ -37,6 +37,7 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.nginxLogs.nameTitle', { defaultMessage: 'Nginx logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.nginxLogs.shortDescription', { defaultMessage: 'Collect and parse access and error logs created by the Nginx HTTP server.', diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts index 8671f7218ffc8d..09031883cef1c4 100644 --- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts @@ -36,6 +36,7 @@ export function nginxMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.nginxMetrics.nameTitle', { defaultMessage: 'Nginx metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.nginxMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from the Nginx HTTP server.', diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts index eb539e15c1bcd9..197821f24dddb2 100644 --- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts +++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts @@ -36,6 +36,7 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori name: i18n.translate('home.tutorials.openmetricsMetrics.nameTitle', { defaultMessage: 'OpenMetrics metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.openmetricsMetrics.shortDescription', { defaultMessage: 'Fetch metrics from an endpoint that serves metrics in OpenMetrics format.', diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts index 3144b0a21aab5f..d2ddd19b930a22 100644 --- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts +++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts @@ -36,6 +36,7 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.oracleMetrics.nameTitle', { defaultMessage: 'oracle metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.oracleMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index 8781d6201a7710..c4869a889a085a 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -37,6 +37,7 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.osqueryLogs.nameTitle', { defaultMessage: 'Osquery logs', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.osqueryLogs.shortDescription', { defaultMessage: 'Collect the result logs created by osqueryd.', diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts index 975b549c9520b3..470cfed2176fd0 100644 --- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts +++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts @@ -36,6 +36,7 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.phpFpmMetrics.nameTitle', { defaultMessage: 'PHP-FPM metrics', }), + moduleName, category: TutorialsCategory.METRICS, isBeta: false, shortDescription: i18n.translate('home.tutorials.phpFpmMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts index 0c280619858196..e158dedcb03e04 100644 --- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts @@ -37,6 +37,7 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.postgresqlLogs.nameTitle', { defaultMessage: 'PostgreSQL logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.postgresqlLogs.shortDescription', { defaultMessage: 'Collect and parse error and slow logs created by PostgreSQL.', diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts index f9bb9d249e755d..1add49c10c2a75 100644 --- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts @@ -36,6 +36,7 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria name: i18n.translate('home.tutorials.postgresqlMetrics.nameTitle', { defaultMessage: 'PostgreSQL metrics', }), + moduleName, category: TutorialsCategory.METRICS, isBeta: false, shortDescription: i18n.translate('home.tutorials.postgresqlMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts index 06e8a138049d53..900c5da7cdbe34 100644 --- a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts +++ b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts @@ -36,6 +36,7 @@ export function prometheusMetricsSpecProvider(context: TutorialContext): Tutoria name: i18n.translate('home.tutorials.prometheusMetrics.nameTitle', { defaultMessage: 'Prometheus metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.prometheusMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts index a646068e4ff341..df0aa57d9feac9 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts @@ -36,6 +36,7 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS name: i18n.translate('home.tutorials.rabbitmqMetrics.nameTitle', { defaultMessage: 'RabbitMQ metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.rabbitmqMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from the RabbitMQ server.', diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts index e017fae0499a3a..785118b9e5d098 100644 --- a/src/plugins/home/server/tutorials/redis_logs/index.ts +++ b/src/plugins/home/server/tutorials/redis_logs/index.ts @@ -37,6 +37,7 @@ export function redisLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.redisLogs.nameTitle', { defaultMessage: 'Redis logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.redisLogs.shortDescription', { defaultMessage: 'Collect and parse error and slow logs created by Redis.', diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts index bcc4d9bb0b67b9..11d05029844b20 100644 --- a/src/plugins/home/server/tutorials/redis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts @@ -36,6 +36,7 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.redisMetrics.nameTitle', { defaultMessage: 'Redis metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.redisMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from Redis.', diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts index ffbb5ab75da879..0bc7769f950ede 100644 --- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts @@ -36,6 +36,7 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu name: i18n.translate('home.tutorials.redisenterpriseMetrics.nameTitle', { defaultMessage: 'Redis Enterprise metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.redisenterpriseMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from Redis Enterprise Server.', diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts index 616bc7450249e4..b1ad3e9c1404ac 100644 --- a/src/plugins/home/server/tutorials/stan_metrics/index.ts +++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts @@ -36,6 +36,7 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.stanMetrics.nameTitle', { defaultMessage: 'STAN metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.stanMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from the STAN server.', diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts index 1dc297e78c791f..9e9d7d6fd3e236 100644 --- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts @@ -33,6 +33,7 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.statsdMetrics.nameTitle', { defaultMessage: 'Statsd metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.statsdMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from statsd.', diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index 6bcfc1d43a2502..eec81b94966479 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -37,6 +37,7 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.suricataLogs.nameTitle', { defaultMessage: 'Suricata logs', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.suricataLogs.shortDescription', { defaultMessage: 'Collect the result logs created by Suricata IDS/IPS/NSM.', diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index 9bad70699a6ed8..f39df25461a5fe 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -37,6 +37,7 @@ export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema name: i18n.translate('home.tutorials.systemLogs.nameTitle', { defaultMessage: 'System logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.systemLogs.shortDescription', { defaultMessage: 'Collect and parse logs written by the local Syslog server.', diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts index ef1a84ecdbf10b..6bdaaa34a9b2cc 100644 --- a/src/plugins/home/server/tutorials/system_metrics/index.ts +++ b/src/plugins/home/server/tutorials/system_metrics/index.ts @@ -36,6 +36,7 @@ export function systemMetricsSpecProvider(context: TutorialContext): TutorialSch name: i18n.translate('home.tutorials.systemMetrics.nameTitle', { defaultMessage: 'System metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.systemMetrics.shortDescription', { defaultMessage: 'Collect CPU, memory, network, and disk statistics from the host.', diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 1876edd6c0bf74..0a84dcb0818835 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -37,6 +37,7 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem name: i18n.translate('home.tutorials.traefikLogs.nameTitle', { defaultMessage: 'Traefik logs', }), + moduleName, category: TutorialsCategory.LOGGING, shortDescription: i18n.translate('home.tutorials.traefikLogs.shortDescription', { defaultMessage: 'Collect and parse access logs created by the Traefik Proxy.', diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts index a97ee3ab9758a5..4048719239a10c 100644 --- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts +++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts @@ -33,6 +33,7 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.traefikMetrics.nameTitle', { defaultMessage: 'Traefik metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.traefikMetrics.shortDescription', { defaultMessage: 'Fetch monitoring metrics from Traefik.', diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index fa854a1c235053..7366583e597781 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -30,11 +30,13 @@ import { } from '../../services/tutorials/lib/tutorials_registry_types'; export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'uptime'; return { id: 'uptimeMonitors', name: i18n.translate('home.tutorials.uptimeMonitors.nameTitle', { defaultMessage: 'Uptime Monitors', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.uptimeMonitors.shortDescription', { defaultMessage: 'Monitor services for their availability', diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts index bbe4ea78ee87c1..f6398be3550fd3 100644 --- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts +++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts @@ -36,6 +36,7 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.uwsgiMetrics.nameTitle', { defaultMessage: 'uWSGI metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.uwsgiMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from the uWSGI server.', diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts index 4450ab30407505..5e1191ffdf8ce5 100644 --- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts +++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts @@ -36,6 +36,7 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.vsphereMetrics.nameTitle', { defaultMessage: 'vSphere metrics', }), + moduleName, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.vsphereMetrics.shortDescription', { defaultMessage: 'Fetch internal metrics from vSphere.', diff --git a/src/plugins/home/server/tutorials/windows_event_logs/index.ts b/src/plugins/home/server/tutorials/windows_event_logs/index.ts index c2ea9ff3015e43..80f7a58ae14be4 100644 --- a/src/plugins/home/server/tutorials/windows_event_logs/index.ts +++ b/src/plugins/home/server/tutorials/windows_event_logs/index.ts @@ -30,11 +30,13 @@ import { } from '../../services/tutorials/lib/tutorials_registry_types'; export function windowsEventLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'windows'; return { id: 'windowsEventLogs', name: i18n.translate('home.tutorials.windowsEventLogs.nameTitle', { defaultMessage: 'Windows Event Log', }), + moduleName, isBeta: false, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.windowsEventLogs.shortDescription', { diff --git a/src/plugins/home/server/tutorials/windows_metrics/index.ts b/src/plugins/home/server/tutorials/windows_metrics/index.ts index 5333a7b1badf6b..18cdcdc985e544 100644 --- a/src/plugins/home/server/tutorials/windows_metrics/index.ts +++ b/src/plugins/home/server/tutorials/windows_metrics/index.ts @@ -36,6 +36,7 @@ export function windowsMetricsSpecProvider(context: TutorialContext): TutorialSc name: i18n.translate('home.tutorials.windowsMetrics.nameTitle', { defaultMessage: 'Windows metrics', }), + moduleName, isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.windowsMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index c273a93b1b0d50..e39dcd3409490b 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -37,6 +37,7 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { name: i18n.translate('home.tutorials.zeekLogs.nameTitle', { defaultMessage: 'Zeek logs', }), + moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.zeekLogs.shortDescription', { defaultMessage: 'Collect the logs created by Zeek/Bro.', diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts index ae146d192432bc..a39540b7399e58 100644 --- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts +++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts @@ -36,6 +36,7 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial name: i18n.translate('home.tutorials.zookeeperMetrics.nameTitle', { defaultMessage: 'Zookeeper metrics', }), + moduleName, euiIconType: '/plugins/home/assets/logos/zookeeper.svg', isBeta: false, category: TutorialsCategory.METRICS, diff --git a/x-pack/plugins/ingest_manager/common/types/models/settings.ts b/x-pack/plugins/ingest_manager/common/types/models/settings.ts index 2921808230b47f..98d99911f1b3fc 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/settings.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/settings.ts @@ -10,6 +10,7 @@ interface BaseSettings { package_auto_upgrade?: boolean; kibana_url?: string; kibana_ca_sha256?: string; + has_seen_add_data_notice?: boolean; } export interface Settings extends BaseSettings { diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index 181b93a9e24252..877184740166f5 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -5,6 +5,6 @@ "ui": true, "configPath": ["xpack", "ingestManager"], "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features", "cloud", "usageCollection"], + "optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"], "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts new file mode 100644 index 00000000000000..bab6049198249b --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { TutorialDirectoryNotice, TutorialDirectoryHeaderLink } from './tutorial_directory_notice'; +export { TutorialModuleNotice } from './tutorial_module_notice'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx new file mode 100644 index 00000000000000..553623380dcc05 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx @@ -0,0 +1,154 @@ +/* + * 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 React, { memo, useState, useCallback, useEffect } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiLink, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { + TutorialDirectoryNoticeComponent, + TutorialDirectoryHeaderLinkComponent, +} from 'src/plugins/home/public'; +import { sendPutSettings, useGetSettings, useLink, useCapabilities } from '../../hooks'; + +const FlexItemButtonWrapper = styled(EuiFlexItem)` + &&& { + margin-bottom: 0; + } +`; + +const tutorialDirectoryNoticeState$ = new BehaviorSubject({ + settingsDataLoaded: false, + hasSeenNotice: false, +}); + +export const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => { + const { getHref } = useLink(); + const { show: hasIngestManager } = useCapabilities(); + const { data: settingsData, isLoading } = useGetSettings(); + const [dismissedNotice, setDismissedNotice] = useState(false); + + const dismissNotice = useCallback(async () => { + setDismissedNotice(true); + await sendPutSettings({ + has_seen_add_data_notice: true, + }); + }, []); + + useEffect(() => { + tutorialDirectoryNoticeState$.next({ + settingsDataLoaded: !isLoading, + hasSeenNotice: Boolean(dismissedNotice || settingsData?.item?.has_seen_add_data_notice), + }); + }, [isLoading, settingsData, dismissedNotice]); + + const hasSeenNotice = + isLoading || settingsData?.item?.has_seen_add_data_notice || dismissedNotice; + + return hasIngestManager && !hasSeenNotice ? ( + <> + + + + + ), + }} + /> + } + > +

+ + + + ), + }} + /> +

+ + +
+ + + +
+
+ +
+ { + dismissNotice(); + }} + > + + +
+
+
+
+ + ) : null; +}); + +export const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => { + const { getHref } = useLink(); + const { show: hasIngestManager } = useCapabilities(); + const [noticeState, setNoticeState] = useState({ + settingsDataLoaded: false, + hasSeenNotice: false, + }); + + useEffect(() => { + const subscription = tutorialDirectoryNoticeState$.subscribe((value) => setNoticeState(value)); + return () => { + subscription.unsubscribe(); + }; + }, []); + + return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( + + + + ) : null; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx new file mode 100644 index 00000000000000..a26691bdd64a00 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx @@ -0,0 +1,74 @@ +/* + * 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 React, { memo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui'; +import { TutorialModuleNoticeComponent } from 'src/plugins/home/public'; +import { useGetPackages, useLink, useCapabilities } from '../../hooks'; + +export const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => { + const { getHref } = useLink(); + const { show: hasIngestManager } = useCapabilities(); + const { data: packagesData, isLoading } = useGetPackages(); + + const pkgInfo = + !isLoading && + packagesData?.response && + packagesData.response.find((pkg) => pkg.name === moduleName); + + if (hasIngestManager && pkgInfo) { + return ( + <> + + +

+ + + + ), + availableAsIntegrationLink: ( + + + + ), + blogPostLink: ( + + + + ), + }} + /> +

+
+ + ); + } + + return null; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 623df428b7dd92..94d3379f35e051 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -22,7 +22,7 @@ import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; -import { DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; +import { DepsContext, ConfigContext, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks'; import { FleetStatusProvider } from './hooks/use_fleet_status'; @@ -260,7 +260,6 @@ export function renderApp( startDeps: IngestManagerStartDeps, config: IngestManagerConfigType ) { - setHttpClient(coreStart.http); ReactDOM.render( Date: Thu, 9 Jul 2020 13:10:31 -0700 Subject: [PATCH 11/15] New Enterprise Search Kibana plugin (#66922) * Initial App Search in Kibana plugin work - Initializes a new platform plugin that ships out of the box w/ x-pack - Contains a very basic front-end that shows AS engines, error states, or a Setup Guide - Contains a very basic server that remotely calls the AS internal engines API and returns results * Update URL casing to match Kibana best practices - URL casing appears to be snake_casing, but kibana.json casing appears to be camelCase * Register App Search plugin in Home Feature Catalogue * Add custom App Search in Kibana logo - I haven't had much success in surfacing a SVG file via a server-side endpoint/URL, but then I realized EuiIcon supports passing in a ReactElement directly. Woo! * Fix appSearch.host config setting to be optional - instead of crashing folks on load * Rename plugin to Enterprise Search - per product decision, URL should be enterprise_search/app_search and Workplace Search should also eventually live here - reorganize folder structure in anticipation for another workplace_search plugin/codebase living alongside app_search - rename app.tsx/main.tsx to a standard top-level index.tsx (which will contain top-level routes/state) - rename AS->ES files/vars where applicable - TODO: React Router * Set up React Router URL structure * Convert showSetupGuide action/flag to a React Router link - remove showSetupGuide flag - add a new shared helper component for combining EuiButton/EuiLink with React Router behavior (https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51) * Implement Kibana Chrome breadcrumbs - create shared helper (WS will presumably also want this) for generating EUI breadcrumb objects with React Router links+click behavior - create React component that calls chrome.setBreadcrumbs on page mount - clean up type definitions - move app-wide props to IAppSearchProps and update most pages/views to simply import it instead of calling their own definitions * Added server unit tests (#2) * Added unit test for server * PR Feedback * Refactor top-level Kibana props to a global context state - rather them passing them around verbosely as props, the components that need them should be able to call the useContext hook + Remove IAppSearchProps in favor of IKibanaContext + Also rename `appSearchUrl` to `enterpriseSearchUrl`, since this context will contained shared/Kibana-wide values/actions useful to both AS and WS * Added unit tests for public (#4) * application.test.ts * Added Unit Test for EngineOverviewHeader * Added Unit Test for generate_breadcrumbs * Added Unit Test for set_breadcrumb.tsx * Added a unit test for link_events - Also changed link_events.tsx to link_events.ts since it's just TS, no React - Modified letBrowserHandleEvent so it will still return a false boolean when target is blank * Betterize these tests Co-Authored-By: Constance Co-authored-by: Constance * Add UI telemetry tracking to AS in Kibana (#5) * Set up Telemetry usageCollection, savedObjects, route, & shared helper - The Kibana UsageCollection plugin handles collecting our telemetry UI data (views, clicks, errors, etc.) and pushing it to elastic's telemetry servers - That data is stored in incremented in Kibana's savedObjects lib/plugin (as well as mapped) - When an end-user hits a certain view or action, the shared helper will ping the app search telemetry route which increments the savedObject store * Update client-side views/links to new shared telemetry helper * Write tests for new telemetry files * Implement remaining unit tests (#7) * Write tests for React Router+EUI helper components * Update generate_breadcrumbs test - add test suite for generateBreadcrumb() itself (in order to cover a missing branch) - minor lint fixes - remove unnecessary import from set_breadcrumbs test * Write test for get_username util + update test to return a more consistent falsey value (null) * Add test for SetupGuide * [Refactor] Pull out various Kibana context mocks into separate files - I'm creating a reusable useContext mock for shallow()ed enzyme components + add more documentation comments + examples * Write tests for empty state components + test new usecontext shallow mock * Empty state components: Add extra getUserName branch test * Write test for app search index/routes * Write tests for engine overview table + fix bonus bug * Write Engine Overview tests + Update EngineOverview logic to account for issues found during tests :) - Move http to async/await syntax instead of promise syntax (works better with existing HttpServiceMock jest.fn()s) - hasValidData wasn't strict enough in type checking/object nest checking and was causing the app itself to crash (no bueno) * Refactor EngineOverviewHeader test to use shallow + to full coverage - missed adding this test during telemetry work - switching to shallow and beforeAll reduces the test time from 5s to 4s! * [Refactor] Pull out React Router history mocks into a test util helper + minor refactors/updates * Add small tests to increase branch coverage - mostly testing fallbacks or removing fallbacks in favor of strict type interface - these are slightly obsessive so I'd also be fine ditching them if they aren't terribly valuable * Address larger tech debt/TODOs (#8) * Fix optional chaining TODO - turns out my local Prettier wasn't up to date, completely my bad * Fix constants TODO - adds a common folder/architecture for others to use in the future * Remove TODO for eslint-disable-line and specify lint rule being skipped - hopefully that's OK for review, I can't think of any other way to sanely do this without re-architecting the entire file or DDoSing our API * Add server-side logging to route dependencies + add basic example of error catching/logging to Telemetry route + [extra] refactor mockResponseFactory name to something slightly easier to read * Move more Engines Overview API logic/logging to server-side - handle data validation in the server-side - wrap server-side API in a try/catch to account for fetch issues - more correctly return 2xx/4xx statuses and more correctly deal with those responses in the front-end - Add server info/error/debug logs (addresses TODO) - Update tests + minor refactors/cleanup - remove expectResponseToBe200With helper (since we're now returning multiple response types) and instead make mockResponse var name more readable - one-line header auth - update tests with example error logs - update schema validation for `type` to be an enum of `indexed`/`meta` (more accurately reflecting API) * Per telemetry team feedback, rename usageCollection telemetry mapping name to simpler 'app_search' - since their mapping already nests under 'kibana.plugins' - note: I left the savedObjects name with the '_telemetry' suffix, as there very well may be a use case for top-level generic 'app_search' saved objects * Update Setup Guide installation instructions (#9) Co-authored-by: Chris Cressman * [Refactor] DRY out route test helper * [Refactor] Rename public/test_utils to public/__mocks__ - to better follow/use jest setups and for .mock.ts suffixes * Add platinum licensing check to Meta Engines table/call (#11) * Licensing plugin setup * Add LicensingContext setup * Update EngineOverview to not hit meta engines API on platinum license * Add Jest test helpers for future shallow/context use * Update plugin to use new Kibana nav + URL update (#12) * Update new nav categories to add Enterprise Search + update plugin to use new category - per @johnbarrierwilson and Matt Riley, Enterprise Search should be under Kibana and above Observability - Run `node scripts/check_published_api_changes.js --accept` since this new category affects public API * [URL UPDATE] Change '/app/enterprise_search/app_search' to '/app/app_search' - This needs to be done because App Search and Workplace search *have* to be registered as separate plugins to have 2 distinct nav links - Currently Kibana doesn't support nested app names (see: https://github.com/elastic/kibana/issues/59190) but potentially will in the future - To support this change, we need to update applications/index.tsx to NOT handle '/app/enterprise_search' level routing, but instead accept an async imported app component (e.g. AppSearch, WorkplaceSearch). - AppSearch should now treat its router as root '/' instead of '/app_search' - (Addl) Per Josh Dover's recommendation, switch to `` from `` since they're deprecating appBasePath * Update breadcrumbs helper to account for new URLs - Remove path for Enterprise Search breadcrumb, since '/app/enterprise_search' will not link anywhere meaningful for the foreseeable future, so the Enterprise Search root should not go anywhere - Update App Search helper to go to root path, per new React Router setup Test changes: - Mock custom basepath for App Search tests - Swap enterpriseSearchBreadcrumbs and appSearchBreadcrumbs test order (since the latter overrides the default mock) * Add create_first_engine_button telemetry tracking to EmptyState * Switch plugin URLs back to /app/enterprise_search/app_search Now that https://github.com/elastic/kibana/pull/66455 has been merged in :tada: * Add i18n formatted messages / translations (#13) * Add i18n provider and formatted/i18n translated messages * Update tests to account for new I18nProvider context + FormattedMessage components - Add new mountWithContext helper that provides all contexts+providers used in top-level app - Add new shallowWithIntl helper for shallow() components that dive into FormattedMessage * Format i18n dates and numbers + update some mock tests to not throw react-intl invalid date messages * Update EngineOverviewHeader to disable button on prop * Address review feedback (#14) * Fix Prettier linting issues * Escape App Search API endpoint URLs - per PR feedback - querystring should automatically encodeURIComponent / escape query param strings * Update server plugin.ts to use getStartServices() rather than storing local references from start() - Per feedback: https://github.com/elastic/kibana/blob/master/src/core/CONVENTIONS.md#applications - Note: savedObjects.registerType needs to be outside of getStartServices, or an error is thrown - Side update to registerTelemetryUsageCollector to simplify args - Update/fix tests to account for changes * E2E testing (#6) * Wired up basics for E2E testing * Added version with App Search * Updated naming * Switched configuration around * Added concept of 'fixtures' * Figured out how to log in as the enterprise_search user * Refactored to use an App Search service * Added some real tests * Added a README * Cleanup * More cleanup * Error handling + README updatre * Removed unnecessary files * Apply suggestions from code review Co-authored-by: Constance * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx Co-authored-by: Constance * PR feedback - updated README * Additional lint fixes Co-authored-by: Constance * Add README and CODEOWNERS (#15) * Add plugin README and CODEOWNERS * Fix Typescript errors (#16) * Fix public mocks * Fix empty states types * Fix engine table component errors * Fix engine overview component errors * Fix setup guide component errors - SetBreadcrumbs will be fixed in a separate commit * Fix App Search index errors * Fix engine overview header component errors * Fix applications context index errors * Fix kibana breadcrumb helper errors * Fix license helper errors * :exclamation: Refactor React Router EUI link/button helpers - in order to fix typescript errors - this changes the component logic significantly to a react render prop, so that the Link and Button components can have different types - however, end behavior should still remain the same * Fix telemetry helper errors * Minor unused var cleanup in plugin files * Fix telemetry collector/savedobjects errors * Fix MockRouter type errors and add IRouteDependencies export - routes will use IRouteDependencies in the next few commits * Fix engines route errors * Fix telemetry route errors * Remove any type from source code - thanks to Scotty for the inspiration * Add eslint rules for Enterprise Search plugin - Add checks for type any, but only on non-test files - Disable react-hooks/exhaustive-deps, since we're already disabling it in a few files and other plugins also have it turned off * Cover uncovered lines in engines_table and telemetry tests * Fixed TS warnings in E2E tests (#17) * Feedback: Convert static CSS values to EUI variables where possible * Feedback: Flatten nested CSS where possible - Prefer setting CSS class overrides on individual EUI components, not on a top-level page + Change CSS class casing from kebab-case to camelCase to better match EUI/Kibana + Remove unnecessary .euiPageContentHeader margin-bottom override by changing the panelPaddingSize of euiPageContent + Decrease engine overview table padding on mobile * Refactor out components shared with Workplace Search (#18) * Move getUserName helper to shared - in preparation for Workplace Search plugin also using this helper * Move Setup Guide layout to a shared component * Setup Guide: add extra props for standard/native auth links Note: It's possible this commit may be unnecessary if we can publish shared Enterprise Search security mode docs * Update copy per feedback from copy team * Address various telemetry issues - saved objects: removing indexing per #43673 - add schema and generate json per #64942 - move definitions over to collectors since saved objects is mostly empty at this point, and schema throws an error when it imports an obj instead of being defined inline - istanbul ignore saved_objects file since it doesn't have anything meaningful to test but was affecting code coverage * Disable plugin access if a normal user does not have access to App Search (#19) * Set up new server security dependency and configs * Set up access capabilities * Set up checkAccess helper/caller * Remove NoUserState component from the public UI - Since this is now being handled by checkAccess / normal users should never see the plugin at all if they don't have an account/access, the component is no longer needed * Update server routes to account for new changes - Remove login redirect catch from routes, since the access helper should now handle that for most users by disabling the plugin (superusers will see a generic cannot connect/error screen) - Refactor out new config values to a shared mock * Refactor Enterprise Search http call to hit/return new internal API endpoint + pull out the http call to a separate library for upcoming public URL work (so that other files can call it directly as well) * [Discussion] Increase timeout but add another warning timeout for slow servers - per recommendation/convo with Brandon * Register feature control * Remove no_as_account from UI telemetry - since we're no longer tracking that in the UI * Address PR feedback - isSuperUser check * Public URL support for Elastic Cloud (#21) * Add server-side public URL route - Per feedback from Kibana platform team, it's not possible to pass info from server/ to public/ without a HTTP call :[ * Update MockRouter for routes without any payload/params * Add client-side helper for calling the new public URL API + API seems to return a URL a trailing slash, which we need to omit * Update public/plugin.ts to check and set a public URL - relies on this.hasCheckedPublicUrl to only make the call once per page load instead of on every page nav * Fix failing feature control tests - Split up scenario cases as needed - Add plugin as an exception alongside ML & Monitoring * Address PR feedback - version: kibana - copy edits - Sass vars - code cleanup * Casing feedback: change all plugin registration IDs from snake_case to camelCase - note: current remainng snake_case exceptions are telemetry keys - file names and api endpoints are snake_case per conventions * Misc security feedback - remove set - remove unnecessary capabilities registration - telemetry namespace agnostic * Security feedback: add warn logging to telemetry collector see https://github.com/elastic/kibana/pull/66922#discussion_r451215760 - add if statement - pass log dependency around (this is kinda medium, should maybe refactor) - update tests - move test file comment to the right file (was meant for telemetry route file) * Address feedback from Pierre - Remove unnecessary ServerConfigType - Remove unnecessary uiCapabilities - Move registerTelemetryRoute / SavedObjectsServiceStart workaround - Remove unnecessary license optional chaining * PR feedback Address type/typos * Fix telemetry API call returning 415 on Chrome - I can't even?? I swear charset=utf-8 fixed the same error a few weeks ago * Fix failing tests * Update Enterprise Search functional tests (without host) to run on CI - Fix incorrect navigateToApp slug (hadn't realized this was a URL, not an ID) - Update without_host_configured tests to run without API key - Update README * Address PR feedback from Pierre - remove unnecessary authz? - remove unnecessary content-type json headers - add loggingSystemMock.collect(mockLogger).error assertion - reconstrcut new MockRouter on beforeEach for better sandboxing - fix incorrect describe()s -should be it() - pull out reusable mockDependencies helper (renamed/extended from mockConfig) for tests that don't particularly use config/log but still want to pass type definitions - Fix comment copy Co-authored-by: Jason Stoltzfus Co-authored-by: Chris Cressman Co-authored-by: scottybollinger Co-authored-by: Elastic Machine --- .eslintrc.js | 12 + .github/CODEOWNERS | 5 + .../collapsible_nav.test.tsx.snap | 6 +- src/core/public/public.api.md | 6 + src/core/server/server.api.md | 6 + src/core/utils/default_app_categories.ts | 12 +- x-pack/.i18nrc.json | 1 + x-pack/plugins/enterprise_search/README.md | 25 ++ .../enterprise_search/common/constants.ts | 7 + x-pack/plugins/enterprise_search/kibana.json | 10 + .../public/applications/__mocks__/index.ts | 13 + .../__mocks__/kibana_context.mock.ts | 17 ++ .../__mocks__/license_context.mock.ts | 11 + .../__mocks__/mount_with_context.mock.tsx | 49 ++++ .../__mocks__/react_router_history.mock.ts | 25 ++ .../__mocks__/shallow_usecontext.mock.ts | 40 ++++ .../__mocks__/shallow_with_i18n.mock.tsx | 30 +++ .../applications/app_search/assets/engine.svg | 3 + .../app_search/assets/getting_started.png | Bin 0 -> 92044 bytes .../applications/app_search/assets/logo.svg | 4 + .../app_search/assets/meta_engine.svg | 4 + .../components/empty_states/empty_state.tsx | 74 ++++++ .../components/empty_states/empty_states.scss | 19 ++ .../empty_states/empty_states.test.tsx | 53 ++++ .../components/empty_states/error_state.tsx | 95 ++++++++ .../components/empty_states/index.ts | 9 + .../components/empty_states/loading_state.tsx | 30 +++ .../engine_overview/engine_overview.scss | 27 +++ .../engine_overview/engine_overview.test.tsx | 171 +++++++++++++ .../engine_overview/engine_overview.tsx | 155 ++++++++++++ .../engine_overview/engine_table.test.tsx | 80 +++++++ .../engine_overview/engine_table.tsx | 153 ++++++++++++ .../components/engine_overview/index.ts | 7 + .../engine_overview_header.test.tsx | 41 ++++ .../engine_overview_header.tsx | 72 ++++++ .../engine_overview_header/index.ts | 7 + .../components/setup_guide/index.ts | 7 + .../setup_guide/setup_guide.test.tsx | 21 ++ .../components/setup_guide/setup_guide.tsx | 64 +++++ .../applications/app_search/index.test.tsx | 46 ++++ .../public/applications/app_search/index.tsx | 28 +++ .../public/applications/index.test.tsx | 40 ++++ .../public/applications/index.tsx | 56 +++++ .../get_enterprise_search_url.test.ts | 30 +++ .../get_enterprise_search_url.ts | 27 +++ .../shared/enterprise_search_url/index.ts | 7 + .../generate_breadcrumbs.test.ts | 206 ++++++++++++++++ .../generate_breadcrumbs.ts | 54 +++++ .../shared/kibana_breadcrumbs/index.ts | 9 + .../set_breadcrumbs.test.tsx | 63 +++++ .../kibana_breadcrumbs/set_breadcrumbs.tsx | 43 ++++ .../applications/shared/licensing/index.ts | 8 + .../shared/licensing/license_checks.test.ts | 33 +++ .../shared/licensing/license_checks.ts | 11 + .../shared/licensing/license_context.test.tsx | 24 ++ .../shared/licensing/license_context.tsx | 29 +++ .../react_router_helpers/eui_link.test.tsx | 77 ++++++ .../shared/react_router_helpers/eui_link.tsx | 57 +++++ .../shared/react_router_helpers/index.ts | 9 + .../react_router_helpers/link_events.test.ts | 102 ++++++++ .../react_router_helpers/link_events.ts | 31 +++ .../applications/shared/setup_guide/index.ts | 7 + .../shared/setup_guide/setup_guide.scss | 51 ++++ .../shared/setup_guide/setup_guide.test.tsx | 44 ++++ .../shared/setup_guide/setup_guide.tsx | 226 ++++++++++++++++++ .../applications/shared/telemetry/index.ts | 8 + .../shared/telemetry/send_telemetry.test.tsx | 56 +++++ .../shared/telemetry/send_telemetry.tsx | 50 ++++ .../plugins/enterprise_search/public/index.ts | 12 + .../enterprise_search/public/plugin.ts | 88 +++++++ .../collectors/app_search/telemetry.test.ts | 143 +++++++++++ .../server/collectors/app_search/telemetry.ts | 156 ++++++++++++ .../plugins/enterprise_search/server/index.ts | 29 +++ .../server/lib/check_access.test.ts | 128 ++++++++++ .../server/lib/check_access.ts | 76 ++++++ .../lib/enterprise_search_config_api.test.ts | 111 +++++++++ .../lib/enterprise_search_config_api.ts | 78 ++++++ .../enterprise_search/server/plugin.ts | 121 ++++++++++ .../server/routes/__mocks__/index.ts | 8 + .../server/routes/__mocks__/router.mock.ts | 102 ++++++++ .../__mocks__/routerDependencies.mock.ts | 27 +++ .../server/routes/app_search/engines.test.ts | 160 +++++++++++++ .../server/routes/app_search/engines.ts | 59 +++++ .../routes/app_search/telemetry.test.ts | 108 +++++++++ .../server/routes/app_search/telemetry.ts | 50 ++++ .../enterprise_search/public_url.test.ts | 52 ++++ .../routes/enterprise_search/public_url.ts | 26 ++ .../saved_objects/app_search/telemetry.ts | 19 ++ .../privileges/privileges.test.ts | 14 +- .../authorization/privileges/privileges.ts | 1 + .../schema/xpack_plugins.json | 34 +++ x-pack/scripts/functional_tests.js | 1 + .../apis/features/features/features.ts | 1 + .../functional_enterprise_search/README.md | 41 ++++ .../app_search/engines.ts | 75 ++++++ .../with_host_configured/index.ts | 13 + .../app_search/setup_guide.ts | 36 +++ .../without_host_configured/index.ts | 15 ++ .../base_config.ts | 20 ++ .../ftr_provider_context.d.ts | 12 + .../page_objects/app_search.ts | 30 +++ .../page_objects/index.ts | 13 + .../services/app_search_client.ts | 121 ++++++++++ .../services/app_search_service.ts | 77 ++++++ .../services/index.ts | 13 + .../with_host_configured.config.ts | 31 +++ .../without_host_configured.config.ts | 23 ++ .../common/nav_links_builder.ts | 4 + .../security_and_spaces/tests/catalogue.ts | 16 +- .../security_and_spaces/tests/nav_links.ts | 12 +- .../security_only/tests/catalogue.ts | 16 +- .../security_only/tests/nav_links.ts | 10 +- 112 files changed, 4968 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/README.md create mode 100644 x-pack/plugins/enterprise_search/common/constants.ts create mode 100644 x-pack/plugins/enterprise_search/kibana.json create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/getting_started.png create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/index.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/index.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx create mode 100644 x-pack/plugins/enterprise_search/public/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/plugin.ts create mode 100644 x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts create mode 100644 x-pack/plugins/enterprise_search/server/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/check_access.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/check_access.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts create mode 100644 x-pack/plugins/enterprise_search/server/plugin.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts create mode 100644 x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts create mode 100644 x-pack/test/functional_enterprise_search/README.md create mode 100644 x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts create mode 100644 x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/index.ts create mode 100644 x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts create mode 100644 x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts create mode 100644 x-pack/test/functional_enterprise_search/base_config.ts create mode 100644 x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts create mode 100644 x-pack/test/functional_enterprise_search/page_objects/app_search.ts create mode 100644 x-pack/test/functional_enterprise_search/page_objects/index.ts create mode 100644 x-pack/test/functional_enterprise_search/services/app_search_client.ts create mode 100644 x-pack/test/functional_enterprise_search/services/app_search_service.ts create mode 100644 x-pack/test/functional_enterprise_search/services/index.ts create mode 100644 x-pack/test/functional_enterprise_search/with_host_configured.config.ts create mode 100644 x-pack/test/functional_enterprise_search/without_host_configured.config.ts diff --git a/.eslintrc.js b/.eslintrc.js index 8d979dc0f8645e..4425ad3a12659d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -906,6 +906,18 @@ module.exports = { }, }, + /** + * Enterprise Search overrides + */ + { + files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], + excludedFiles: ['x-pack/plugins/enterprise_search/**/*.{test,mock}.{ts,tsx}'], + rules: { + 'react-hooks/exhaustive-deps': 'off', + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + /** * disable jsx-a11y for kbn-ui-framework */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4aab9943022d4c..f053c6da9c29bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -201,6 +201,11 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Design **/*.scss @elastic/kibana-design +# Enterprise Search +/x-pack/plugins/enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend +/x-pack/test/functional_enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design + # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui /src/plugins/console/ @elastic/es-ui diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 9fee7b50f371b2..1cfded4dc7b8f4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -149,7 +149,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "euiIconType": "logoSecurity", "id": "security", "label": "Security", - "order": 3000, + "order": 4000, }, "data-test-subj": "siem", "href": "siem", @@ -164,7 +164,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "euiIconType": "logoObservability", "id": "observability", "label": "Observability", - "order": 2000, + "order": 3000, }, "data-test-subj": "metrics", "href": "metrics", @@ -233,7 +233,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "euiIconType": "logoObservability", "id": "observability", "label": "Observability", - "order": 2000, + "order": 3000, }, "data-test-subj": "logs", "href": "logs", diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 86e281a49b744a..40fc3f977006f4 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -582,6 +582,12 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{ euiIconType: string; order: number; }; + enterpriseSearch: { + id: string; + label: string; + order: number; + euiIconType: string; + }; observability: { id: string; label: string; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index efeafc9e68d359..95912c3af63e51 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -566,6 +566,12 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{ euiIconType: string; order: number; }; + enterpriseSearch: { + id: string; + label: string; + order: number; + euiIconType: string; + }; observability: { id: string; label: string; diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 5708bcfeac31a7..cc9bfb1db04d5e 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -29,20 +29,28 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ euiIconType: 'logoKibana', order: 1000, }, + enterpriseSearch: { + id: 'enterpriseSearch', + label: i18n.translate('core.ui.enterpriseSearchNavList.label', { + defaultMessage: 'Enterprise Search', + }), + order: 2000, + euiIconType: 'logoEnterpriseSearch', + }, observability: { id: 'observability', label: i18n.translate('core.ui.observabilityNavList.label', { defaultMessage: 'Observability', }), euiIconType: 'logoObservability', - order: 2000, + order: 3000, }, security: { id: 'security', label: i18n.translate('core.ui.securityNavList.label', { defaultMessage: 'Security', }), - order: 3000, + order: 4000, euiIconType: 'logoSecurity', }, management: { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 596ba17d343c0c..d0055008eb9bf7 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -16,6 +16,7 @@ "xpack.data": "plugins/data_enhanced", "xpack.embeddableEnhanced": "plugins/embeddable_enhanced", "xpack.endpoint": "plugins/endpoint", + "xpack.enterpriseSearch": "plugins/enterprise_search", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", "xpack.globalSearch": ["plugins/global_search"], diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md new file mode 100644 index 00000000000000..8c316c848184b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/README.md @@ -0,0 +1,25 @@ +# Enterprise Search + +## Overview + +This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In its current MVP state, the plugin provides a basic engines overview from App Search with the goal of gathering user feedback and raising product awareness. + +## Development + +1. When developing locally, Enterprise Search should be running locally alongside Kibana on `localhost:3002`. +2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'` +3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana. + +## Testing + +### Unit tests + +From `kibana-root-folder/x-pack`, run: + +```bash +yarn test:jest plugins/enterprise_search +``` + +### E2E tests + +See [our functional test runner README](../../test/functional_enterprise_search). diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts new file mode 100644 index 00000000000000..c134131caba75c --- /dev/null +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -0,0 +1,7 @@ +/* + * 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 const ENGINES_PAGE_SIZE = 10; diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json new file mode 100644 index 00000000000000..9a2daefcd8c6e6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "enterpriseSearch", + "version": "kibana", + "kibanaVersion": "kibana", + "requiredPlugins": ["home", "features", "licensing"], + "configPath": ["enterpriseSearch"], + "optionalPlugins": ["usageCollection", "security"], + "server": true, + "ui": true +} diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts new file mode 100644 index 00000000000000..14fde357a980a0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { mockHistory } from './react_router_history.mock'; +export { mockKibanaContext } from './kibana_context.mock'; +export { mockLicenseContext } from './license_context.mock'; +export { mountWithContext, mountWithKibanaContext } from './mount_with_context.mock'; +export { shallowWithIntl } from './shallow_with_i18n.mock'; + +// Note: shallow_usecontext must be imported directly as a file diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts new file mode 100644 index 00000000000000..fcfa1b0a21f130 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts @@ -0,0 +1,17 @@ +/* + * 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 { httpServiceMock } from 'src/core/public/mocks'; + +/** + * A set of default Kibana context values to use across component tests. + * @see enterprise_search/public/index.tsx for the KibanaContext definition/import + */ +export const mockKibanaContext = { + http: httpServiceMock.createSetupContract(), + setBreadcrumbs: jest.fn(), + enterpriseSearchUrl: 'http://localhost:3002', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts new file mode 100644 index 00000000000000..7c37ecc7cde1b8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts @@ -0,0 +1,11 @@ +/* + * 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 { licensingMock } from '../../../../licensing/public/mocks'; + +export const mockLicenseContext = { + license: licensingMock.createLicense(), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx new file mode 100644 index 00000000000000..dfcda544459d44 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import { mount } from 'enzyme'; + +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContext } from '../'; +import { mockKibanaContext } from './kibana_context.mock'; +import { LicenseContext } from '../shared/licensing'; +import { mockLicenseContext } from './license_context.mock'; + +/** + * This helper mounts a component with all the contexts/providers used + * by the production app, while allowing custom context to be + * passed in via a second arg + * + * Example usage: + * + * const wrapper = mountWithContext(, { enterpriseSearchUrl: 'someOverride', license: {} }); + */ +export const mountWithContext = (children: React.ReactNode, context?: object) => { + return mount( + + + + {children} + + + + ); +}; + +/** + * This helper mounts a component with just the default KibanaContext - + * useful for isolated / helper components that only need this context + * + * Same usage/override functionality as mountWithContext + */ +export const mountWithKibanaContext = (children: React.ReactNode, context?: object) => { + return mount( + + {children} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts new file mode 100644 index 00000000000000..fd422465d87f1b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * NOTE: This variable name MUST start with 'mock*' in order for + * Jest to accept its use within a jest.mock() + */ +export const mockHistory = { + createHref: jest.fn(({ pathname }) => `/enterprise_search${pathname}`), + push: jest.fn(), + location: { + pathname: '/current-path', + }, +}; + +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn(() => mockHistory), +})); + +/** + * For example usage, @see public/applications/shared/react_router_helpers/eui_link.test.tsx + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts new file mode 100644 index 00000000000000..767a52a75d1fbb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ + +/** + * NOTE: These variable names MUST start with 'mock*' in order for + * Jest to accept its use within a jest.mock() + */ +import { mockKibanaContext } from './kibana_context.mock'; +import { mockLicenseContext } from './license_context.mock'; + +jest.mock('react', () => ({ + ...(jest.requireActual('react') as object), + useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })), +})); + +/** + * Example usage within a component test using shallow(): + * + * import '../../../test_utils/mock_shallow_usecontext'; // Must come before React's import, adjust relative path as needed + * + * import React from 'react'; + * import { shallow } from 'enzyme'; + * + * // ... etc. + */ + +/** + * If you need to override the default mock context values, you can do so via jest.mockImplementation: + * + * import React, { useContext } from 'react'; + * + * // ... etc. + * + * it('some test', () => { + * useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'someOverride' })); + * }); + */ diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx new file mode 100644 index 00000000000000..ae7d0b09f9872e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx @@ -0,0 +1,30 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { IntlProvider } from 'react-intl'; + +const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {}); +const { intl } = intlProvider.getChildContext(); + +/** + * This helper shallow wraps a component with react-intl's which + * fixes "Could not find required `intl` object" console errors when running tests + * + * Example usage (should be the same as shallow()): + * + * const wrapper = shallowWithIntl(); + */ +export const shallowWithIntl = (children: React.ReactNode) => { + const context = { context: { intl } }; + + return shallow({children}, context) + .childAt(0) + .dive(context) + .shallow(); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg new file mode 100644 index 00000000000000..ceab918e92e702 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/getting_started.png new file mode 100644 index 0000000000000000000000000000000000000000..4d988d14f0483c31045783b714eedde9baac92b9 GIT binary patch literal 92044 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 b{wDtaU9#Ss{&OGS00000NkvXXu0mjf=D5g< literal 0 HcmV?d00001 diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg new file mode 100644 index 00000000000000..2284a425b5add2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg new file mode 100644 index 00000000000000..4e01e9a0b34fb9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg @@ -0,0 +1,4 @@ + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx new file mode 100644 index 00000000000000..9bb5cd3bffdf52 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx @@ -0,0 +1,74 @@ +/* + * 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 React, { useContext } from 'react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { sendTelemetry } from '../../../shared/telemetry'; +import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; +import { KibanaContext, IKibanaContext } from '../../../index'; + +import { EngineOverviewHeader } from '../engine_overview_header'; + +import './empty_states.scss'; + +export const EmptyState: React.FC = () => { + const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + + const buttonProps = { + href: `${enterpriseSearchUrl}/as/engines/new`, + target: '_blank', + onClick: () => + sendTelemetry({ + http, + product: 'app_search', + action: 'clicked', + metric: 'create_first_engine_button', + }), + }; + + return ( + + + + + + + + + + } + titleSize="l" + body={ +

+ +

+ } + actions={ + + + + } + /> +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss new file mode 100644 index 00000000000000..01b0903add5598 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/** + * Empty/Error UI states + */ +.emptyState { + min-height: $euiSizeXXL * 11.25; + display: flex; + flex-direction: column; + justify-content: center; + + &__prompt > .euiIcon { + margin-bottom: $euiSizeS; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx new file mode 100644 index 00000000000000..12bf0035641039 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 '../../../__mocks__/shallow_usecontext.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiEmptyPrompt, EuiButton, EuiLoadingContent } from '@elastic/eui'; + +jest.mock('../../../shared/telemetry', () => ({ + sendTelemetry: jest.fn(), + SendAppSearchTelemetry: jest.fn(), +})); +import { sendTelemetry } from '../../../shared/telemetry'; + +import { ErrorState, EmptyState, LoadingState } from './'; + +describe('ErrorState', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); +}); + +describe('EmptyState', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('sends telemetry on create first engine click', () => { + const wrapper = shallow(); + const prompt = wrapper.find(EuiEmptyPrompt).dive(); + const button = prompt.find(EuiButton); + + button.simulate('click'); + expect(sendTelemetry).toHaveBeenCalled(); + (sendTelemetry as jest.Mock).mockClear(); + }); +}); + +describe('LoadingState', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiLoadingContent)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx new file mode 100644 index 00000000000000..d8eeff2aba1c69 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx @@ -0,0 +1,95 @@ +/* + * 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 React, { useContext } from 'react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiButton } from '../../../shared/react_router_helpers'; +import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; +import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { KibanaContext, IKibanaContext } from '../../../index'; +import { EngineOverviewHeader } from '../engine_overview_header'; + +import './empty_states.scss'; + +export const ErrorState: React.FC = () => { + const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + + return ( + + + + + + + + + + + } + titleSize="l" + body={ + <> +

+ {enterpriseSearchUrl}, + }} + /> +

+
    +
  1. + config/kibana.yml, + }} + /> +
  2. +
  3. + +
  4. +
  5. + [enterpriseSearch][plugins], + }} + /> +
  6. +
+ + } + actions={ + + + + } + /> +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts new file mode 100644 index 00000000000000..e92bf214c4cc75 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { LoadingState } from './loading_state'; +export { EmptyState } from './empty_state'; +export { ErrorState } from './error_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx new file mode 100644 index 00000000000000..2be917c8df0965 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx @@ -0,0 +1,30 @@ +/* + * 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 React from 'react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; + +import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; +import { EngineOverviewHeader } from '../engine_overview_header'; + +import './empty_states.scss'; + +export const LoadingState: React.FC = () => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss new file mode 100644 index 00000000000000..2c7f7de6458e2a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Engine Overview + */ +.engineOverview { + width: 100%; + + &__body { + padding: $euiSize; + + @include euiBreakpoint('m', 'l', 'xl') { + padding: $euiSizeXL; + } + } +} + +.engineIcon { + display: inline-block; + width: $euiSize; + height: $euiSize; + margin-right: $euiSizeXS; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx new file mode 100644 index 00000000000000..4d2a2ea1df9aa9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -0,0 +1,171 @@ +/* + * 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 '../../../__mocks__/react_router_history.mock'; + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { render, ReactWrapper } from 'enzyme'; + +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContext } from '../../../'; +import { LicenseContext } from '../../../shared/licensing'; +import { mountWithContext, mockKibanaContext } from '../../../__mocks__'; + +import { EmptyState, ErrorState } from '../empty_states'; +import { EngineTable, IEngineTablePagination } from './engine_table'; + +import { EngineOverview } from './'; + +describe('EngineOverview', () => { + describe('non-happy-path states', () => { + it('isLoading', () => { + // We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect) + // TODO: Consider pulling this out to a renderWithContext mock/helper + const wrapper: Cheerio = render( + + + + + + + + ); + + // render() directly renders HTML which means we have to look for selectors instead of for LoadingState directly + expect(wrapper.find('.euiLoadingContent')).toHaveLength(2); + }); + + it('isEmpty', async () => { + const wrapper = await mountWithApiMock({ + get: () => ({ + results: [], + meta: { page: { total_results: 0 } }, + }), + }); + + expect(wrapper.find(EmptyState)).toHaveLength(1); + }); + + it('hasErrorConnecting', async () => { + const wrapper = await mountWithApiMock({ + get: () => ({ invalidPayload: true }), + }); + expect(wrapper.find(ErrorState)).toHaveLength(1); + }); + }); + + describe('happy-path states', () => { + const mockedApiResponse = { + results: [ + { + name: 'hello-world', + created_at: 'Fri, 1 Jan 1970 12:00:00 +0000', + document_count: 50, + field_count: 10, + }, + ], + meta: { + page: { + current: 1, + total_pages: 10, + total_results: 100, + size: 10, + }, + }, + }; + const mockApi = jest.fn(() => mockedApiResponse); + let wrapper: ReactWrapper; + + beforeAll(async () => { + wrapper = await mountWithApiMock({ get: mockApi }); + }); + + it('renders', () => { + expect(wrapper.find(EngineTable)).toHaveLength(1); + }); + + it('calls the engines API', () => { + expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { + query: { + type: 'indexed', + pageIndex: 1, + }, + }); + }); + + describe('pagination', () => { + const getTablePagination: () => IEngineTablePagination = () => + wrapper.find(EngineTable).first().prop('pagination'); + + it('passes down page data from the API', () => { + const pagination = getTablePagination(); + + expect(pagination.totalEngines).toEqual(100); + expect(pagination.pageIndex).toEqual(0); + }); + + it('re-polls the API on page change', async () => { + await act(async () => getTablePagination().onPaginate(5)); + wrapper.update(); + + expect(mockApi).toHaveBeenLastCalledWith('/api/app_search/engines', { + query: { + type: 'indexed', + pageIndex: 5, + }, + }); + expect(getTablePagination().pageIndex).toEqual(4); + }); + }); + + describe('when on a platinum license', () => { + beforeAll(async () => { + mockApi.mockClear(); + wrapper = await mountWithApiMock({ + license: { type: 'platinum', isActive: true }, + get: mockApi, + }); + }); + + it('renders a 2nd meta engines table', () => { + expect(wrapper.find(EngineTable)).toHaveLength(2); + }); + + it('makes a 2nd call to the engines API with type meta', () => { + expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { + query: { + type: 'meta', + pageIndex: 1, + }, + }); + }); + }); + }); + + /** + * Test helpers + */ + + const mountWithApiMock = async ({ get, license }: { get(): any; license?: object }) => { + let wrapper: ReactWrapper | undefined; + const httpMock = { ...mockKibanaContext.http, get }; + + // We get a lot of act() warning/errors in the terminal without this. + // TBH, I don't fully understand why since Enzyme's mount is supposed to + // have act() baked in - could be because of the wrapping context provider? + await act(async () => { + wrapper = mountWithContext(, { http: httpMock, license }); + }); + if (wrapper) { + wrapper.update(); // This seems to be required for the DOM to actually update + + return wrapper; + } else { + throw new Error('Could not mount wrapper'); + } + }; +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx new file mode 100644 index 00000000000000..13d092a657d11c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -0,0 +1,155 @@ +/* + * 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 React, { useContext, useEffect, useState } from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentBody, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; +import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; +import { KibanaContext, IKibanaContext } from '../../../index'; + +import EnginesIcon from '../../assets/engine.svg'; +import MetaEnginesIcon from '../../assets/meta_engine.svg'; + +import { LoadingState, EmptyState, ErrorState } from '../empty_states'; +import { EngineOverviewHeader } from '../engine_overview_header'; +import { EngineTable } from './engine_table'; + +import './engine_overview.scss'; + +interface IGetEnginesParams { + type: string; + pageIndex: number; +} +interface ISetEnginesCallbacks { + setResults: React.Dispatch>; + setResultsTotal: React.Dispatch>; +} + +export const EngineOverview: React.FC = () => { + const { http } = useContext(KibanaContext) as IKibanaContext; + const { license } = useContext(LicenseContext) as ILicenseContext; + + const [isLoading, setIsLoading] = useState(true); + const [hasErrorConnecting, setHasErrorConnecting] = useState(false); + + const [engines, setEngines] = useState([]); + const [enginesPage, setEnginesPage] = useState(1); + const [enginesTotal, setEnginesTotal] = useState(0); + const [metaEngines, setMetaEngines] = useState([]); + const [metaEnginesPage, setMetaEnginesPage] = useState(1); + const [metaEnginesTotal, setMetaEnginesTotal] = useState(0); + + const getEnginesData = async ({ type, pageIndex }: IGetEnginesParams) => { + return await http.get('/api/app_search/engines', { + query: { type, pageIndex }, + }); + }; + const setEnginesData = async (params: IGetEnginesParams, callbacks: ISetEnginesCallbacks) => { + try { + const response = await getEnginesData(params); + + callbacks.setResults(response.results); + callbacks.setResultsTotal(response.meta.page.total_results); + + setIsLoading(false); + } catch (error) { + setHasErrorConnecting(true); + } + }; + + useEffect(() => { + const params = { type: 'indexed', pageIndex: enginesPage }; + const callbacks = { setResults: setEngines, setResultsTotal: setEnginesTotal }; + + setEnginesData(params, callbacks); + }, [enginesPage]); + + useEffect(() => { + if (hasPlatinumLicense(license)) { + const params = { type: 'meta', pageIndex: metaEnginesPage }; + const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal }; + + setEnginesData(params, callbacks); + } + }, [license, metaEnginesPage]); + + if (hasErrorConnecting) return ; + if (isLoading) return ; + if (!engines.length) return ; + + return ( + + + + + + + + + + +

+ + +

+
+
+ + + + + {metaEngines.length > 0 && ( + <> + + + +

+ + +

+
+
+ + + + + )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx new file mode 100644 index 00000000000000..46b6e61e352de5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -0,0 +1,80 @@ +/* + * 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 React from 'react'; +import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; + +import { mountWithContext } from '../../../__mocks__'; +jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); +import { sendTelemetry } from '../../../shared/telemetry'; + +import { EngineTable } from './engine_table'; + +describe('EngineTable', () => { + const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream + + const wrapper = mountWithContext( + + ); + const table = wrapper.find(EuiBasicTable); + + it('renders', () => { + expect(table).toHaveLength(1); + expect(table.prop('pagination').totalItemCount).toEqual(50); + + const tableContent = table.text(); + expect(tableContent).toContain('test-engine'); + expect(tableContent).toContain('January 1, 1970'); + expect(tableContent).toContain('99,999'); + expect(tableContent).toContain('10'); + + expect(table.find(EuiPagination).find(EuiButtonEmpty)).toHaveLength(5); // Should display 5 pages at 10 engines per page + }); + + it('contains engine links which send telemetry', () => { + const engineLinks = wrapper.find(EuiLink); + + engineLinks.forEach((link) => { + expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine'); + link.simulate('click'); + + expect(sendTelemetry).toHaveBeenCalledWith({ + http: expect.any(Object), + product: 'app_search', + action: 'clicked', + metric: 'engine_table_link', + }); + }); + }); + + it('triggers onPaginate', () => { + table.prop('onChange')({ page: { index: 4 } }); + + expect(onPaginate).toHaveBeenCalledWith(5); + }); + + it('handles empty data', () => { + const emptyWrapper = mountWithContext( + {} }} /> + ); + const emptyTable = emptyWrapper.find(EuiBasicTable); + expect(emptyTable.prop('pagination').pageIndex).toEqual(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx new file mode 100644 index 00000000000000..1e58d820dc83b0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -0,0 +1,153 @@ +/* + * 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 React, { useContext } from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; +import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { sendTelemetry } from '../../../shared/telemetry'; +import { KibanaContext, IKibanaContext } from '../../../index'; + +import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; + +export interface IEngineTableData { + name: string; + created_at: string; + document_count: number; + field_count: number; +} +export interface IEngineTablePagination { + totalEngines: number; + pageIndex: number; + onPaginate(pageIndex: number): void; +} +export interface IEngineTableProps { + data: IEngineTableData[]; + pagination: IEngineTablePagination; +} +export interface IOnChange { + page: { + index: number; + }; +} + +export const EngineTable: React.FC = ({ + data, + pagination: { totalEngines, pageIndex, onPaginate }, +}) => { + const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + const engineLinkProps = (name: string) => ({ + href: `${enterpriseSearchUrl}/as/engines/${name}`, + target: '_blank', + onClick: () => + sendTelemetry({ + http, + product: 'app_search', + action: 'clicked', + metric: 'engine_table_link', + }), + }); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name', { + defaultMessage: 'Name', + }), + render: (name: string) => ( + + {name} + + ), + width: '30%', + truncateText: true, + mobileOptions: { + header: true, + // Note: the below props are valid props per https://elastic.github.io/eui/#/tabular-content/tables (Responsive tables), but EUI's types have a bug reporting it as an error + // @ts-ignore + enlarge: true, + fullWidth: true, + truncateText: false, + }, + }, + { + field: 'created_at', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.createdAt', + { + defaultMessage: 'Created At', + } + ), + dataType: 'string', + render: (dateString: string) => ( + // e.g., January 1, 1970 + + ), + }, + { + field: 'document_count', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.documentCount', + { + defaultMessage: 'Document Count', + } + ), + dataType: 'number', + render: (number: number) => , + truncateText: true, + }, + { + field: 'field_count', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.fieldCount', + { + defaultMessage: 'Field Count', + } + ), + dataType: 'number', + render: (number: number) => , + truncateText: true, + }, + { + field: 'name', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.actions', + { + defaultMessage: 'Actions', + } + ), + dataType: 'string', + render: (name: string) => ( + + + + ), + align: 'right', + width: '100px', + }, + ]; + + return ( + { + const { index } = page; + onPaginate(index + 1); // Note on paging - App Search's API pages start at 1, EuiBasicTables' pages start at 0 + }} + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts new file mode 100644 index 00000000000000..48b7645dc39e8b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { EngineOverview } from './engine_overview'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx new file mode 100644 index 00000000000000..2e49540270ef07 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 '../../../__mocks__/shallow_usecontext.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); +import { sendTelemetry } from '../../../shared/telemetry'; + +import { EngineOverviewHeader } from '../engine_overview_header'; + +describe('EngineOverviewHeader', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find('h1')).toHaveLength(1); + }); + + it('renders a launch app search button that sends telemetry on click', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="launchButton"]'); + + expect(button.prop('href')).toBe('http://localhost:3002/as'); + expect(button.prop('isDisabled')).toBeFalsy(); + + button.simulate('click'); + expect(sendTelemetry).toHaveBeenCalled(); + }); + + it('renders a disabled button when isButtonDisabled is true', () => { + const wrapper = shallow(); + const button = wrapper.find('[data-test-subj="launchButton"]'); + + expect(button.prop('isDisabled')).toBe(true); + expect(button.prop('href')).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx new file mode 100644 index 00000000000000..9aafa8ec0380c7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx @@ -0,0 +1,72 @@ +/* + * 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 React, { useContext } from 'react'; +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiButton, + EuiButtonProps, + EuiLinkProps, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { sendTelemetry } from '../../../shared/telemetry'; +import { KibanaContext, IKibanaContext } from '../../../index'; + +interface IEngineOverviewHeaderProps { + isButtonDisabled?: boolean; +} + +export const EngineOverviewHeader: React.FC = ({ + isButtonDisabled, +}) => { + const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + + const buttonProps = { + fill: true, + iconType: 'popout', + 'data-test-subj': 'launchButton', + } as EuiButtonProps & EuiLinkProps; + + if (isButtonDisabled) { + buttonProps.isDisabled = true; + } else { + buttonProps.href = `${enterpriseSearchUrl}/as`; + buttonProps.target = '_blank'; + buttonProps.onClick = () => + sendTelemetry({ + http, + product: 'app_search', + action: 'clicked', + metric: 'header_launch_button', + }); + } + + return ( + + + +

+ +

+
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts new file mode 100644 index 00000000000000..2d37f037e21e5c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { EngineOverviewHeader } from './engine_overview_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts new file mode 100644 index 00000000000000..c367424d375f9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { SetupGuide } from './setup_guide'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx new file mode 100644 index 00000000000000..82cc344d496322 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx @@ -0,0 +1,21 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; +import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; +import { SetupGuide } from './'; + +describe('SetupGuide', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SetupGuideLayout)).toHaveLength(1); + expect(wrapper.find(SetBreadcrumbs)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx new file mode 100644 index 00000000000000..df278bf938a690 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -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 React from 'react'; +import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; +import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs'; +import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import GettingStarted from '../../assets/getting_started.png'; + +export const SetupGuide: React.FC = () => ( + + + + +
+ {i18n.translate('xpack.enterpriseSearch.appSearch.setupGuide.videoAlt', + + + +

+ +

+
+ + +

+ +

+
+ +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx new file mode 100644 index 00000000000000..45e318ca0f9d95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 '../__mocks__/shallow_usecontext.mock'; + +import React, { useContext } from 'react'; +import { Redirect } from 'react-router-dom'; +import { shallow } from 'enzyme'; + +import { SetupGuide } from './components/setup_guide'; +import { EngineOverview } from './components/engine_overview'; + +import { AppSearch } from './'; + +describe('App Search Routes', () => { + describe('/', () => { + it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); + const wrapper = shallow(); + + expect(wrapper.find(Redirect)).toHaveLength(1); + expect(wrapper.find(EngineOverview)).toHaveLength(0); + }); + + it('renders Engine Overview when enterpriseSearchUrl is set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ + enterpriseSearchUrl: 'https://foo.bar', + })); + const wrapper = shallow(); + + expect(wrapper.find(EngineOverview)).toHaveLength(1); + expect(wrapper.find(Redirect)).toHaveLength(0); + }); + }); + + describe('/setup_guide', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SetupGuide)).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx new file mode 100644 index 00000000000000..8f7142f1631a95 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -0,0 +1,28 @@ +/* + * 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 React, { useContext } from 'react'; +import { Route, Redirect } from 'react-router-dom'; + +import { KibanaContext, IKibanaContext } from '../index'; + +import { SetupGuide } from './components/setup_guide'; +import { EngineOverview } from './components/engine_overview'; + +export const AppSearch: React.FC = () => { + const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + + return ( + <> + + {!enterpriseSearchUrl ? : } + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx new file mode 100644 index 00000000000000..1aead8468ca3b0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 React from 'react'; + +import { coreMock } from 'src/core/public/mocks'; +import { licensingMock } from '../../../licensing/public/mocks'; + +import { renderApp } from './'; +import { AppSearch } from './app_search'; + +describe('renderApp', () => { + const params = coreMock.createAppMountParamters(); + const core = coreMock.createStart(); + const config = {}; + const plugins = { + licensing: licensingMock.createSetup(), + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('mounts and unmounts UI', () => { + const MockApp = () =>
Hello world!
; + + const unmount = renderApp(MockApp, core, params, config, plugins); + expect(params.element.querySelector('.hello-world')).not.toBeNull(); + unmount(); + expect(params.element.innerHTML).toEqual(''); + }); + + it('renders AppSearch', () => { + renderApp(AppSearch, core, params, config, plugins); + expect(params.element.querySelector('.setupGuide')).not.toBeNull(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx new file mode 100644 index 00000000000000..4ef7aca8260a20 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -0,0 +1,56 @@ +/* + * 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 React from 'react'; +import ReactDOM from 'react-dom'; +import { Router } from 'react-router-dom'; + +import { I18nProvider } from '@kbn/i18n/react'; +import { CoreStart, AppMountParameters, HttpSetup, ChromeBreadcrumb } from 'src/core/public'; +import { ClientConfigType, PluginsSetup } from '../plugin'; +import { LicenseProvider } from './shared/licensing'; + +export interface IKibanaContext { + enterpriseSearchUrl?: string; + http: HttpSetup; + setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; +} + +export const KibanaContext = React.createContext({}); + +/** + * This file serves as a reusable wrapper to share Kibana-level context and other helpers + * between various Enterprise Search plugins (e.g. AppSearch, WorkplaceSearch, ES landing page) + * which should be imported and passed in as the first param in plugin.ts. + */ + +export const renderApp = ( + App: React.FC, + core: CoreStart, + params: AppMountParameters, + config: ClientConfigType, + plugins: PluginsSetup +) => { + ReactDOM.render( + + + + + + + + + , + params.element + ); + return () => ReactDOM.unmountComponentAtNode(params.element); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts new file mode 100644 index 00000000000000..42f308c5542688 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts @@ -0,0 +1,30 @@ +/* + * 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 { getPublicUrl } from './'; + +describe('Enterprise Search URL helper', () => { + const httpMock = { get: jest.fn() } as any; + + it('calls and returns the public URL API endpoint', async () => { + httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://some.vanity.url' })); + + expect(await getPublicUrl(httpMock)).toEqual('http://some.vanity.url'); + }); + + it('strips trailing slashes', async () => { + httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://trailing.slash/' })); + + expect(await getPublicUrl(httpMock)).toEqual('http://trailing.slash'); + }); + + // For the most part, error logging/handling is done on the server side. + // On the front-end, we should simply gracefully fall back to config.host + // if we can't fetch a public URL + it('falls back to an empty string', async () => { + expect(await getPublicUrl(httpMock)).toEqual(''); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts new file mode 100644 index 00000000000000..419c187a0048a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts @@ -0,0 +1,27 @@ +/* + * 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 { HttpSetup } from 'src/core/public'; + +/** + * On Elastic Cloud, the host URL set in kibana.yml is not necessarily the same + * URL we want to send users to in the front-end (e.g. if a vanity URL is set). + * + * This helper checks a Kibana API endpoint (which has checks an Enterprise + * Search internal API endpoint) for the correct public-facing URL to use. + */ +export const getPublicUrl = async (http: HttpSetup): Promise => { + try { + const { publicUrl } = await http.get('/api/enterprise_search/public_url'); + return stripTrailingSlash(publicUrl); + } catch { + return ''; + } +}; + +const stripTrailingSlash = (url: string): string => { + return url.endsWith('/') ? url.slice(0, -1) : url; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts new file mode 100644 index 00000000000000..bbbb688b8ea7b4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { getPublicUrl } from './get_enterprise_search_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts new file mode 100644 index 00000000000000..7ea73577c4de6f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts @@ -0,0 +1,206 @@ +/* + * 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 { generateBreadcrumb } from './generate_breadcrumbs'; +import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from './'; + +import { mockHistory as mockHistoryUntyped } from '../../__mocks__'; +const mockHistory = mockHistoryUntyped as any; + +jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) })); +import { letBrowserHandleEvent } from '../react_router_helpers'; + +describe('generateBreadcrumb', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("creates a breadcrumb object matching EUI's breadcrumb type", () => { + const breadcrumb = generateBreadcrumb({ + text: 'Hello World', + path: '/hello_world', + history: mockHistory, + }); + expect(breadcrumb).toEqual({ + text: 'Hello World', + href: '/enterprise_search/hello_world', + onClick: expect.any(Function), + }); + }); + + it('prevents default navigation and uses React Router history on click', () => { + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }) as any; + const event = { preventDefault: jest.fn() }; + breadcrumb.onClick(event); + + expect(mockHistory.push).toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('does not prevent default browser behavior on new tab/window clicks', () => { + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }) as any; + + (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true); + breadcrumb.onClick(); + + expect(mockHistory.push).not.toHaveBeenCalled(); + }); + + it('does not generate link behavior if path is excluded', () => { + const breadcrumb = generateBreadcrumb({ text: 'Unclickable breadcrumb' }); + + expect(breadcrumb.href).toBeUndefined(); + expect(breadcrumb.onClick).toBeUndefined(); + }); +}); + +describe('enterpriseSearchBreadcrumbs', () => { + const breadCrumbs = [ + { + text: 'Page 1', + path: '/page1', + }, + { + text: 'Page 2', + path: '/page2', + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const subject = () => enterpriseSearchBreadcrumbs(mockHistory)(breadCrumbs); + + it('Builds a chain of breadcrumbs with Enterprise Search at the root', () => { + expect(subject()).toEqual([ + { + text: 'Enterprise Search', + }, + { + href: '/enterprise_search/page1', + onClick: expect.any(Function), + text: 'Page 1', + }, + { + href: '/enterprise_search/page2', + onClick: expect.any(Function), + text: 'Page 2', + }, + ]); + }); + + it('shows just the root if breadcrumbs is empty', () => { + expect(enterpriseSearchBreadcrumbs(mockHistory)()).toEqual([ + { + text: 'Enterprise Search', + }, + ]); + }); + + describe('links', () => { + const eventMock = { + preventDefault: jest.fn(), + } as any; + + it('has Enterprise Search text first', () => { + expect(subject()[0].onClick).toBeUndefined(); + }); + + it('has a link to page 1 second', () => { + (subject()[1] as any).onClick(eventMock); + expect(mockHistory.push).toHaveBeenCalledWith('/page1'); + }); + + it('has a link to page 2 last', () => { + (subject()[2] as any).onClick(eventMock); + expect(mockHistory.push).toHaveBeenCalledWith('/page2'); + }); + }); +}); + +describe('appSearchBreadcrumbs', () => { + const breadCrumbs = [ + { + text: 'Page 1', + path: '/page1', + }, + { + text: 'Page 2', + path: '/page2', + }, + ]; + + beforeEach(() => { + jest.clearAllMocks(); + mockHistory.createHref.mockImplementation( + ({ pathname }: any) => `/enterprise_search/app_search${pathname}` + ); + }); + + const subject = () => appSearchBreadcrumbs(mockHistory)(breadCrumbs); + + it('Builds a chain of breadcrumbs with Enterprise Search and App Search at the root', () => { + expect(subject()).toEqual([ + { + text: 'Enterprise Search', + }, + { + href: '/enterprise_search/app_search/', + onClick: expect.any(Function), + text: 'App Search', + }, + { + href: '/enterprise_search/app_search/page1', + onClick: expect.any(Function), + text: 'Page 1', + }, + { + href: '/enterprise_search/app_search/page2', + onClick: expect.any(Function), + text: 'Page 2', + }, + ]); + }); + + it('shows just the root if breadcrumbs is empty', () => { + expect(appSearchBreadcrumbs(mockHistory)()).toEqual([ + { + text: 'Enterprise Search', + }, + { + href: '/enterprise_search/app_search/', + onClick: expect.any(Function), + text: 'App Search', + }, + ]); + }); + + describe('links', () => { + const eventMock = { + preventDefault: jest.fn(), + } as any; + + it('has Enterprise Search text first', () => { + expect(subject()[0].onClick).toBeUndefined(); + }); + + it('has a link to App Search second', () => { + (subject()[1] as any).onClick(eventMock); + expect(mockHistory.push).toHaveBeenCalledWith('/'); + }); + + it('has a link to page 1 third', () => { + (subject()[2] as any).onClick(eventMock); + expect(mockHistory.push).toHaveBeenCalledWith('/page1'); + }); + + it('has a link to page 2 last', () => { + (subject()[3] as any).onClick(eventMock); + expect(mockHistory.push).toHaveBeenCalledWith('/page2'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts new file mode 100644 index 00000000000000..0e1bb796cbf2ec --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts @@ -0,0 +1,54 @@ +/* + * 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 { Breadcrumb as EuiBreadcrumb } from '@elastic/eui'; +import { History } from 'history'; + +import { letBrowserHandleEvent } from '../react_router_helpers'; + +/** + * Generate React-Router-friendly EUI breadcrumb objects + * https://elastic.github.io/eui/#/navigation/breadcrumbs + */ + +interface IGenerateBreadcrumbProps { + text: string; + path?: string; + history?: History; +} + +export const generateBreadcrumb = ({ text, path, history }: IGenerateBreadcrumbProps) => { + const breadcrumb = { text } as EuiBreadcrumb; + + if (path && history) { + breadcrumb.href = history.createHref({ pathname: path }); + breadcrumb.onClick = (event) => { + if (letBrowserHandleEvent(event)) return; + event.preventDefault(); + history.push(path); + }; + } + + return breadcrumb; +}; + +/** + * Product-specific breadcrumb helpers + */ + +export type TBreadcrumbs = IGenerateBreadcrumbProps[]; + +export const enterpriseSearchBreadcrumbs = (history: History) => ( + breadcrumbs: TBreadcrumbs = [] +) => [ + generateBreadcrumb({ text: 'Enterprise Search' }), + ...breadcrumbs.map(({ text, path }: IGenerateBreadcrumbProps) => + generateBreadcrumb({ text, path, history }) + ), +]; + +export const appSearchBreadcrumbs = (history: History) => (breadcrumbs: TBreadcrumbs = []) => + enterpriseSearchBreadcrumbs(history)([{ text: 'App Search', path: '/' }, ...breadcrumbs]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts new file mode 100644 index 00000000000000..cf8bbbc593f2f7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { enterpriseSearchBreadcrumbs } from './generate_breadcrumbs'; +export { appSearchBreadcrumbs } from './generate_breadcrumbs'; +export { SetAppSearchBreadcrumbs } from './set_breadcrumbs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx new file mode 100644 index 00000000000000..974ca54277c51c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import '../../__mocks__/react_router_history.mock'; +import { mountWithKibanaContext } from '../../__mocks__'; + +jest.mock('./generate_breadcrumbs', () => ({ appSearchBreadcrumbs: jest.fn() })); +import { appSearchBreadcrumbs, SetAppSearchBreadcrumbs } from './'; + +describe('SetAppSearchBreadcrumbs', () => { + const setBreadcrumbs = jest.fn(); + const builtBreadcrumbs = [] as any; + const appSearchBreadCrumbsInnerCall = jest.fn().mockReturnValue(builtBreadcrumbs); + const appSearchBreadCrumbsOuterCall = jest.fn().mockReturnValue(appSearchBreadCrumbsInnerCall); + (appSearchBreadcrumbs as jest.Mock).mockImplementation(appSearchBreadCrumbsOuterCall); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const mountSetAppSearchBreadcrumbs = (props: any) => { + return mountWithKibanaContext(, { + http: {}, + enterpriseSearchUrl: 'http://localhost:3002', + setBreadcrumbs, + }); + }; + + describe('when isRoot is false', () => { + const subject = () => mountSetAppSearchBreadcrumbs({ text: 'Page 1', isRoot: false }); + + it('calls appSearchBreadcrumbs to build breadcrumbs, then registers them with Kibana', () => { + subject(); + + // calls appSearchBreadcrumbs to build breadcrumbs with the target page and current location + expect(appSearchBreadCrumbsInnerCall).toHaveBeenCalledWith([ + { text: 'Page 1', path: '/current-path' }, + ]); + + // then registers them with Kibana + expect(setBreadcrumbs).toHaveBeenCalledWith(builtBreadcrumbs); + }); + }); + + describe('when isRoot is true', () => { + const subject = () => mountSetAppSearchBreadcrumbs({ text: 'Page 1', isRoot: true }); + + it('calls appSearchBreadcrumbs to build breadcrumbs with an empty breadcrumb, then registers them with Kibana', () => { + subject(); + + // uses an empty bredcrumb + expect(appSearchBreadCrumbsInnerCall).toHaveBeenCalledWith([]); + + // then registers them with Kibana + expect(setBreadcrumbs).toHaveBeenCalledWith(builtBreadcrumbs); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx new file mode 100644 index 00000000000000..ad3cd65c09516b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx @@ -0,0 +1,43 @@ +/* + * 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 React, { useContext, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Breadcrumb as EuiBreadcrumb } from '@elastic/eui'; +import { KibanaContext, IKibanaContext } from '../../index'; +import { appSearchBreadcrumbs, TBreadcrumbs } from './generate_breadcrumbs'; + +/** + * Small on-mount helper for setting Kibana's chrome breadcrumbs on any App Search view + * @see https://github.com/elastic/kibana/blob/master/src/core/public/chrome/chrome_service.tsx + */ + +export type TSetBreadcrumbs = (breadcrumbs: EuiBreadcrumb[]) => void; + +interface IBreadcrumbProps { + text: string; + isRoot?: never; +} +interface IRootBreadcrumbProps { + isRoot: true; + text?: never; +} + +export const SetAppSearchBreadcrumbs: React.FC = ({ + text, + isRoot, +}) => { + const history = useHistory(); + const { setBreadcrumbs } = useContext(KibanaContext) as IKibanaContext; + + const crumb = isRoot ? [] : [{ text, path: history.location.pathname }]; + + useEffect(() => { + setBreadcrumbs(appSearchBreadcrumbs(history)(crumb as TBreadcrumbs | [])); + }, []); + + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts new file mode 100644 index 00000000000000..9c8c1417d48db2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { LicenseContext, LicenseProvider, ILicenseContext } from './license_context'; +export { hasPlatinumLicense } from './license_checks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts new file mode 100644 index 00000000000000..ad134e7d36b10c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { hasPlatinumLicense } from './license_checks'; + +describe('hasPlatinumLicense', () => { + it('is true for platinum licenses', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); + }); + + it('is true for enterprise licenses', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true); + }); + + it('is true for trial licenses', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); + }); + + it('is false if the current license is expired', () => { + expect(hasPlatinumLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'trial' } as any)).toEqual(false); + }); + + it('is false for licenses below platinum', () => { + expect(hasPlatinumLicense({ isActive: true, type: 'basic' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: false, type: 'standard' } as any)).toEqual(false); + expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts new file mode 100644 index 00000000000000..de4a17ce2bd3c1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts @@ -0,0 +1,11 @@ +/* + * 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 { ILicense } from '../../../../../licensing/public'; + +export const hasPlatinumLicense = (license?: ILicense) => { + return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type as string); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx new file mode 100644 index 00000000000000..c65474ec1f5900 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx @@ -0,0 +1,24 @@ +/* + * 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 React, { useContext } from 'react'; + +import { mountWithContext } from '../../__mocks__'; +import { LicenseContext, ILicenseContext } from './'; + +describe('LicenseProvider', () => { + const MockComponent: React.FC = () => { + const { license } = useContext(LicenseContext) as ILicenseContext; + return
{license?.type}
; + }; + + it('renders children', () => { + const wrapper = mountWithContext(, { license: { type: 'basic' } }); + + expect(wrapper.find('.license-test')).toHaveLength(1); + expect(wrapper.text()).toEqual('basic'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx new file mode 100644 index 00000000000000..9b47959ff75447 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { Observable } from 'rxjs'; + +import { ILicense } from '../../../../../licensing/public'; + +export interface ILicenseContext { + license: ILicense; +} +interface ILicenseContextProps { + license$: Observable; + children: React.ReactNode; +} + +export const LicenseContext = React.createContext({}); + +export const LicenseProvider: React.FC = ({ license$, children }) => { + // Listen for changes to license subscription + const license = useObservable(license$); + + // Render rest of application and pass down license via context + return ; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx new file mode 100644 index 00000000000000..7d4c068b211555 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 React from 'react'; +import { shallow, mount } from 'enzyme'; +import { EuiLink, EuiButton } from '@elastic/eui'; + +import '../../__mocks__/react_router_history.mock'; +import { mockHistory } from '../../__mocks__'; + +import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; + +describe('EUI & React Router Component Helpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiLink)).toHaveLength(1); + }); + + it('renders an EuiButton', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButton)).toHaveLength(1); + }); + + it('passes down all ...rest props', () => { + const wrapper = shallow(); + const link = wrapper.find(EuiLink); + + expect(link.prop('external')).toEqual(true); + expect(link.prop('data-test-subj')).toEqual('foo'); + }); + + it('renders with the correct href and onClick props', () => { + const wrapper = mount(); + const link = wrapper.find(EuiLink); + + expect(link.prop('onClick')).toBeInstanceOf(Function); + expect(link.prop('href')).toEqual('/enterprise_search/foo/bar'); + expect(mockHistory.createHref).toHaveBeenCalled(); + }); + + describe('onClick', () => { + it('prevents default navigation and uses React Router history', () => { + const wrapper = mount(); + + const simulatedEvent = { + button: 0, + target: { getAttribute: () => '_self' }, + preventDefault: jest.fn(), + }; + wrapper.find(EuiLink).simulate('click', simulatedEvent); + + expect(simulatedEvent.preventDefault).toHaveBeenCalled(); + expect(mockHistory.push).toHaveBeenCalled(); + }); + + it('does not prevent default browser behavior on new tab/window clicks', () => { + const wrapper = mount(); + + const simulatedEvent = { + shiftKey: true, + target: { getAttribute: () => '_blank' }, + }; + wrapper.find(EuiLink).simulate('click', simulatedEvent); + + expect(mockHistory.push).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx new file mode 100644 index 00000000000000..f486e432bae76a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui'; + +import { letBrowserHandleEvent } from './link_events'; + +/** + * Generates either an EuiLink or EuiButton with a React-Router-ified link + * + * Based off of EUI's recommendations for handling React Router: + * https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51 + */ + +interface IEuiReactRouterProps { + to: string; +} + +export const EuiReactRouterHelper: React.FC = ({ to, children }) => { + const history = useHistory(); + + const onClick = (event: React.MouseEvent) => { + if (letBrowserHandleEvent(event)) return; + + // Prevent regular link behavior, which causes a browser refresh. + event.preventDefault(); + + // Push the route to the history. + history.push(to); + }; + + // Generate the correct link href (with basename etc. accounted for) + const href = history.createHref({ pathname: to }); + + const reactRouterProps = { href, onClick }; + return React.cloneElement(children as React.ReactElement, reactRouterProps); +}; + +type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; +type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; + +export const EuiReactRouterLink: React.FC = ({ to, ...rest }) => ( + + + +); + +export const EuiReactRouterButton: React.FC = ({ to, ...rest }) => ( + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts new file mode 100644 index 00000000000000..46dc3286331533 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { letBrowserHandleEvent } from './link_events'; +export { EuiReactRouterLink as EuiLink } from './eui_link'; +export { EuiReactRouterButton as EuiButton } from './eui_link'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts new file mode 100644 index 00000000000000..3682946b63a136 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { letBrowserHandleEvent } from '../react_router_helpers'; + +describe('letBrowserHandleEvent', () => { + const event = { + defaultPrevented: false, + metaKey: false, + altKey: false, + ctrlKey: false, + shiftKey: false, + button: 0, + target: { + getAttribute: () => '_self', + }, + } as any; + + describe('the browser should handle the link when', () => { + it('default is prevented', () => { + expect(letBrowserHandleEvent({ ...event, defaultPrevented: true })).toBe(true); + }); + + it('is modified with metaKey', () => { + expect(letBrowserHandleEvent({ ...event, metaKey: true })).toBe(true); + }); + + it('is modified with altKey', () => { + expect(letBrowserHandleEvent({ ...event, altKey: true })).toBe(true); + }); + + it('is modified with ctrlKey', () => { + expect(letBrowserHandleEvent({ ...event, ctrlKey: true })).toBe(true); + }); + + it('is modified with shiftKey', () => { + expect(letBrowserHandleEvent({ ...event, shiftKey: true })).toBe(true); + }); + + it('it is not a left click event', () => { + expect(letBrowserHandleEvent({ ...event, button: 2 })).toBe(true); + }); + + it('the target is anything value other than _self', () => { + expect( + letBrowserHandleEvent({ + ...event, + target: targetValue('_blank'), + }) + ).toBe(true); + }); + }); + + describe('the browser should NOT handle the link when', () => { + it('default is not prevented', () => { + expect(letBrowserHandleEvent({ ...event, defaultPrevented: false })).toBe(false); + }); + + it('is not modified', () => { + expect( + letBrowserHandleEvent({ + ...event, + metaKey: false, + altKey: false, + ctrlKey: false, + shiftKey: false, + }) + ).toBe(false); + }); + + it('it is a left click event', () => { + expect(letBrowserHandleEvent({ ...event, button: 0 })).toBe(false); + }); + + it('the target is a value of _self', () => { + expect( + letBrowserHandleEvent({ + ...event, + target: targetValue('_self'), + }) + ).toBe(false); + }); + + it('the target has no value', () => { + expect( + letBrowserHandleEvent({ + ...event, + target: targetValue(null), + }) + ).toBe(false); + }); + }); +}); + +const targetValue = (value: string | null) => { + return { + getAttribute: () => value, + }; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts new file mode 100644 index 00000000000000..93da2ab71d9527 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts @@ -0,0 +1,31 @@ +/* + * 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 { MouseEvent } from 'react'; + +/** + * Helper functions for determining which events we should + * let browsers handle natively, e.g. new tabs/windows + */ + +type THandleEvent = (event: MouseEvent) => boolean; + +export const letBrowserHandleEvent: THandleEvent = (event) => + event.defaultPrevented || + isModifiedEvent(event) || + !isLeftClickEvent(event) || + isTargetBlank(event); + +const isModifiedEvent: THandleEvent = (event) => + !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + +const isLeftClickEvent: THandleEvent = (event) => event.button === 0; + +const isTargetBlank: THandleEvent = (event) => { + const element = event.target as HTMLElement; + const target = element.getAttribute('target'); + return !!target && target !== '_self'; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts new file mode 100644 index 00000000000000..c367424d375f9d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { SetupGuide } from './setup_guide'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss new file mode 100644 index 00000000000000..ecfa13cc828f0a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss @@ -0,0 +1,51 @@ +/* + * 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. + */ + +/** + * Setup Guide + */ +.setupGuide { + padding: 0; + min-height: 100vh; + + &__sidebar { + flex-basis: $euiSizeXXL * 7.5; + flex-shrink: 0; + padding: $euiSizeL; + margin-right: 0; + + background-color: $euiColorLightestShade; + border-color: $euiBorderColor; + border-style: solid; + border-width: 0 0 $euiBorderWidthThin 0; // bottom - mobile view + + @include euiBreakpoint('m', 'l', 'xl') { + border-width: 0 $euiBorderWidthThin 0 0; // right - desktop view + } + @include euiBreakpoint('m', 'l') { + flex-basis: $euiSizeXXL * 10; + } + @include euiBreakpoint('xl') { + flex-basis: $euiSizeXXL * 12.5; + } + } + + &__body { + align-self: start; + padding: $euiSizeL; + + @include euiBreakpoint('l') { + padding: $euiSizeXXL ($euiSizeXXL * 1.25); + } + } + + &__thumbnail { + display: block; + max-width: 100%; + height: auto; + margin: $euiSizeL auto; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx new file mode 100644 index 00000000000000..0423ae61779af1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { EuiSteps, EuiIcon, EuiLink } from '@elastic/eui'; + +import { mountWithContext } from '../../__mocks__'; + +import { SetupGuide } from './'; + +describe('SetupGuide', () => { + it('renders', () => { + const wrapper = shallow( + +

Wow!

+
+ ); + + expect(wrapper.find('h1').text()).toEqual('Enterprise Search'); + expect(wrapper.find(EuiIcon).prop('type')).toEqual('logoEnterpriseSearch'); + expect(wrapper.find('[data-test-subj="test"]').text()).toEqual('Wow!'); + expect(wrapper.find(EuiSteps)).toHaveLength(1); + }); + + it('renders with optional auth links', () => { + const wrapper = mountWithContext( + + Baz + + ); + + expect(wrapper.find(EuiLink).first().prop('href')).toEqual('http://bar.com'); + expect(wrapper.find(EuiLink).last().prop('href')).toEqual('http://foo.com'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx new file mode 100644 index 00000000000000..31ff0089dbd7c9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx @@ -0,0 +1,226 @@ +/* + * 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 React from 'react'; +import { + EuiPage, + EuiPageSideBar, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiIcon, + EuiSteps, + EuiCode, + EuiCodeBlock, + EuiAccordion, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import './setup_guide.scss'; + +/** + * Shared Setup Guide component. Sidebar content and product name/links are + * customizable, but the basic layout and instruction steps are DRYed out + */ + +interface ISetupGuideProps { + children: React.ReactNode; + productName: string; + productEuiIcon: 'logoAppSearch' | 'logoWorkplaceSearch' | 'logoEnterpriseSearch'; + standardAuthLink?: string; + elasticsearchNativeAuthLink?: string; +} + +export const SetupGuide: React.FC = ({ + children, + productName, + productEuiIcon, + standardAuthLink, + elasticsearchNativeAuthLink, +}) => ( + + + + + + + + + + + + + + + +

{productName}

+
+
+
+ + {children} +
+ + + + +

+ config/kibana.yml, + configSetting: enterpriseSearch.host, + }} + /> +

+ + enterpriseSearch.host: 'http://localhost:3002' + + + ), + }, + { + title: i18n.translate('xpack.enterpriseSearch.setupGuide.step2.title', { + defaultMessage: 'Reload your Kibana instance', + }), + children: ( + +

+ +

+

+ + Elasticsearch Native Auth + + ) : ( + 'Elasticsearch Native Auth' + ), + }} + /> +

+
+ ), + }, + { + title: i18n.translate('xpack.enterpriseSearch.setupGuide.step3.title', { + defaultMessage: 'Troubleshooting issues', + }), + children: ( + <> + + +

+ +

+
+
+ + + +

+ +

+
+
+ + + +

+ + Standard Auth + + ) : ( + 'Standard Auth' + ), + }} + /> +

+
+
+ + ), + }, + ]} + /> +
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts new file mode 100644 index 00000000000000..f871f48b171548 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { sendTelemetry } from './send_telemetry'; +export { SendAppSearchTelemetry } from './send_telemetry'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx new file mode 100644 index 00000000000000..9825c0d8ab889d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 React from 'react'; + +import { httpServiceMock } from 'src/core/public/mocks'; +import { mountWithKibanaContext } from '../../__mocks__'; +import { sendTelemetry, SendAppSearchTelemetry } from './'; + +describe('Shared Telemetry Helpers', () => { + const httpMock = httpServiceMock.createSetupContract(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('sendTelemetry', () => { + it('successfully calls the server-side telemetry endpoint', () => { + sendTelemetry({ + http: httpMock, + product: 'enterprise_search', + action: 'viewed', + metric: 'setup_guide', + }); + + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + headers: { 'Content-Type': 'application/json' }, + body: '{"action":"viewed","metric":"setup_guide"}', + }); + }); + + it('throws an error if the telemetry endpoint fails', () => { + const httpRejectMock = sendTelemetry({ + http: { put: () => Promise.reject() }, + } as any); + + expect(httpRejectMock).rejects.toThrow('Unable to send telemetry'); + }); + }); + + describe('React component helpers', () => { + it('SendAppSearchTelemetry component', () => { + mountWithKibanaContext(, { + http: httpMock, + }); + + expect(httpMock.put).toHaveBeenCalledWith('/api/app_search/telemetry', { + headers: { 'Content-Type': 'application/json' }, + body: '{"action":"clicked","metric":"button"}', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx new file mode 100644 index 00000000000000..300cb182727174 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -0,0 +1,50 @@ +/* + * 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 React, { useContext, useEffect } from 'react'; + +import { HttpSetup } from 'src/core/public'; +import { KibanaContext, IKibanaContext } from '../../index'; + +interface ISendTelemetryProps { + action: 'viewed' | 'error' | 'clicked'; + metric: string; // e.g., 'setup_guide' +} + +interface ISendTelemetry extends ISendTelemetryProps { + http: HttpSetup; + product: 'app_search' | 'workplace_search' | 'enterprise_search'; +} + +/** + * Base function - useful for non-component actions, e.g. clicks + */ + +export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => { + try { + await http.put(`/api/${product}/telemetry`, { + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action, metric }), + }); + } catch (error) { + throw new Error('Unable to send telemetry'); + } +}; + +/** + * React component helpers - useful for on-page-load/views + * TODO: SendWorkplaceSearchTelemetry and SendEnterpriseSearchTelemetry + */ + +export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => { + const { http } = useContext(KibanaContext) as IKibanaContext; + + useEffect(() => { + sendTelemetry({ http, action, metric, product: 'app_search' }); + }, [action, metric, http]); + + return null; +}; diff --git a/x-pack/plugins/enterprise_search/public/index.ts b/x-pack/plugins/enterprise_search/public/index.ts new file mode 100644 index 00000000000000..06272641b19294 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/public'; +import { EnterpriseSearchPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new EnterpriseSearchPlugin(initializerContext); +}; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts new file mode 100644 index 00000000000000..fbfcc303de47a2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -0,0 +1,88 @@ +/* + * 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 { + Plugin, + PluginInitializerContext, + CoreSetup, + CoreStart, + AppMountParameters, + HttpSetup, +} from 'src/core/public'; + +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { LicensingPluginSetup } from '../../licensing/public'; + +import { getPublicUrl } from './applications/shared/enterprise_search_url'; +import AppSearchLogo from './applications/app_search/assets/logo.svg'; + +export interface ClientConfigType { + host?: string; +} +export interface PluginsSetup { + home: HomePublicPluginSetup; + licensing: LicensingPluginSetup; +} + +export class EnterpriseSearchPlugin implements Plugin { + private config: ClientConfigType; + private hasCheckedPublicUrl: boolean = false; + + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } + + public setup(core: CoreSetup, plugins: PluginsSetup) { + const config = { host: this.config.host }; + + core.application.register({ + id: 'appSearch', + title: 'App Search', + appRoute: '/app/enterprise_search/app_search', + category: DEFAULT_APP_CATEGORIES.enterpriseSearch, + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); + + await this.setPublicUrl(config, coreStart.http); + + const { renderApp } = await import('./applications'); + const { AppSearch } = await import('./applications/app_search'); + + return renderApp(AppSearch, coreStart, params, config, plugins); + }, + }); + // TODO: Workplace Search will need to register its own plugin. + + plugins.home.featureCatalogue.register({ + id: 'appSearch', + title: 'App Search', + icon: AppSearchLogo, + description: + 'Leverage dashboards, analytics, and APIs for advanced application search made simple.', + path: '/app/enterprise_search/app_search', + category: FeatureCatalogueCategory.DATA, + showOnHomePage: true, + }); + // TODO: Workplace Search will need to register its own feature catalogue section/card. + } + + public start(core: CoreStart) {} + + public stop() {} + + private async setPublicUrl(config: ClientConfigType, http: HttpSetup) { + if (!config.host) return; // No API to check + if (this.hasCheckedPublicUrl) return; // We've already performed the check + + const publicUrl = await getPublicUrl(http); + if (publicUrl) config.host = publicUrl; + this.hasCheckedPublicUrl = true; + } +} diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts new file mode 100644 index 00000000000000..e95056b8713248 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; + +jest.mock('../../../../../../src/core/server', () => ({ + SavedObjectsErrorHelpers: { + isNotFoundError: jest.fn(), + }, +})); +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; + +import { registerTelemetryUsageCollector, incrementUICounter } from './telemetry'; + +describe('App Search Telemetry Usage Collector', () => { + const mockLogger = loggingSystemMock.create().get(); + + const makeUsageCollectorStub = jest.fn(); + const registerStub = jest.fn(); + const usageCollectionMock = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + } as any; + + const savedObjectsRepoStub = { + get: () => ({ + attributes: { + 'ui_viewed.setup_guide': 10, + 'ui_viewed.engines_overview': 20, + 'ui_error.cannot_connect': 3, + 'ui_clicked.create_first_engine_button': 40, + 'ui_clicked.header_launch_button': 50, + 'ui_clicked.engine_table_link': 60, + }, + }), + incrementCounter: jest.fn(), + }; + const savedObjectsMock = { + createInternalRepository: jest.fn(() => savedObjectsRepoStub), + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('registerTelemetryUsageCollector', () => { + it('should make and register the usage collector', () => { + registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger); + + expect(registerStub).toHaveBeenCalledTimes(1); + expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); + expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('app_search'); + expect(makeUsageCollectorStub.mock.calls[0][0].isReady()).toBe(true); + }); + }); + + describe('fetchTelemetryMetrics', () => { + it('should return existing saved objects data', async () => { + registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger); + const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch(); + + expect(savedObjectsCounts).toEqual({ + ui_viewed: { + setup_guide: 10, + engines_overview: 20, + }, + ui_error: { + cannot_connect: 3, + }, + ui_clicked: { + create_first_engine_button: 40, + header_launch_button: 50, + engine_table_link: 60, + }, + }); + }); + + it('should return a default telemetry object if no saved data exists', async () => { + const emptySavedObjectsMock = { + createInternalRepository: () => ({ + get: () => ({ attributes: null }), + }), + } as any; + + registerTelemetryUsageCollector(usageCollectionMock, emptySavedObjectsMock, mockLogger); + const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch(); + + expect(savedObjectsCounts).toEqual({ + ui_viewed: { + setup_guide: 0, + engines_overview: 0, + }, + ui_error: { + cannot_connect: 0, + }, + ui_clicked: { + create_first_engine_button: 0, + header_launch_button: 0, + engine_table_link: 0, + }, + }); + }); + + it('should not throw but log a warning if saved objects errors', async () => { + const errorSavedObjectsMock = { createInternalRepository: () => ({}) } as any; + registerTelemetryUsageCollector(usageCollectionMock, errorSavedObjectsMock, mockLogger); + + // Without log warning (not found) + (SavedObjectsErrorHelpers.isNotFoundError as jest.Mock).mockImplementationOnce(() => true); + await makeUsageCollectorStub.mock.calls[0][0].fetch(); + + expect(mockLogger.warn).not.toHaveBeenCalled(); + + // With log warning + (SavedObjectsErrorHelpers.isNotFoundError as jest.Mock).mockImplementationOnce(() => false); + await makeUsageCollectorStub.mock.calls[0][0].fetch(); + + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to retrieve App Search telemetry data: TypeError: savedObjectsRepository.get is not a function' + ); + }); + }); + + describe('incrementUICounter', () => { + it('should increment the saved objects internal repository', async () => { + const response = await incrementUICounter({ + savedObjects: savedObjectsMock, + uiAction: 'ui_clicked', + metric: 'button', + }); + + expect(savedObjectsRepoStub.incrementCounter).toHaveBeenCalledWith( + 'app_search_telemetry', + 'app_search_telemetry', + 'ui_clicked.button' + ); + expect(response).toEqual({ success: true }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts new file mode 100644 index 00000000000000..a10f96907ad28a --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -0,0 +1,156 @@ +/* + * 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 { get } from 'lodash'; +import { + ISavedObjectsRepository, + SavedObjectsServiceStart, + SavedObjectAttributes, + Logger, +} from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +// This throws `Error: Cannot find module 'src/core/server'` if I import it via alias ¯\_(ツ)_/¯ +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; + +interface ITelemetry { + ui_viewed: { + setup_guide: number; + engines_overview: number; + }; + ui_error: { + cannot_connect: number; + }; + ui_clicked: { + create_first_engine_button: number; + header_launch_button: number; + engine_table_link: number; + }; +} + +export const AS_TELEMETRY_NAME = 'app_search_telemetry'; + +/** + * Register the telemetry collector + */ + +export const registerTelemetryUsageCollector = ( + usageCollection: UsageCollectionSetup, + savedObjects: SavedObjectsServiceStart, + log: Logger +) => { + const telemetryUsageCollector = usageCollection.makeUsageCollector({ + type: 'app_search', + fetch: async () => fetchTelemetryMetrics(savedObjects, log), + isReady: () => true, + schema: { + ui_viewed: { + setup_guide: { type: 'long' }, + engines_overview: { type: 'long' }, + }, + ui_error: { + cannot_connect: { type: 'long' }, + }, + ui_clicked: { + create_first_engine_button: { type: 'long' }, + header_launch_button: { type: 'long' }, + engine_table_link: { type: 'long' }, + }, + }, + }); + usageCollection.registerCollector(telemetryUsageCollector); +}; + +/** + * Fetch the aggregated telemetry metrics from our saved objects + */ + +const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log: Logger) => { + const savedObjectsRepository = savedObjects.createInternalRepository(); + const savedObjectAttributes = (await getSavedObjectAttributesFromRepo( + savedObjectsRepository, + log + )) as SavedObjectAttributes; + + const defaultTelemetrySavedObject: ITelemetry = { + ui_viewed: { + setup_guide: 0, + engines_overview: 0, + }, + ui_error: { + cannot_connect: 0, + }, + ui_clicked: { + create_first_engine_button: 0, + header_launch_button: 0, + engine_table_link: 0, + }, + }; + + // If we don't have an existing/saved telemetry object, return the default + if (!savedObjectAttributes) { + return defaultTelemetrySavedObject; + } + + return { + ui_viewed: { + setup_guide: get(savedObjectAttributes, 'ui_viewed.setup_guide', 0), + engines_overview: get(savedObjectAttributes, 'ui_viewed.engines_overview', 0), + }, + ui_error: { + cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0), + }, + ui_clicked: { + create_first_engine_button: get( + savedObjectAttributes, + 'ui_clicked.create_first_engine_button', + 0 + ), + header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0), + engine_table_link: get(savedObjectAttributes, 'ui_clicked.engine_table_link', 0), + }, + } as ITelemetry; +}; + +/** + * Helper function - fetches saved objects attributes + */ + +const getSavedObjectAttributesFromRepo = async ( + savedObjectsRepository: ISavedObjectsRepository, + log: Logger +) => { + try { + return (await savedObjectsRepository.get(AS_TELEMETRY_NAME, AS_TELEMETRY_NAME)).attributes; + } catch (e) { + if (!SavedObjectsErrorHelpers.isNotFoundError(e)) { + log.warn(`Failed to retrieve App Search telemetry data: ${e}`); + } + return null; + } +}; + +/** + * Set saved objection attributes - used by telemetry route + */ + +interface IIncrementUICounter { + savedObjects: SavedObjectsServiceStart; + uiAction: string; + metric: string; +} + +export async function incrementUICounter({ savedObjects, uiAction, metric }: IIncrementUICounter) { + const internalRepository = savedObjects.createInternalRepository(); + + await internalRepository.incrementCounter( + AS_TELEMETRY_NAME, + AS_TELEMETRY_NAME, + `${uiAction}.${metric}` // e.g., ui_viewed.setup_guide + ); + + return { success: true }; +} diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts new file mode 100644 index 00000000000000..1e4159124ed942 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { EnterpriseSearchPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new EnterpriseSearchPlugin(initializerContext); +}; + +export const configSchema = schema.object({ + host: schema.maybe(schema.string()), + enabled: schema.boolean({ defaultValue: true }), + accessCheckTimeout: schema.number({ defaultValue: 5000 }), + accessCheckTimeoutWarning: schema.number({ defaultValue: 300 }), +}); + +export type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + host: true, + }, +}; diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts new file mode 100644 index 00000000000000..11d4a387b533ff --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -0,0 +1,128 @@ +/* + * 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. + */ + +jest.mock('./enterprise_search_config_api', () => ({ + callEnterpriseSearchConfigAPI: jest.fn(), +})); +import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; + +import { checkAccess } from './check_access'; + +describe('checkAccess', () => { + const mockSecurity = { + authz: { + mode: { + useRbacForRequest: () => true, + }, + checkPrivilegesWithRequest: () => ({ + globally: () => ({ + hasAllRequested: false, + }), + }), + actions: { + ui: { + get: () => null, + }, + }, + }, + }; + const mockDependencies = { + request: {}, + config: { host: 'http://localhost:3002' }, + security: mockSecurity, + } as any; + + describe('when security is disabled', () => { + it('should allow all access', async () => { + const security = undefined; + expect(await checkAccess({ ...mockDependencies, security })).toEqual({ + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }); + }); + }); + + describe('when the user is a superuser', () => { + it('should allow all access', async () => { + const security = { + ...mockSecurity, + authz: { + mode: { useRbacForRequest: () => true }, + checkPrivilegesWithRequest: () => ({ + globally: () => ({ + hasAllRequested: true, + }), + }), + actions: { ui: { get: () => {} } }, + }, + }; + expect(await checkAccess({ ...mockDependencies, security })).toEqual({ + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, + }); + }); + + it('falls back to assuming a non-superuser role if auth credentials are missing', async () => { + const security = { + authz: { + ...mockSecurity.authz, + checkPrivilegesWithRequest: () => ({ + globally: () => Promise.reject({ statusCode: 403 }), + }), + }, + }; + expect(await checkAccess({ ...mockDependencies, security })).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); + + it('throws other authz errors', async () => { + const security = { + authz: { + ...mockSecurity.authz, + checkPrivilegesWithRequest: undefined, + }, + }; + await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow(); + }); + }); + + describe('when the user is a non-superuser', () => { + describe('when enterpriseSearch.host is not set in kibana.yml', () => { + it('should deny all access', async () => { + const config = { host: undefined }; + expect(await checkAccess({ ...mockDependencies, config })).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); + }); + + describe('when enterpriseSearch.host is set in kibana.yml', () => { + it('should make a http call and return the access response', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({ + access: { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: true, + }, + })); + expect(await checkAccess(mockDependencies)).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: true, + }); + }); + + it('falls back to no access if no http response', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({})); + expect(await checkAccess(mockDependencies)).toEqual({ + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts new file mode 100644 index 00000000000000..0239cb6422d032 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -0,0 +1,76 @@ +/* + * 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 { KibanaRequest, Logger } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { ConfigType } from '../'; + +import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; + +interface ICheckAccess { + request: KibanaRequest; + security?: SecurityPluginSetup; + config: ConfigType; + log: Logger; +} +export interface IAccess { + hasAppSearchAccess: boolean; + hasWorkplaceSearchAccess: boolean; +} + +const ALLOW_ALL_PLUGINS = { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: true, +}; +const DENY_ALL_PLUGINS = { + hasAppSearchAccess: false, + hasWorkplaceSearchAccess: false, +}; + +/** + * Determines whether the user has access to our Enterprise Search products + * via HTTP call. If not, we hide the corresponding plugin links from the + * nav and catalogue in `plugin.ts`, which disables plugin access + */ +export const checkAccess = async ({ + config, + security, + request, + log, +}: ICheckAccess): Promise => { + // If security has been disabled, always show the plugin + if (!security?.authz.mode.useRbacForRequest(request)) { + return ALLOW_ALL_PLUGINS; + } + + // If the user is a "superuser" or has the base Kibana all privilege globally, always show the plugin + const isSuperUser = async (): Promise => { + try { + const { hasAllRequested } = await security.authz + .checkPrivilegesWithRequest(request) + .globally(security.authz.actions.ui.get('enterpriseSearch', 'all')); + return hasAllRequested; + } catch (err) { + if (err.statusCode === 401 || err.statusCode === 403) { + return false; + } + throw err; + } + }; + if (await isSuperUser()) { + return ALLOW_ALL_PLUGINS; + } + + // Hide the plugin when enterpriseSearch.host is not defined in kibana.yml + if (!config.host) { + return DENY_ALL_PLUGINS; + } + + // When enterpriseSearch.host is defined in kibana.yml, + // make a HTTP call which returns product access + const { access } = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; + return access || DENY_ALL_PLUGINS; +}; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts new file mode 100644 index 00000000000000..cf35a458b48258 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -0,0 +1,111 @@ +/* + * 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. + */ + +jest.mock('node-fetch'); +const fetchMock = require('node-fetch') as jest.Mock; +const { Response } = jest.requireActual('node-fetch'); + +import { loggingSystemMock } from 'src/core/server/mocks'; + +import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; + +describe('callEnterpriseSearchConfigAPI', () => { + const mockConfig = { + host: 'http://localhost:3002', + accessCheckTimeout: 200, + accessCheckTimeoutWarning: 100, + }; + const mockRequest = { + url: { path: '/app/kibana' }, + headers: { authorization: '==someAuth' }, + }; + const mockDependencies = { + config: mockConfig, + request: mockRequest, + log: loggingSystemMock.create().get(), + } as any; + + const mockResponse = { + version: { + number: '1.0.0', + }, + settings: { + external_url: 'http://some.vanity.url/', + }, + access: { + user: 'someuser', + products: { + app_search: true, + workplace_search: false, + }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls the config API endpoint', async () => { + fetchMock.mockImplementationOnce((url: string) => { + expect(url).toEqual('http://localhost:3002/api/ent/v1/internal/client_config'); + return Promise.resolve(new Response(JSON.stringify(mockResponse))); + }); + + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ + publicUrl: 'http://some.vanity.url/', + access: { + hasAppSearchAccess: true, + hasWorkplaceSearchAccess: false, + }, + }); + }); + + it('returns early if config.host is not set', async () => { + const config = { host: '' }; + + expect(await callEnterpriseSearchConfigAPI({ ...mockDependencies, config })).toEqual({}); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('handles server errors', async () => { + fetchMock.mockImplementationOnce(() => { + return Promise.reject('500'); + }); + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); + expect(mockDependencies.log.error).toHaveBeenCalledWith( + 'Could not perform access check to Enterprise Search: 500' + ); + + fetchMock.mockImplementationOnce(() => { + return Promise.resolve('Bad Data'); + }); + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); + expect(mockDependencies.log.error).toHaveBeenCalledWith( + 'Could not perform access check to Enterprise Search: TypeError: response.json is not a function' + ); + }); + + it('handles timeouts', async () => { + jest.useFakeTimers(); + + // Warning + callEnterpriseSearchConfigAPI(mockDependencies); + jest.advanceTimersByTime(150); + expect(mockDependencies.log.warn).toHaveBeenCalledWith( + 'Enterprise Search access check took over 100ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.' + ); + + // Timeout + fetchMock.mockImplementationOnce(async () => { + jest.advanceTimersByTime(250); + return Promise.reject({ name: 'AbortError' }); + }); + expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); + expect(mockDependencies.log.warn).toHaveBeenCalledWith( + "Exceeded 200ms timeout while checking http://localhost:3002. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses." + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts new file mode 100644 index 00000000000000..7a6d1eac1b4545 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -0,0 +1,78 @@ +/* + * 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 AbortController from 'abort-controller'; +import fetch from 'node-fetch'; + +import { KibanaRequest, Logger } from 'src/core/server'; +import { ConfigType } from '../'; +import { IAccess } from './check_access'; + +interface IParams { + request: KibanaRequest; + config: ConfigType; + log: Logger; +} +interface IReturn { + publicUrl?: string; + access?: IAccess; +} + +/** + * Calls an internal Enterprise Search API endpoint which returns + * useful various settings (e.g. product access, external URL) + * needed by the Kibana plugin at the setup stage + */ +const ENDPOINT = '/api/ent/v1/internal/client_config'; + +export const callEnterpriseSearchConfigAPI = async ({ + config, + log, + request, +}: IParams): Promise => { + if (!config.host) return {}; + + const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.`; + const TIMEOUT_MESSAGE = `Exceeded ${config.accessCheckTimeout}ms timeout while checking ${config.host}. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses.`; + const CONNECTION_ERROR = 'Could not perform access check to Enterprise Search'; + + const warningTimeout = setTimeout(() => { + log.warn(TIMEOUT_WARNING); + }, config.accessCheckTimeoutWarning); + + const controller = new AbortController(); + const timeout = setTimeout(() => { + controller.abort(); + }, config.accessCheckTimeout); + + try { + const enterpriseSearchUrl = encodeURI(`${config.host}${ENDPOINT}`); + const response = await fetch(enterpriseSearchUrl, { + headers: { Authorization: request.headers.authorization as string }, + signal: controller.signal, + }); + const data = await response.json(); + + return { + publicUrl: data?.settings?.external_url, + access: { + hasAppSearchAccess: !!data?.access?.products?.app_search, + hasWorkplaceSearchAccess: !!data?.access?.products?.workplace_search, + }, + }; + } catch (err) { + if (err.name === 'AbortError') { + log.warn(TIMEOUT_MESSAGE); + } else { + log.error(`${CONNECTION_ERROR}: ${err.toString()}`); + if (err instanceof Error) log.debug(err.stack as string); + } + return {}; + } finally { + clearTimeout(warningTimeout); + clearTimeout(timeout); + } +}; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts new file mode 100644 index 00000000000000..70be8600862e9c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -0,0 +1,121 @@ +/* + * 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 { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { + Plugin, + PluginInitializerContext, + CoreSetup, + Logger, + SavedObjectsServiceStart, + IRouter, + KibanaRequest, +} from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; + +import { ConfigType } from './'; +import { checkAccess } from './lib/check_access'; +import { registerPublicUrlRoute } from './routes/enterprise_search/public_url'; +import { registerEnginesRoute } from './routes/app_search/engines'; +import { registerTelemetryRoute } from './routes/app_search/telemetry'; +import { registerTelemetryUsageCollector } from './collectors/app_search/telemetry'; +import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; + +export interface PluginsSetup { + usageCollection?: UsageCollectionSetup; + security?: SecurityPluginSetup; + features: FeaturesPluginSetup; +} + +export interface IRouteDependencies { + router: IRouter; + config: ConfigType; + log: Logger; + getSavedObjectsService?(): SavedObjectsServiceStart; +} + +export class EnterpriseSearchPlugin implements Plugin { + private config: Observable; + private logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.create(); + this.logger = initializerContext.logger.get(); + } + + public async setup( + { capabilities, http, savedObjects, getStartServices }: CoreSetup, + { usageCollection, security, features }: PluginsSetup + ) { + const config = await this.config.pipe(first()).toPromise(); + + /** + * Register space/feature control + */ + features.registerFeature({ + id: 'enterpriseSearch', + name: 'Enterprise Search', + order: 0, + icon: 'logoEnterpriseSearch', + navLinkId: 'appSearch', // TODO - remove this once functional tests no longer rely on navLinkId + app: ['kibana', 'appSearch'], // TODO: 'enterpriseSearch', 'workplaceSearch' + catalogue: ['appSearch'], // TODO: 'enterpriseSearch', 'workplaceSearch' + privileges: null, + }); + + /** + * Register user access to the Enterprise Search plugins + */ + capabilities.registerSwitcher(async (request: KibanaRequest) => { + const dependencies = { config, security, request, log: this.logger }; + + const { hasAppSearchAccess } = await checkAccess(dependencies); + // TODO: hasWorkplaceSearchAccess + + return { + navLinks: { + appSearch: hasAppSearchAccess, + }, + catalogue: { + appSearch: hasAppSearchAccess, + }, + }; + }); + + /** + * Register routes + */ + const router = http.createRouter(); + const dependencies = { router, config, log: this.logger }; + + registerPublicUrlRoute(dependencies); + registerEnginesRoute(dependencies); + + /** + * Bootstrap the routes, saved objects, and collector for telemetry + */ + savedObjects.registerType(appSearchTelemetryType); + let savedObjectsStarted: SavedObjectsServiceStart; + + getStartServices().then(([coreStart]) => { + savedObjectsStarted = coreStart.savedObjects; + if (usageCollection) { + registerTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger); + } + }); + registerTelemetryRoute({ + ...dependencies, + getSavedObjectsService: () => savedObjectsStarted, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts new file mode 100644 index 00000000000000..3cca5e21ce9c3c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { MockRouter } from './router.mock'; +export { mockConfig, mockLogger, mockDependencies } from './routerDependencies.mock'; diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts new file mode 100644 index 00000000000000..1ca7755979f99b --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts @@ -0,0 +1,102 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { + IRouter, + KibanaRequest, + RequestHandlerContext, + RouteValidatorConfig, +} from 'src/core/server'; + +/** + * Test helper that mocks Kibana's router and DRYs out various helper (callRoute, schema validation) + */ + +type methodType = 'get' | 'post' | 'put' | 'patch' | 'delete'; +type payloadType = 'params' | 'query' | 'body'; + +interface IMockRouterProps { + method: methodType; + payload?: payloadType; +} +interface IMockRouterRequest { + body?: object; + query?: object; + params?: object; +} +type TMockRouterRequest = KibanaRequest | IMockRouterRequest; + +export class MockRouter { + public router!: jest.Mocked; + public method: methodType; + public payload?: payloadType; + public response = httpServerMock.createResponseFactory(); + + constructor({ method, payload }: IMockRouterProps) { + this.createRouter(); + this.method = method; + this.payload = payload; + } + + public createRouter = () => { + this.router = httpServiceMock.createRouter(); + }; + + public callRoute = async (request: TMockRouterRequest) => { + const [, handler] = this.router[this.method].mock.calls[0]; + + const context = {} as jest.Mocked; + await handler(context, httpServerMock.createKibanaRequest(request as any), this.response); + }; + + /** + * Schema validation helpers + */ + + public validateRoute = (request: TMockRouterRequest) => { + if (!this.payload) throw new Error('Cannot validate wihout a payload type specified.'); + + const [config] = this.router[this.method].mock.calls[0]; + const validate = config.validate as RouteValidatorConfig<{}, {}, {}>; + + const payloadValidation = validate[this.payload] as { validate(request: KibanaRequest): void }; + const payloadRequest = request[this.payload] as KibanaRequest; + + payloadValidation.validate(payloadRequest); + }; + + public shouldValidate = (request: TMockRouterRequest) => { + expect(() => this.validateRoute(request)).not.toThrow(); + }; + + public shouldThrow = (request: TMockRouterRequest) => { + expect(() => this.validateRoute(request)).toThrow(); + }; +} + +/** + * Example usage: + */ +// const mockRouter = new MockRouter({ method: 'get', payload: 'body' }); +// +// beforeEach(() => { +// jest.clearAllMocks(); +// mockRouter.createRouter(); +// +// registerExampleRoute({ router: mockRouter.router, ...dependencies }); // Whatever other dependencies the route needs +// }); + +// it('hits the endpoint successfully', async () => { +// await mockRouter.callRoute({ body: { foo: 'bar' } }); +// +// expect(mockRouter.response.ok).toHaveBeenCalled(); +// }); + +// it('validates', () => { +// const request = { body: { foo: 'bar' } }; +// mockRouter.shouldValidate(request); +// }); diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts new file mode 100644 index 00000000000000..9b6fa30271d613 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts @@ -0,0 +1,27 @@ +/* + * 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 { loggingSystemMock } from 'src/core/server/mocks'; +import { ConfigType } from '../../'; + +export const mockLogger = loggingSystemMock.createLogger().get(); + +export const mockConfig = { + enabled: true, + host: 'http://localhost:3002', + accessCheckTimeout: 5000, + accessCheckTimeoutWarning: 300, +} as ConfigType; + +/** + * This is useful for tests that don't use either config or log, + * but should still pass them in to pass Typescript definitions + */ +export const mockDependencies = { + // Mock router should be handled on a per-test basis + config: mockConfig, + log: mockLogger, +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts new file mode 100644 index 00000000000000..d5b1bc50034562 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { MockRouter, mockConfig, mockLogger } from '../__mocks__'; + +import { registerEnginesRoute } from './engines'; + +jest.mock('node-fetch'); +const fetch = jest.requireActual('node-fetch'); +const { Response } = fetch; +const fetchMock = require('node-fetch') as jest.Mocked; + +describe('engine routes', () => { + describe('GET /api/app_search/engines', () => { + const AUTH_HEADER = 'Basic 123'; + const mockRequest = { + headers: { + authorization: AUTH_HEADER, + }, + query: { + type: 'indexed', + pageIndex: 1, + }, + }; + + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + + registerEnginesRoute({ + router: mockRouter.router, + log: mockLogger, + config: mockConfig, + }); + }); + + describe('when the underlying App Search API returns a 200', () => { + beforeEach(() => { + AppSearchAPI.shouldBeCalledWith( + `http://localhost:3002/as/engines/collection?type=indexed&page%5Bcurrent%5D=1&page%5Bsize%5D=10`, + { headers: { Authorization: AUTH_HEADER } } + ).andReturn({ + results: [{ name: 'engine1' }], + meta: { page: { total_results: 1 } }, + }); + }); + + it('should return 200 with a list of engines from the App Search API', async () => { + await mockRouter.callRoute(mockRequest); + + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: { results: [{ name: 'engine1' }], meta: { page: { total_results: 1 } } }, + }); + }); + }); + + describe('when the App Search URL is invalid', () => { + beforeEach(() => { + AppSearchAPI.shouldBeCalledWith( + `http://localhost:3002/as/engines/collection?type=indexed&page%5Bcurrent%5D=1&page%5Bsize%5D=10`, + { headers: { Authorization: AUTH_HEADER } } + ).andReturnError(); + }); + + it('should return 404 with a message', async () => { + await mockRouter.callRoute(mockRequest); + + expect(mockRouter.response.notFound).toHaveBeenCalledWith({ + body: 'cannot-connect', + }); + expect(mockLogger.error).toHaveBeenCalledWith('Cannot connect to App Search: Failed'); + expect(mockLogger.debug).not.toHaveBeenCalled(); + }); + }); + + describe('when the App Search API returns invalid data', () => { + beforeEach(() => { + AppSearchAPI.shouldBeCalledWith( + `http://localhost:3002/as/engines/collection?type=indexed&page%5Bcurrent%5D=1&page%5Bsize%5D=10`, + { headers: { Authorization: AUTH_HEADER } } + ).andReturnInvalidData(); + }); + + it('should return 404 with a message', async () => { + await mockRouter.callRoute(mockRequest); + + expect(mockRouter.response.notFound).toHaveBeenCalledWith({ + body: 'cannot-connect', + }); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Cannot connect to App Search: Error: Invalid data received from App Search: {"foo":"bar"}' + ); + expect(mockLogger.debug).toHaveBeenCalled(); + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { query: { type: 'meta', pageIndex: 5 } }; + mockRouter.shouldValidate(request); + }); + + it('wrong pageIndex type', () => { + const request = { query: { type: 'indexed', pageIndex: 'indexed' } }; + mockRouter.shouldThrow(request); + }); + + it('wrong type string', () => { + const request = { query: { type: 'invalid', pageIndex: 1 } }; + mockRouter.shouldThrow(request); + }); + + it('missing pageIndex', () => { + const request = { query: { type: 'indexed' } }; + mockRouter.shouldThrow(request); + }); + + it('missing type', () => { + const request = { query: { pageIndex: 1 } }; + mockRouter.shouldThrow(request); + }); + }); + + const AppSearchAPI = { + shouldBeCalledWith(expectedUrl: string, expectedParams: object) { + return { + andReturn(response: object) { + fetchMock.mockImplementation((url: string, params: object) => { + expect(url).toEqual(expectedUrl); + expect(params).toEqual(expectedParams); + + return Promise.resolve(new Response(JSON.stringify(response))); + }); + }, + andReturnInvalidData() { + fetchMock.mockImplementation((url: string, params: object) => { + expect(url).toEqual(expectedUrl); + expect(params).toEqual(expectedParams); + + return Promise.resolve(new Response(JSON.stringify({ foo: 'bar' }))); + }); + }, + andReturnError() { + fetchMock.mockImplementation((url: string, params: object) => { + expect(url).toEqual(expectedUrl); + expect(params).toEqual(expectedParams); + + return Promise.reject('Failed'); + }); + }, + }; + }, + }; + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts new file mode 100644 index 00000000000000..ca83c0e187ddb5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -0,0 +1,59 @@ +/* + * 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 fetch from 'node-fetch'; +import querystring from 'querystring'; +import { schema } from '@kbn/config-schema'; + +import { IRouteDependencies } from '../../plugin'; +import { ENGINES_PAGE_SIZE } from '../../../common/constants'; + +export function registerEnginesRoute({ router, config, log }: IRouteDependencies) { + router.get( + { + path: '/api/app_search/engines', + validate: { + query: schema.object({ + type: schema.oneOf([schema.literal('indexed'), schema.literal('meta')]), + pageIndex: schema.number(), + }), + }, + }, + async (context, request, response) => { + try { + const enterpriseSearchUrl = config.host as string; + const { type, pageIndex } = request.query; + + const params = querystring.stringify({ + type, + 'page[current]': pageIndex, + 'page[size]': ENGINES_PAGE_SIZE, + }); + const url = `${encodeURI(enterpriseSearchUrl)}/as/engines/collection?${params}`; + + const enginesResponse = await fetch(url, { + headers: { Authorization: request.headers.authorization as string }, + }); + + const engines = await enginesResponse.json(); + const hasValidData = + Array.isArray(engines?.results) && typeof engines?.meta?.page?.total_results === 'number'; + + if (hasValidData) { + return response.ok({ body: engines }); + } else { + // Either a completely incorrect Enterprise Search host URL was configured, or App Search is returning bad data + throw new Error(`Invalid data received from App Search: ${JSON.stringify(engines)}`); + } + } catch (e) { + log.error(`Cannot connect to App Search: ${e.toString()}`); + if (e instanceof Error) log.debug(e.stack as string); + + return response.notFound({ body: 'cannot-connect' }); + } + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts new file mode 100644 index 00000000000000..e2d5fbcec37056 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { MockRouter, mockConfig, mockLogger } from '../__mocks__'; + +import { registerTelemetryRoute } from './telemetry'; + +jest.mock('../../collectors/app_search/telemetry', () => ({ + incrementUICounter: jest.fn(), +})); +import { incrementUICounter } from '../../collectors/app_search/telemetry'; + +/** + * Since these route callbacks are so thin, these serve simply as integration tests + * to ensure they're wired up to the collector functions correctly. Business logic + * is tested more thoroughly in the collectors/telemetry tests. + */ +describe('App Search Telemetry API', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + + registerTelemetryRoute({ + router: mockRouter.router, + getSavedObjectsService: () => savedObjectsServiceMock.createStartContract(), + log: mockLogger, + config: mockConfig, + }); + }); + + describe('PUT /api/app_search/telemetry', () => { + it('increments the saved objects counter', async () => { + const successResponse = { success: true }; + (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => successResponse)); + + await mockRouter.callRoute({ body: { action: 'viewed', metric: 'setup_guide' } }); + + expect(incrementUICounter).toHaveBeenCalledWith({ + savedObjects: expect.any(Object), + uiAction: 'ui_viewed', + metric: 'setup_guide', + }); + expect(mockRouter.response.ok).toHaveBeenCalledWith({ body: successResponse }); + }); + + it('throws an error when incrementing fails', async () => { + (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => Promise.reject('Failed'))); + + await mockRouter.callRoute({ body: { action: 'error', metric: 'error' } }); + + expect(incrementUICounter).toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalled(); + expect(mockRouter.response.internalError).toHaveBeenCalled(); + }); + + it('throws an error if the Saved Objects service is unavailable', async () => { + jest.clearAllMocks(); + registerTelemetryRoute({ + router: mockRouter.router, + getSavedObjectsService: null, + log: mockLogger, + } as any); + await mockRouter.callRoute({}); + + expect(incrementUICounter).not.toHaveBeenCalled(); + expect(mockLogger.error).toHaveBeenCalled(); + expect(mockRouter.response.internalError).toHaveBeenCalled(); + expect(loggingSystemMock.collect(mockLogger).error[0][0]).toEqual( + expect.stringContaining( + 'App Search UI telemetry error: Error: Could not find Saved Objects service' + ) + ); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: { action: 'viewed', metric: 'setup_guide' } }; + mockRouter.shouldValidate(request); + }); + + it('wrong action string', () => { + const request = { body: { action: 'invalid', metric: 'setup_guide' } }; + mockRouter.shouldThrow(request); + }); + + it('wrong metric type', () => { + const request = { body: { action: 'clicked', metric: true } }; + mockRouter.shouldThrow(request); + }); + + it('action is missing', () => { + const request = { body: { metric: 'engines_overview' } }; + mockRouter.shouldThrow(request); + }); + + it('metric is missing', () => { + const request = { body: { action: 'error' } }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts new file mode 100644 index 00000000000000..4cc9b64adc0927 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts @@ -0,0 +1,50 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { IRouteDependencies } from '../../plugin'; +import { incrementUICounter } from '../../collectors/app_search/telemetry'; + +export function registerTelemetryRoute({ + router, + getSavedObjectsService, + log, +}: IRouteDependencies) { + router.put( + { + path: '/api/app_search/telemetry', + validate: { + body: schema.object({ + action: schema.oneOf([ + schema.literal('viewed'), + schema.literal('clicked'), + schema.literal('error'), + ]), + metric: schema.string(), + }), + }, + }, + async (ctx, request, response) => { + const { action, metric } = request.body; + + try { + if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); + + return response.ok({ + body: await incrementUICounter({ + savedObjects: getSavedObjectsService(), + uiAction: `ui_${action}`, + metric, + }), + }); + } catch (e) { + log.error(`App Search UI telemetry error: ${e instanceof Error ? e.stack : e.toString()}`); + return response.internalError({ body: 'App Search UI telemetry failed' }); + } + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts new file mode 100644 index 00000000000000..846aae3fce56f0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { MockRouter, mockDependencies } from '../__mocks__'; + +jest.mock('../../lib/enterprise_search_config_api', () => ({ + callEnterpriseSearchConfigAPI: jest.fn(), +})); +import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; + +import { registerPublicUrlRoute } from './public_url'; + +describe('Enterprise Search Public URL API', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + mockRouter = new MockRouter({ method: 'get' }); + + registerPublicUrlRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + describe('GET /api/enterprise_search/public_url', () => { + it('returns a publicUrl', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve({ publicUrl: 'http://some.vanity.url' }); + }); + + await mockRouter.callRoute({}); + + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: { publicUrl: 'http://some.vanity.url' }, + headers: { 'content-type': 'application/json' }, + }); + }); + + // For the most part, all error logging is handled by callEnterpriseSearchConfigAPI. + // This endpoint should mostly just fall back gracefully to an empty string + it('falls back to an empty string', async () => { + await mockRouter.callRoute({}); + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: { publicUrl: '' }, + headers: { 'content-type': 'application/json' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts new file mode 100644 index 00000000000000..a9edd4eb10da03 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouteDependencies } from '../../plugin'; +import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; + +export function registerPublicUrlRoute({ router, config, log }: IRouteDependencies) { + router.get( + { + path: '/api/enterprise_search/public_url', + validate: false, + }, + async (context, request, response) => { + const { publicUrl = '' } = + (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; + + return response.ok({ + body: { publicUrl }, + headers: { 'content-type': 'application/json' }, + }); + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts new file mode 100644 index 00000000000000..32322d494b5e21 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +/* istanbul ignore file */ + +import { SavedObjectsType } from 'src/core/server'; +import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry'; + +export const appSearchTelemetryType: SavedObjectsType = { + name: AS_TELEMETRY_NAME, + hidden: false, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: {}, + }, +}; diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 06f064a379fe6e..8a499a3eba8fa7 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -189,13 +189,15 @@ describe('features', () => { group: 'global', expectManageSpaces: true, expectGetFeatures: true, + expectEnterpriseSearch: true, }, { group: 'space', expectManageSpaces: false, expectGetFeatures: false, + expectEnterpriseSearch: false, }, -].forEach(({ group, expectManageSpaces, expectGetFeatures }) => { +].forEach(({ group, expectManageSpaces, expectGetFeatures, expectEnterpriseSearch }) => { describe(`${group}`, () => { test('actions defined in any feature privilege are included in `all`', () => { const features: Feature[] = [ @@ -256,6 +258,7 @@ describe('features', () => { actions.ui.get('management', 'kibana', 'spaces'), ] : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), actions.ui.get('catalogue', 'all-catalogue-1'), actions.ui.get('catalogue', 'all-catalogue-2'), actions.ui.get('management', 'all-management', 'all-management-1'), @@ -450,6 +453,7 @@ describe('features', () => { actions.ui.get('management', 'kibana', 'spaces'), ] : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), ]); expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); }); @@ -514,6 +518,7 @@ describe('features', () => { actions.ui.get('management', 'kibana', 'spaces'), ] : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), ]); expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); }); @@ -579,6 +584,7 @@ describe('features', () => { actions.ui.get('management', 'kibana', 'spaces'), ] : []), + ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), ]); expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); }); @@ -840,6 +846,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), actions.ui.get('foo', 'foo'), ]); expect(actual).toHaveProperty('global.read', [ @@ -991,6 +998,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), @@ -1189,6 +1197,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), ]); expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); @@ -1315,6 +1324,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), @@ -1477,6 +1487,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), ]); expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); @@ -1592,6 +1603,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), actions.savedObject.get('all-sub-feature-type', 'find'), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 5a15290a7f1a29..f9ee5fc7501275 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -101,6 +101,7 @@ export function privilegesFactory( actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('enterpriseSearch', 'all'), ...allActions, ], read: [actions.login, actions.version, ...readActions], diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 13d7c62316040b..1ea16a2a9940c9 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -7,6 +7,40 @@ } } }, + "app_search": { + "properties": { + "ui_viewed": { + "properties": { + "setup_guide": { + "type": "long" + }, + "engines_overview": { + "type": "long" + } + } + }, + "ui_error": { + "properties": { + "cannot_connect": { + "type": "long" + } + } + }, + "ui_clicked": { + "properties": { + "create_first_engine_button": { + "type": "long" + }, + "header_launch_button": { + "type": "long" + }, + "engine_table_link": { + "type": "long" + } + } + } + } + }, "fileUploadTelemetry": { "properties": { "filesUploadedTotalCount": { diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 29be6d826c1bc4..ee8af9e040401a 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -53,6 +53,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/reporting_api_integration/config.js'), require.resolve('../test/functional_embedded/config.ts'), require.resolve('../test/ingest_manager_api_integration/config.ts'), + require.resolve('../test/functional_enterprise_search/without_host_configured.config.ts'), ]; require('@kbn/plugin-helpers').babelRegister(); diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 11fb9b2de71991..df6eca795f8019 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -97,6 +97,7 @@ export default function ({ getService }: FtrProviderContext) { 'visualize', 'dashboard', 'dev_tools', + 'enterpriseSearch', 'advancedSettings', 'indexPatterns', 'timelion', diff --git a/x-pack/test/functional_enterprise_search/README.md b/x-pack/test/functional_enterprise_search/README.md new file mode 100644 index 00000000000000..63d13cbac7020c --- /dev/null +++ b/x-pack/test/functional_enterprise_search/README.md @@ -0,0 +1,41 @@ +# Enterprise Search Functional E2E Tests + +## Running these tests + +Follow the [Functional Test Runner instructions](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html#_running_functional_tests). + +There are two suites available to run, a suite that requires a Kibana instance without an `enterpriseSearch.host` +configured, and one that does. The later also [requires a running Enterprise Search instance](#enterprise-search-requirement), and a Private API key +from that instance set in an Environment variable. + +Ex. + +```sh +# Run specs from the x-pack directory +cd x-pack + +# Run tests that do not require enterpriseSearch.host variable +node scripts/functional_tests --config test/functional_enterprise_search/without_host_configured.config.ts + +# Run tests that require enterpriseSearch.host variable +APP_SEARCH_API_KEY=[use private key from local App Search instance here] node scripts/functional_tests --config test/functional_enterprise_search/with_host_configured.config.ts +``` + +## Enterprise Search Requirement + +The `with_host_configured` tests will not currently start an instance of App Search automatically. As such, they are not run as part of CI and are most useful for local regression testing. + +The easiest way to start Enterprise Search for these tests is to check out the `ent-search` project +and use the following script. + +```sh +cd script/stack_scripts +/start-with-license-and-expiration.sh platinum 500000 +``` + +Requirements for Enterprise Search: + +- Running on port 3002 against a separate Elasticsearch cluster. +- Elasticsearch must have a platinum or greater level license (or trial). +- Must have Standard or Native Auth configured with an `enterprise_search` user with password `changeme`. +- There should be NO existing Engines or Meta Engines. diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts new file mode 100644 index 00000000000000..e4ebd61c0692a2 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/app_search/engines.ts @@ -0,0 +1,75 @@ +/* + * 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 expect from '@kbn/expect'; +import { EsArchiver } from 'src/es_archiver'; +import { AppSearchService, IEngine } from '../../../../services/app_search_service'; +import { Browser } from '../../../../../../../test/functional/services/common'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function enterpriseSearchSetupEnginesTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const esArchiver = getService('esArchiver') as EsArchiver; + const browser = getService('browser') as Browser; + const retry = getService('retry'); + const appSearch = getService('appSearch') as AppSearchService; + + const PageObjects = getPageObjects(['appSearch', 'security']); + + describe('Engines Overview', function () { + let engine1: IEngine; + let engine2: IEngine; + let metaEngine: IEngine; + + before(async () => { + await esArchiver.load('empty_kibana'); + engine1 = await appSearch.createEngine(); + engine2 = await appSearch.createEngine(); + metaEngine = await appSearch.createMetaEngine([engine1.name, engine2.name]); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + appSearch.destroyEngine(engine1.name); + appSearch.destroyEngine(engine2.name); + appSearch.destroyEngine(metaEngine.name); + }); + + describe('when an enterpriseSearch.host is configured', () => { + it('navigating to the enterprise_search plugin will redirect a user to the App Search Engines Overview page', async () => { + await PageObjects.security.forceLogout(); + const { user, password } = appSearch.getEnterpriseSearchUser(); + await PageObjects.security.login(user, password, { + expectSpaceSelector: false, + }); + + await PageObjects.appSearch.navigateToPage(); + await retry.try(async function () { + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).to.contain('/app_search'); + }); + }); + + it('lists engines', async () => { + const engineLinks = await PageObjects.appSearch.getEngineLinks(); + const engineLinksText = await Promise.all(engineLinks.map((l) => l.getVisibleText())); + + expect(engineLinksText.includes(engine1.name)).to.equal(true); + expect(engineLinksText.includes(engine2.name)).to.equal(true); + }); + + it('lists meta engines', async () => { + const metaEngineLinks = await PageObjects.appSearch.getMetaEngineLinks(); + const metaEngineLinksText = await Promise.all( + metaEngineLinks.map((l) => l.getVisibleText()) + ); + expect(metaEngineLinksText.includes(metaEngine.name)).to.equal(true); + }); + }); + }); +} diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/index.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/index.ts new file mode 100644 index 00000000000000..ac4984e0db0190 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/with_host_configured/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Enterprise Search', function () { + loadTestFile(require.resolve('./app_search/engines')); + }); +} diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts new file mode 100644 index 00000000000000..1d478c6baf29cb --- /dev/null +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/app_search/setup_guide.ts @@ -0,0 +1,36 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function enterpriseSearchSetupGuideTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const retry = getService('retry'); + + const PageObjects = getPageObjects(['appSearch']); + + describe('Setup Guide', function () { + before(async () => await esArchiver.load('empty_kibana')); + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('when no enterpriseSearch.host is configured', () => { + it('navigating to the enterprise_search plugin will redirect a user to the setup guide', async () => { + await PageObjects.appSearch.navigateToPage(); + await retry.try(async function () { + const currentUrl = await browser.getCurrentUrl(); + expect(currentUrl).to.contain('/app_search/setup_guide'); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts new file mode 100644 index 00000000000000..31a92e752fcf4e --- /dev/null +++ b/x-pack/test/functional_enterprise_search/apps/enterprise_search/without_host_configured/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Enterprise Search', function () { + this.tags('ciGroup10'); + + loadTestFile(require.resolve('./app_search/setup_guide')); + }); +} diff --git a/x-pack/test/functional_enterprise_search/base_config.ts b/x-pack/test/functional_enterprise_search/base_config.ts new file mode 100644 index 00000000000000..f737b6cd4b5f45 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/base_config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackFunctionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + // default to the xpack functional config + ...xPackFunctionalConfig.getAll(), + services, + pageObjects, + }; +} diff --git a/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts b/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..bb257cdcbfe1b5 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/ftr_provider_context.d.ts @@ -0,0 +1,12 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { pageObjects } from './page_objects'; +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional_enterprise_search/page_objects/app_search.ts b/x-pack/test/functional_enterprise_search/page_objects/app_search.ts new file mode 100644 index 00000000000000..d845a1935a1496 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/page_objects/app_search.ts @@ -0,0 +1,30 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; +import { TestSubjects } from '../../../../test/functional/services/common'; +import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; + +export function AppSearchPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects') as TestSubjects; + + return { + async navigateToPage(): Promise { + return await PageObjects.common.navigateToApp('enterprise_search/app_search'); + }, + + async getEngineLinks(): Promise { + const engines = await testSubjects.find('appSearchEngines'); + return await testSubjects.findAllDescendant('engineNameLink', engines); + }, + + async getMetaEngineLinks(): Promise { + const metaEngines = await testSubjects.find('appSearchMetaEngines'); + return await testSubjects.findAllDescendant('engineNameLink', metaEngines); + }, + }; +} diff --git a/x-pack/test/functional_enterprise_search/page_objects/index.ts b/x-pack/test/functional_enterprise_search/page_objects/index.ts new file mode 100644 index 00000000000000..009fb264824195 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/page_objects/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { pageObjects as basePageObjects } from '../../functional/page_objects'; +import { AppSearchPageProvider } from './app_search'; + +export const pageObjects = { + ...basePageObjects, + appSearch: AppSearchPageProvider, +}; diff --git a/x-pack/test/functional_enterprise_search/services/app_search_client.ts b/x-pack/test/functional_enterprise_search/services/app_search_client.ts new file mode 100644 index 00000000000000..fbd15b83f97ea7 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/services/app_search_client.ts @@ -0,0 +1,121 @@ +/* + * 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 http from 'http'; + +/** + * A simple request client for making API calls to the App Search API + */ +const makeRequest = (method: string, path: string, body?: object): Promise => { + return new Promise(function (resolve, reject) { + const APP_SEARCH_API_KEY = process.env.APP_SEARCH_API_KEY; + + if (!APP_SEARCH_API_KEY) { + throw new Error('Please provide a valid APP_SEARCH_API_KEY. See README for more details.'); + } + + let postData; + + if (body) { + postData = JSON.stringify(body); + } + + const req = http.request( + { + method, + hostname: 'localhost', + port: 3002, + path, + agent: false, // Create a new agent just for this one request + headers: { + Authorization: `Bearer ${APP_SEARCH_API_KEY}`, + 'Content-Type': 'application/json', + ...(!!postData && { 'Content-Length': Buffer.byteLength(postData) }), + }, + }, + (res) => { + const bodyChunks: Uint8Array[] = []; + res.on('data', function (chunk) { + bodyChunks.push(chunk); + }); + + res.on('end', function () { + let responseBody; + try { + responseBody = JSON.parse(Buffer.concat(bodyChunks).toString()); + } catch (e) { + reject(e); + } + + if (res.statusCode && res.statusCode > 299) { + reject('Error calling App Search API: ' + JSON.stringify(responseBody)); + } + + resolve(responseBody); + }); + } + ); + + req.on('error', (e) => { + reject(e); + }); + + if (postData) { + req.write(postData); + } + req.end(); + }); +}; + +export interface IEngine { + name: string; +} + +export const createEngine = async (engineName: string): Promise => { + return await makeRequest('POST', '/api/as/v1/engines', { name: engineName }); +}; + +export const destroyEngine = async (engineName: string): Promise => { + return await makeRequest('DELETE', `/api/as/v1/engines/${engineName}`); +}; + +export const createMetaEngine = async ( + engineName: string, + sourceEngines: string[] +): Promise => { + return await makeRequest('POST', '/api/as/v1/engines', { + name: engineName, + type: 'meta', + source_engines: sourceEngines, + }); +}; + +export interface ISearchResponse { + results: object[]; +} + +const search = async (engineName: string): Promise => { + return await makeRequest('POST', `/api/as/v1/engines/${engineName}/search`, { query: '' }); +}; + +// Since the App Search API does not issue document receipts, the only way to tell whether or not documents +// are fully indexed is to poll the search endpoint. +export const waitForIndexedDocs = (engineName: string) => { + return new Promise(async function (resolve) { + let isReady = false; + while (!isReady) { + const response = await search(engineName); + if (response.results && response.results.length > 0) { + isReady = true; + resolve(); + } + } + }); +}; + +export const indexData = async (engineName: string, docs: object[]) => { + return await makeRequest('POST', `/api/as/v1/engines/${engineName}/documents`, docs); +}; diff --git a/x-pack/test/functional_enterprise_search/services/app_search_service.ts b/x-pack/test/functional_enterprise_search/services/app_search_service.ts new file mode 100644 index 00000000000000..9a43783402f4b3 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/services/app_search_service.ts @@ -0,0 +1,77 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +const ENTERPRISE_SEARCH_USER = 'enterprise_search'; +const ENTERPRISE_SEARCH_PASSWORD = 'changeme'; +import { + createEngine, + createMetaEngine, + indexData, + waitForIndexedDocs, + destroyEngine, + IEngine, +} from './app_search_client'; + +export interface IUser { + user: string; + password: string; +} +export { IEngine }; + +export class AppSearchService { + getEnterpriseSearchUser(): IUser { + return { + user: ENTERPRISE_SEARCH_USER, + password: ENTERPRISE_SEARCH_PASSWORD, + }; + } + + createEngine(): Promise { + const engineName = `test-engine-${new Date().getTime()}`; + return createEngine(engineName); + } + + async createEngineWithDocs(): Promise { + const engine = await this.createEngine(); + const docs = [ + { id: 1, name: 'doc1' }, + { id: 2, name: 'doc2' }, + { id: 3, name: 'doc2' }, + ]; + await indexData(engine.name, docs); + await waitForIndexedDocs(engine.name); + return engine; + } + + createMetaEngine(sourceEngines: string[]): Promise { + const engineName = `test-meta-engine-${new Date().getTime()}`; + return createMetaEngine(engineName, sourceEngines); + } + + destroyEngine(engineName: string) { + return destroyEngine(engineName); + } +} + +export async function AppSearchServiceProvider({ getService }: FtrProviderContext) { + const lifecycle = getService('lifecycle'); + const security = getService('security'); + + lifecycle.beforeTests.add(async () => { + // The App Search plugin passes through the current user name and password + // through on the API call to App Search. Therefore, we need to be signed + // in as the enterprise_search user in order for this plugin to work. + await security.user.create(ENTERPRISE_SEARCH_USER, { + password: ENTERPRISE_SEARCH_PASSWORD, + roles: ['kibana_admin'], + full_name: ENTERPRISE_SEARCH_USER, + }); + }); + + return new AppSearchService(); +} diff --git a/x-pack/test/functional_enterprise_search/services/index.ts b/x-pack/test/functional_enterprise_search/services/index.ts new file mode 100644 index 00000000000000..1715c98677ac6f --- /dev/null +++ b/x-pack/test/functional_enterprise_search/services/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { services as functionalServices } from '../../functional/services'; +import { AppSearchServiceProvider } from './app_search_service'; + +export const services = { + ...functionalServices, + appSearch: AppSearchServiceProvider, +}; diff --git a/x-pack/test/functional_enterprise_search/with_host_configured.config.ts b/x-pack/test/functional_enterprise_search/with_host_configured.config.ts new file mode 100644 index 00000000000000..f425f806f4bcd5 --- /dev/null +++ b/x-pack/test/functional_enterprise_search/with_host_configured.config.ts @@ -0,0 +1,31 @@ +/* + * 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 { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('./base_config')); + + return { + // default to the xpack functional config + ...baseConfig.getAll(), + + testFiles: [resolve(__dirname, './apps/enterprise_search/with_host_configured')], + + junit: { + reportName: 'X-Pack Enterprise Search Functional Tests with Host Configured', + }, + + kbnTestServer: { + ...baseConfig.get('kbnTestServer'), + serverArgs: [ + ...baseConfig.get('kbnTestServer.serverArgs'), + '--enterpriseSearch.host=http://localhost:3002', + ], + }, + }; +} diff --git a/x-pack/test/functional_enterprise_search/without_host_configured.config.ts b/x-pack/test/functional_enterprise_search/without_host_configured.config.ts new file mode 100644 index 00000000000000..0f2afd214abedf --- /dev/null +++ b/x-pack/test/functional_enterprise_search/without_host_configured.config.ts @@ -0,0 +1,23 @@ +/* + * 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 { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const baseConfig = await readConfigFile(require.resolve('./base_config')); + + return { + // default to the xpack functional config + ...baseConfig.getAll(), + + testFiles: [resolve(__dirname, './apps/enterprise_search/without_host_configured')], + + junit: { + reportName: 'X-Pack Enterprise Search Functional Tests without Host Configured', + }, + }; +} diff --git a/x-pack/test/ui_capabilities/common/nav_links_builder.ts b/x-pack/test/ui_capabilities/common/nav_links_builder.ts index 405ef4dbdc5b1d..b20a499ba7e20d 100644 --- a/x-pack/test/ui_capabilities/common/nav_links_builder.ts +++ b/x-pack/test/ui_capabilities/common/nav_links_builder.ts @@ -15,6 +15,10 @@ export class NavLinksBuilder { management: { navLinkId: 'kibana:stack_management', }, + // TODO: Temp until navLinkIds fix is merged in + appSearch: { + navLinkId: 'appSearch', + }, }; } diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index f8f3f2be2b2ec1..0e0d46c6ce2cd2 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -32,17 +32,27 @@ export default function catalogueTests({ getService }: FtrProviderContext) { break; } case 'global_all at everything_space': - case 'dual_privileges_all at everything_space': + case 'dual_privileges_all at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } case 'everything_space_all at everything_space': case 'global_read at everything_space': case 'dual_privileges_read at everything_space': case 'everything_space_read at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring is enabled + // everything except ml and monitoring and enterprise search is enabled const expected = mapValues( uiCapabilities.value!.catalogue, - (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + (enabled, catalogueId) => !['ml', 'monitoring', 'appSearch'].includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index 10ecf5d25d3469..08a7d789153e77 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -38,14 +38,20 @@ export default function navLinksTests({ getService }: FtrProviderContext) { break; case 'global_all at everything_space': case 'dual_privileges_all at everything_space': - case 'dual_privileges_read at everything_space': - case 'global_read at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); + break; case 'everything_space_all at everything_space': + case 'global_read at everything_space': + case 'dual_privileges_read at everything_space': case 'everything_space_read at everything_space': expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'enterpriseSearch', 'appSearch') ); break; case 'superuser at nothing_space': diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index 52a1f30147b4ff..99f91407dc1d2b 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -32,9 +32,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { break; } case 'all': - case 'read': - case 'dual_privileges_all': - case 'dual_privileges_read': { + case 'dual_privileges_all': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); // everything except ml and monitoring is enabled @@ -45,6 +43,18 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } + case 'read': + case 'dual_privileges_read': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except ml and monitoring and enterprise search is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => !['ml', 'monitoring', 'appSearch'].includes(catalogueId) + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } case 'foo_all': case 'foo_read': { expect(uiCapabilities.success).to.be(true); diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index fe9ffa9286de83..d3bd2e1afd357c 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -37,15 +37,21 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); break; case 'all': - case 'read': case 'dual_privileges_all': - case 'dual_privileges_read': expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( navLinksBuilder.except('ml', 'monitoring') ); break; + case 'read': + case 'dual_privileges_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring', 'appSearch') + ); + break; case 'foo_all': case 'foo_read': expect(uiCapabilities.success).to.be(true); From 633968e0536ab899ddd1c7837ee934a51ebeea58 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Thu, 9 Jul 2020 22:12:52 +0200 Subject: [PATCH 12/15] Remove IE support in functional tests (#71285) * [ftr] remove ie support * remove ie integration tests config --- .../lib/config/schema.ts | 2 +- test/functional/apps/home/_navigation.ts | 10 -- test/functional/config.ie.js | 52 ---------- test/functional/page_objects/time_picker.ts | 7 -- test/functional/services/common/browser.ts | 97 +++++-------------- .../web_element_wrapper.ts | 55 +++-------- test/functional/services/remote/browsers.ts | 1 - test/functional/services/remote/remote.ts | 9 +- test/functional/services/remote/webdriver.ts | 36 +------ x-pack/test/functional/config.ie.js | 74 -------------- ...ig.stack_functional_integration_base_ie.js | 18 ---- 11 files changed, 39 insertions(+), 322 deletions(-) delete mode 100644 test/functional/config.ie.js delete mode 100644 x-pack/test/functional/config.ie.js delete mode 100644 x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 6cbdc5ec7fc208..e1d3bf1a8d9016 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -148,7 +148,7 @@ export const schema = Joi.object() browser: Joi.object() .keys({ - type: Joi.string().valid('chrome', 'firefox', 'ie', 'msedge').default('chrome'), + type: Joi.string().valid('chrome', 'firefox', 'msedge').default('chrome'), logPollingMs: Joi.number().default(100), acceptInsecureCerts: Joi.boolean().default(false), diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index cfe4f9cc3e014b..b8fa5b184cd1f4 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -26,21 +26,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']); const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); describe('Kibana browser back navigation should work', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('discover'); await esArchiver.loadIfNeeded('logstash_functional'); - if (browser.isInternetExplorer) { - await kibanaServer.uiSettings.replace({ 'state:storeInSessionStorage': false }); - } - }); - - after(async () => { - if (browser.isInternetExplorer) { - await kibanaServer.uiSettings.replace({ 'state:storeInSessionStorage': true }); - } }); it('detect navigate back issues', async () => { diff --git a/test/functional/config.ie.js b/test/functional/config.ie.js deleted file mode 100644 index bc47ce707003eb..00000000000000 --- a/test/functional/config.ie.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); - - return { - ...defaultConfig.getAll(), - - browser: { - type: 'ie', - }, - - junit: { - reportName: 'Internet Explorer UI Functional Tests', - }, - - uiSettings: { - defaults: { - 'accessibility:disableAnimations': true, - 'dateFormat:tz': 'UTC', - 'state:storeInSessionStorage': true, - 'notifications:lifetime:info': 10000, - }, - }, - - kbnTestServer: { - ...defaultConfig.get('kbnTestServer'), - serverArgs: [ - ...defaultConfig.get('kbnTestServer.serverArgs'), - '--csp.strict=false', - '--telemetry.optIn=false', - ], - }, - }; -} diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index 7ef291c8c7005f..8a726cee444c16 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -98,13 +98,6 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const input = await testSubjects.find(dataTestSubj); await input.clearValue(); await input.type(value); - } else if (browser.isInternetExplorer) { - const input = await testSubjects.find(dataTestSubj); - const currentValue = await input.getAttribute('value'); - await input.type(browser.keys.ARROW_RIGHT.repeat(currentValue.length)); - await input.type(browser.keys.BACK_SPACE.repeat(currentValue.length)); - await input.type(value); - await input.click(); } else { await testSubjects.setValue(dataTestSubj, value); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 2d35551b04808f..c38ac771e41625 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -34,8 +34,6 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { const log = getService('log'); const { driver, browserType } = await getService('__webdriver__').init(); - const isW3CEnabled = (driver as any).executor_.w3c === true; - return new (class BrowserService { /** * Keyboard events @@ -53,19 +51,12 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { public readonly isFirefox: boolean = browserType === Browsers.Firefox; - public readonly isInternetExplorer: boolean = browserType === Browsers.InternetExplorer; - - /** - * Is WebDriver instance W3C compatible - */ - isW3CEnabled = isW3CEnabled; - /** * Returns instance of Actions API based on driver w3c flag * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#actions */ public getActions() { - return this.isW3CEnabled ? driver.actions() : driver.actions({ bridge: true }); + return driver.actions(); } /** @@ -164,12 +155,7 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { */ public async getCurrentUrl() { // strip _t=Date query param when url is read - let current: string; - if (this.isInternetExplorer) { - current = await driver.executeScript('return window.document.location.href'); - } else { - current = await driver.getCurrentUrl(); - } + const current = await driver.getCurrentUrl(); const currentWithoutTime = modifyUrl(current, (parsed) => { delete (parsed.query as any)._t; return void 0; @@ -214,15 +200,8 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { * @return {Promise} */ public async moveMouseTo(point: { x: number; y: number }): Promise { - if (this.isW3CEnabled) { - await this.getActions().move({ x: 0, y: 0 }).perform(); - await this.getActions().move({ x: point.x, y: point.y, origin: Origin.POINTER }).perform(); - } else { - await this.getActions() - .pause(this.getActions().mouse) - .move({ x: point.x, y: point.y, origin: Origin.POINTER }) - .perform(); - } + await this.getActions().move({ x: 0, y: 0 }).perform(); + await this.getActions().move({ x: point.x, y: point.y, origin: Origin.POINTER }).perform(); } /** @@ -237,44 +216,20 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { from: { offset?: { x: any; y: any }; location: any }, to: { offset?: { x: any; y: any }; location: any } ) { - if (this.isW3CEnabled) { - // The offset should be specified in pixels relative to the center of the element's bounding box - const getW3CPoint = (data: any) => { - if (!data.offset) { - data.offset = {}; - } - return data.location instanceof WebElementWrapper - ? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement } - : { x: data.location.x, y: data.location.y, origin: Origin.POINTER }; - }; - - const startPoint = getW3CPoint(from); - const endPoint = getW3CPoint(to); - await this.getActions().move({ x: 0, y: 0 }).perform(); - return await this.getActions().move(startPoint).press().move(endPoint).release().perform(); - } else { - // The offset should be specified in pixels relative to the top-left corner of the element's bounding box - const getOffset: any = (offset: { x: number; y: number }) => - offset ? { x: offset.x || 0, y: offset.y || 0 } : { x: 0, y: 0 }; - - if (from.location instanceof WebElementWrapper === false) { - throw new Error('Dragging point should be WebElementWrapper instance'); - } else if (typeof to.location.x === 'number') { - return await this.getActions() - .move({ origin: from.location._webElement }) - .press() - .move({ x: to.location.x, y: to.location.y, origin: Origin.POINTER }) - .release() - .perform(); - } else { - return await new LegacyActionSequence(driver) - .mouseMove(from.location._webElement, getOffset(from.offset)) - .mouseDown() - .mouseMove(to.location._webElement, getOffset(to.offset)) - .mouseUp() - .perform(); + // The offset should be specified in pixels relative to the center of the element's bounding box + const getW3CPoint = (data: any) => { + if (!data.offset) { + data.offset = {}; } - } + return data.location instanceof WebElementWrapper + ? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement } + : { x: data.location.x, y: data.location.y, origin: Origin.POINTER }; + }; + + const startPoint = getW3CPoint(from); + const endPoint = getW3CPoint(to); + await this.getActions().move({ x: 0, y: 0 }).perform(); + return await this.getActions().move(startPoint).press().move(endPoint).release().perform(); } /** @@ -341,19 +296,11 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { * @return {Promise} */ public async clickMouseButton(point: { x: number; y: number }) { - if (this.isW3CEnabled) { - await this.getActions().move({ x: 0, y: 0 }).perform(); - await this.getActions() - .move({ x: point.x, y: point.y, origin: Origin.POINTER }) - .click() - .perform(); - } else { - await this.getActions() - .pause(this.getActions().mouse) - .move({ x: point.x, y: point.y, origin: Origin.POINTER }) - .click() - .perform(); - } + await this.getActions().move({ x: 0, y: 0 }).perform(); + await this.getActions() + .move({ x: point.x, y: point.y, origin: Origin.POINTER }) + .click() + .perform(); } /** diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 281a412653bd0a..5011235551bd82 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -47,7 +47,6 @@ const RETRY_CLICK_RETRY_ON_ERRORS = [ export class WebElementWrapper { private By = By; private Keys = Key; - public isW3CEnabled: boolean = (this.driver as any).executor_.w3c === true; public isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes(this.browserType); public static create( @@ -141,7 +140,7 @@ export class WebElementWrapper { } private getActions() { - return this.isW3CEnabled ? this.driver.actions() : this.driver.actions({ bridge: true }); + return this.driver.actions(); } /** @@ -233,9 +232,6 @@ export class WebElementWrapper { * @default { withJS: false } */ async clearValue(options: ClearOptions = { withJS: false }) { - if (this.browserType === Browsers.InternetExplorer) { - return this.clearValueWithKeyboard(); - } await this.retryCall(async function clearValue(wrapper) { if (wrapper.isChromium || options.withJS) { // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2702 @@ -252,16 +248,6 @@ export class WebElementWrapper { * @default { charByChar: false } */ async clearValueWithKeyboard(options: TypeOptions = { charByChar: false }) { - if (this.browserType === Browsers.InternetExplorer) { - const value = await this.getAttribute('value'); - // For IE testing, the text field gets clicked in the middle so - // first go HOME and then DELETE all chars - await this.pressKeys(this.Keys.HOME); - for (let i = 0; i <= value.length; i++) { - await this.pressKeys(this.Keys.DELETE); - } - return; - } if (options.charByChar === true) { const value = await this.getAttribute('value'); for (let i = 0; i <= value.length; i++) { @@ -429,19 +415,11 @@ export class WebElementWrapper { public async moveMouseTo(options = { xOffset: 0, yOffset: 0 }) { await this.retryCall(async function moveMouseTo(wrapper) { await wrapper.scrollIntoViewIfNecessary(); - if (wrapper.isW3CEnabled) { - await wrapper.getActions().move({ x: 0, y: 0 }).perform(); - await wrapper - .getActions() - .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) - .perform(); - } else { - await wrapper - .getActions() - .pause(wrapper.getActions().mouse) - .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) - .perform(); - } + await wrapper.getActions().move({ x: 0, y: 0 }).perform(); + await wrapper + .getActions() + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) + .perform(); }); } @@ -456,21 +434,12 @@ export class WebElementWrapper { public async clickMouseButton(options = { xOffset: 0, yOffset: 0 }) { await this.retryCall(async function clickMouseButton(wrapper) { await wrapper.scrollIntoViewIfNecessary(); - if (wrapper.isW3CEnabled) { - await wrapper.getActions().move({ x: 0, y: 0 }).perform(); - await wrapper - .getActions() - .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) - .click() - .perform(); - } else { - await wrapper - .getActions() - .pause(wrapper.getActions().mouse) - .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) - .click() - .perform(); - } + await wrapper.getActions().move({ x: 0, y: 0 }).perform(); + await wrapper + .getActions() + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) + .click() + .perform(); }); } diff --git a/test/functional/services/remote/browsers.ts b/test/functional/services/remote/browsers.ts index aa6e364d0a09d0..f7942e708a3bb1 100644 --- a/test/functional/services/remote/browsers.ts +++ b/test/functional/services/remote/browsers.ts @@ -20,6 +20,5 @@ export enum Browsers { Chrome = 'chrome', Firefox = 'firefox', - InternetExplorer = 'ie', ChromiumEdge = 'msedge', } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index 99643929c4682c..a45403e31095c7 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -64,15 +64,12 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { }; const { driver, consoleLog$ } = await initWebDriver(log, browserType, lifecycle, browserConfig); - const isW3CEnabled = (driver as any).executor_.w3c; - const caps = await driver.getCapabilities(); - const browserVersion = caps.get(isW3CEnabled ? 'browserVersion' : 'version'); log.info( - `Remote initialized: ${caps.get( - 'browserName' - )} ${browserVersion}, w3c compliance=${isW3CEnabled}, collectingCoverage=${collectCoverage}` + `Remote initialized: ${caps.get('browserName')} ${caps.get( + 'browserVersion' + )}, collectingCoverage=${collectCoverage}` ); if ([Browsers.Chrome, Browsers.ChromiumEdge].includes(browserType)) { diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 78f659a064a0c0..c5613b9e27094a 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -17,7 +17,7 @@ * under the License. */ -import { delimiter, resolve } from 'path'; +import { resolve } from 'path'; import Fs from 'fs'; import * as Rx from 'rxjs'; @@ -279,40 +279,6 @@ async function attemptToCreateCommand( }; } - case 'ie': { - // https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/ie_exports_Options.html - const driverPath = require.resolve('iedriver/lib/iedriver'); - process.env.PATH = driverPath + delimiter + process.env.PATH; - - const ieCapabilities = Capabilities.ie(); - ieCapabilities.set('se:ieOptions', { - 'ie.ensureCleanSession': true, - ignoreProtectedModeSettings: true, - ignoreZoomSetting: false, // requires us to have 100% zoom level - nativeEvents: true, // need this for values to stick but it requires 100% scaling and window focus - requireWindowFocus: true, - logLevel: 'TRACE', - }); - - let session; - if (remoteSessionUrl) { - session = await new Builder() - .forBrowser(browserType) - .withCapabilities(ieCapabilities) - .usingServer(remoteSessionUrl) - .build(); - } else { - session = await new Builder() - .forBrowser(browserType) - .withCapabilities(ieCapabilities) - .build(); - } - return { - session, - consoleLog$: Rx.EMPTY, - }; - } - default: throw new Error(`${browserType} is not supported yet`); } diff --git a/x-pack/test/functional/config.ie.js b/x-pack/test/functional/config.ie.js deleted file mode 100644 index 1289bb723cfec1..00000000000000 --- a/x-pack/test/functional/config.ie.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 default async function ({ readConfigFile }) { - const defaultConfig = await readConfigFile(require.resolve('./config')); - - return { - ...defaultConfig.getAll(), - //csp.strict: false - // testFiles: [ - // require.resolve(__dirname, './apps/advanced_settings'), - // require.resolve(__dirname, './apps/canvas'), - // require.resolve(__dirname, './apps/graph'), - // require.resolve(__dirname, './apps/monitoring'), - // require.resolve(__dirname, './apps/watcher'), - // require.resolve(__dirname, './apps/dashboard'), - // require.resolve(__dirname, './apps/dashboard_mode'), - // require.resolve(__dirname, './apps/discover'), - // require.resolve(__dirname, './apps/security'), - // require.resolve(__dirname, './apps/spaces'), - // require.resolve(__dirname, './apps/lens'), - // require.resolve(__dirname, './apps/logstash'), - // require.resolve(__dirname, './apps/grok_debugger'), - // require.resolve(__dirname, './apps/infra'), - // require.resolve(__dirname, './apps/ml'), - // require.resolve(__dirname, './apps/rollup_job'), - // require.resolve(__dirname, './apps/maps'), - // require.resolve(__dirname, './apps/status_page'), - // require.resolve(__dirname, './apps/timelion'), - // require.resolve(__dirname, './apps/upgrade_assistant'), - // require.resolve(__dirname, './apps/visualize'), - // require.resolve(__dirname, './apps/uptime'), - // require.resolve(__dirname, './apps/saved_objects_management'), - // require.resolve(__dirname, './apps/dev_tools'), - // require.resolve(__dirname, './apps/apm'), - // require.resolve(__dirname, './apps/index_patterns'), - // require.resolve(__dirname, './apps/index_management'), - // require.resolve(__dirname, './apps/index_lifecycle_management'), - // require.resolve(__dirname, './apps/snapshot_restore'), - // require.resolve(__dirname, './apps/cross_cluster_replication'), - // require.resolve(__dirname, './apps/remote_clusters'), - // // This license_management file must be last because it is destructive. - // require.resolve(__dirname, './apps/license_management'), - // ], - - browser: { - type: 'ie', - }, - - junit: { - reportName: 'Internet Explorer UI Functional X-Pack Tests', - }, - - uiSettings: { - defaults: { - 'accessibility:disableAnimations': true, - 'dateFormat:tz': 'UTC', - 'state:storeInSessionStorage': true, - }, - }, - - kbnTestServer: { - ...defaultConfig.get('kbnTestServer'), - serverArgs: [ - ...defaultConfig.get('kbnTestServer.serverArgs'), - '--csp.strict=false', - '--telemetry.optIn=false', - ], - }, - }; -} diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js deleted file mode 100644 index 933a59e4e25b90..00000000000000 --- a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 default async ({ readConfigFile }) => { - const baseConfigs = await readConfigFile( - require.resolve('./config.stack_functional_integration_base.js') - ); - return { - ...baseConfigs.getAll(), - browser: { - type: 'ie', - }, - security: { disableTestUser: true }, - }; -}; From 5e9f333fca3ef45fd4c0fa6f1624ab5f4117876b Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 9 Jul 2020 13:19:52 -0700 Subject: [PATCH 13/15] [DOCS] Clarify trial subscription levels (#70636) --- docs/management/images/management-license.png | Bin 215829 -> 138156 bytes docs/management/managing-licenses.asciidoc | 21 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/management/images/management-license.png b/docs/management/images/management-license.png index 3347aec8632e4fa332caf49d9662be4ddf82f0bf..8df9402939b2e933b4e6a61e245c1d02bcc2d3a2 100644 GIT binary patch literal 138156 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

^k_6I?;`hKlT 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�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%0Ak`o>O~G!mnEpidf-!oIcm04&Mx{ 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�B0KZ;)7|iYtwUUlf8Iu-A=z>I@p}`BIpp z4qKaQAw0{X)J|H@07?Hm*JT>=FKwlNcw9$O()jf92-uKyw19XuqT7)yclLEgJef*I-RIM(BS zrH_XTQ5Dc=CsoE;=2Z}gh?ZmwAN72K!i28zw2Z|5bCit`m9`LDB+}~ zA>#+7M69>BbC$B|GKo=v%q=FL2m7Nq3scO_tr z_mOzShSy`N_T@7i+FWkj<&rxZXs5{WcL9{^v3r-TW-@mPY;F@zUAW+PF)KX24&MNN zj#-x%&JV_F6j-O|b>})gudShn25BV0Ha!hhypADpVJ?_CBHh)yD2%I(tH6H?qXVtk zu2(!9Y~+v-zB(duI7AP+YN4gEwI{QP2M%aS5W3>m_&N8Ls=;>GAK9lm#|SJqWSVB| zD2dJHFJWOM#LSxBS4}5979u^vQSEM&TK&jauX<<5#i zB$U;vtOGmmN>FD@dy*q4TiuaK!3VMa6haaY%0ln0HFFcS{)t3MxB*Yu0Q+)k+A1C) zEopAXI1-XR^hCR_4iaZVYDPw8e`pW2UKsoSwG8^zf8_-1pRA~=PAjSTvk_(VecKEu zVn;54;V}2bZg~{7I+oED3uXDOE~n2k z)P?+~hm&oP-pHZq{d6gbjh4s*g}RBb(j&l{2&$Ql2{-c)R%TO!8TyXxgwF~;9j=MI zlKdWfF?zn)(MgY*hDR4r<#Hq0g?|mDB79@FG317QIp&)wtAw`&HFZ(Bz`wW`usAxN zhW|DQ-yMK=2oS~D>d!~BK}yR7L66&Xs6ddNk$Ce>&%;J$nXmd%n#x~<-WBg4={8w3 zAIbV3>wvyn?7VHotoq#`FPgXZ=H|j9@1Y>>$~Vpyi@%ue;d*b618Ei;NI-k}$ImJh zmr0`4BBQY>T?7kC3I`6Ed-lQzDoH3hEW>=gE!qvkL_!lKD3gTTc%ET|y_d!6Cxzi4 zqmVJO7Phb{B_ci!;6;_e4h|DJ-AtaaH|G{LlSsha4}0|f`jO!i_qK$$YY==(3ZQ5< zjcY5TXqG-D5xr8JlpznfJ0e?kzd$-9`ImjbgNK~FPtw(3GRLajwS{+I79$elxESY4 zS`z%W7r4#BjIBw;RHKhKzw34i`=jb)pwfd{tWqwhH1W308+z;H>aPjfe4>k)z?&p@ zSGQhqd2+lP@W+X~AWL6GIdPVg5|V$q)*a0z2K1kcF`3xlWd;HM{DsE-f}j2%z9Zz) zB61xW4>>%O{I%ioqW%Lm3lfSa2dRb*wImF)R{pz*j8-GuZUPi5t6~Wg5Ec^4*3Lz( zX#3d`3f-d4?ccZ&y$Gs-8b#iNWQPbSUs!npW|`6wqd^7)`+Hl)2bWV1B}pL}<6pmN zbpwt+J%Lx^_T6u2POz|rwD{m6Bnvn@O@L~*PojLZ#0CM<>{7+YXLA}#)O70*p4_KC z6>uaZ&^pMU+eT$ zdrg}^U)syO)v`>S98ocX5l7gDF&Rhk02UX^ZO(;Q*s!3+ZP#9=H=0{Hq%9Q7CP_-MB~hEo?Yheln`}bd*AcUDjDdp~x)%oA3SCZYlVIdbTV)`2no8U4$m}<7`8^juO~fG0S|J50rz@YNsWB zt)5kYh4N@&g)Ek$Jh`N#aJk#Bz=aP72Xi}bWl0h}RTiba+nqBDUT@@xGqyElcc(Ls zQ%^$xNWJZ=XX1HoyfDm=TVrMZ@m+6NNt5=i)&kKgYbQZvYU1g-5};z{X_#i;P2FP~ z#GrXgmAyM{NwqOvh~**q2Wy-V_*Dn2M?#`;k|jA^;8mK}h({%o1<2AJ|wZyE*1+HF*6bnN-ZUYk}Gj#>JAngS7M26>AznxKad*#Tn z6ksxpyU5|NRh)QxoZ#a>^FEkCi4MZ^gnaViNsE)99=(N#VQOYRe|M1km=3XI6KAM@ z+ko;`Ab6u7gTu|2RU~=EA&*O1VxKM6?m1lUNd5iX%#czHKp*e)TtpyCaGEK2gmJjn zM3Kp4#;2>wI>d?$mq$FM7?IV8pv*L^lX@13|7PPR`ibSmGd#eyLkG<-Wkth5$EDlLA-koX^S8vwG+XxykpALo|XIk`2 zGT`WuR^70OR-5 zdqei4ro!x(unrqE7jD$Kcmch9M3^Z)7kI z{S=bPG%*on!6Vc}Js3Cp=`bfO4p`&}=Gj)&7{)wl%36#rQy!J&%&g{n5%?{S2f=Ne zojzb!x?Ev%NoDy~pOMgbQ~bAQUv}_b-&(uhN61mGM5W71Or~p~T?G*OlZqIcVSp&p zz-VDMBUMbb;taOy9PaAi#wDx4P;`|?sOQ0MoTY$<+ z0`;X}-HC_r2+5Ose$#YG4~)p7&ZlrOKJ&~XI1lL+yA8y{C1v~VNBHUN_K>l$cTVRA za_jP>sbg7s9{FiM>OIzTPIr&t{&FR_=5igB--j-UbxPomG^qtZY*?|aHs~>)+=tYK z1;R$0E%jmK8ef|_wYwcz1m1GC{S28S>ii8cobR+;Ltsv3HvQ6PoKjh;B1OOHW~+VK zPU79B4_S^KRyI-EzTaTr);SX)!ZOrTXpt4-tT)5=15OK(3gx1n%r$$_Y0E+Wcy4BZ zN4Ggp%V4%bS6xXYF;nGb{!<%Fp|N$x-;ZpWQiQrZEq;D)G7{Dpv{;kxYg>v=Id*qO)GOH{~JKaPD@;bzst&A7u*4tGuvuGXI|DDa=vum?mdG+gyE|1 zlVti*QB$&!5ZRL$Pc{apNM!%R+CKm3H|mmvVXZ3+)31Zz^!s8D+#1W8PAg)kmT8tkNOS$tgYj46iPA0$r%p9%3^D{ zeN+so3}Bs-Sns&>H~(SoRKbO<#hRb#hn}R#?|MHXZvYe5+a1cY{#uU>n3`Nb5g9<4 zGW;O$+6GcrP3Zpae%SJoAPGSQKtn-UJZ$v`>ZpmLHgukyRa$9zU4w~&A(cGONV!QxF+ zH6?U#1;Uw@9$H8uuwSH;bi2lEX7P1WE;U?L_wBC@UH|bE`NPtb4q6&J@Oiyzk z48sNthSBX@U0^UmFS5~g8z}SAkPPBUO`xYI)3G>RLq9Rzw+WoHEFBWFAN5fr|GIi? z`JlKxH9t^AnD6L2c&+<;Mn<=k9+ld5Ug$uQ`y~|hyiXP9fyTFs z$Z#JJ%~fw6aDSBg{TDduIj7+*ozw1BK8(@1L;Z=x5K&Z@@4hF@$7r*Eju@G2r|y&7A^l50F6L$zt>^QS074BJ#88r1o`=6^0Z*+P+@*H_9zZQz!yKcb6#BzEB#(Y zK%Zx;Xd__qOc=U&BkI$4u)9nKSMP~fv@jgnDkcbZl-ISTCI{<3{S5nZ>M3NgMA+O( znBYGa=BVVe;&JlmF{GS6jxT?7!wahy4gEaD!#gjP;)i!$m6UoqCg$)7no5s9Uzn*> z;_$x1IC=UE4TFBghZf#==l)Qxx%7U@K7;pZd{acFo;JVF?CBHW=j{fSicG}D#pB3P z)+2H=);Zha;TwY_fu@Xt4^=AWfLto|a$)TqikTCF;6+|2IsOcy_U@*zrUHkzZ$Yq^ zGlEAOz%t-gJfT~F<45;k|M7GfO_-0T=D9&b%>eG+cGCJ!i66SZqF5Tcmrqq`Pmzg3 zM-Ict*$0bfO~iztQ7Fwy#^K!*jOA4!ar?K}>FN+wSsvwnCka zIQrfDl2T98$O7YN;X9T49$VcmoQ*kvBPY%wJ9-;l(Xhlf|GMR}NFiM0hpqzOSkysO zLexXS3ZT900{$+PdhE~k?g3aZHv+z{mZ(UJ!*4qdQV@FZxNCJz9ezCSg*b8(K@Vuz%-n#0p;Tn{N<0BM>v}UA2sV$Ni51djKIR!b?)@cV5!K!wARU+|Ts zF`mD7CXKQN%*^tNT>rh^0{?|AVD1=zzb%=F_kTQ!13&zXNwWgrS{93=^w0(9k#zUd zm}1*K%4i51zYl+Z^#{~Zk!0!QjcL=lr*j>JOF8FpI3}LTvQ+#|Ig;HNJFXA%9f%6| zi((xcic~q6yUixWuvXD9#T_qyiW_JJVnCykKa{Ba5>#DEw=i9Ej1QlJzzH;XrR+)1 zDU9n+8)AR@T2kt%>l(ttHw;t5{NQA+iKJ79aWv*MQsa-{z0WK$=FJtbprN`G=QBql z0cFP*;`hy`vGD0oDs+0OzB~noPw;tK7WDq50)XD8FHUMnf}^zA$`HP8=1!Cq)7%fZ=G$FVmi1(}o^eL;D`bGJ=bppvOm!6knY z(O(&SH1dVUv zqxWLl!DJZv&BRkn#?kmg3oc%c()!#gWDumf%4(v#slX?M{AJL17&TWw%{CPFG@v@Y z;Ri{nr>3okF}~r9)9VLkA`7IRIf`R3aY#yxp^Vo8Zm&NGdqeSp5KX#)kywhLNtY&9$Jms((`!vgY8f= zw7{gLHzLA84?0#v(qghp<+=Fzn>9E?rCyiG7|fl*wUGTw{ZbL)PM*Nl180#erf9OvGMd1_PFW(081O zdskSaI`0_X{Q3yA?LBeRvKesJQ-h6HfZVfB$YvB@RheJwejODNGDPBuyRYzHuK()f z#l$DfqamEOt_`MA&N_CqDN2j#$j6SRJgWA}H3fheL!WBL#9OEp$#ItJ-Z~-`s=u-i(nNE+Go)Z6{16579xQ>Bm zp%y$B-i8Nn2}rpm0Q! z?$;y`G9@w*>Sm5ucGvy5i*S4f8lt=|1PA-U{@eHQ`L=i@MgEGTbAvF?O|+jjf)-4J z>gf#3c;GR*-@D5#xtPb(mBNa5-o@saEF4HIRVenTxH@|j`_JS{UY9{j@5l3Z&4GnN zKqQ!`J(fE7WbIy%@lsnu^oba#?X4* z6i+|*04BOvD6Dw=5$NZIYI>fX$;m|lm3SRq7BC{@&m?yvY`U0Io2%U~`X=RwW=nmX6!;i6DHKO4BR1>6`LOHh97-9k;!r?#2&O)Ui*+Y# zaSuIBZ(ragF;Jqo@bC~Awm*rVqsoz(l8oZIiEuGd#o5hY;dpu-Y`xi5kF3OG3hyN9 z3`EETXT1N!dpJ~HPvO!gOd0<$72DmNnW^-wFm>5oSbbACOti&9Iv#-@4tS05xzXqI zDO^cKheEMeos)u+wpPg!?WPAG!L1Xm)#%0_wll~gQ7a;#Le zX<_DV_u;X7r^8&~O&b~C0g z(_c$N`_i3II-z~l-!zzI^O;c(k8$1{5Jd? zd66uO6=u-jX4Uctq6i3%tqUO`fpFRIIX+kyg|g$D@$1PcST)6ApdwGX5#s~Oe?+H}t zkHVZ==&5sSxTMq*?NFK&jqQix347d$DYra;$8VnnYeFdt3Jayd$QA><|K3(49HC*# zlIetylCS>*Do(FF2hVZ*^Fq`1cd6J@hn0;d1&-5TBxgVPxHhD419;$EHd-m{S@gHn zcfKHH0v2e%<^&IBy*lM5vWJWsmNiJW7<;g{ou)UQ(bxqJmy zEeR)YDfgiF^Yg|pgckd58x?PxB2o3XP?&WRo{+{O7qSI}rWNkx+J5uI(a@51T_F@g zOoN@yJNRPHIYe&VPX&V)#!*n$hnyzk+2I&z!M~n+7b)Z!+@~$YKc2e-RuWb} z_gjH>D5S?uLd7LyXOxidlq>ci)5U~Y;V{Z;#Nw3?6N1Z8VGZpCuhHgs?afbcJh=dS z63VF9<5;2b;HgbJ5LMDB8Bs1@`6zCnV$D#RfF{771_och|2{TEr()}_B-}^EUN;vi z_n@@HioZXFnX3#e-8BY66J_{U}8NJ=I?ee)eSb!H)jdl`obQ?O!| zSI=t+W0nn@C@-Uwy{Z+mAmy}uk5%CoOde!(2C`1= zlN5XIK_QqjL0RAtos^H69H@3%I$|F`R4Ca1Y@a4miDnQ z$5t(ZD??WZenwCeQ54o~i*@o~Hd_QQyos{h0O%;>KmIh(=_+|1Yh!az5F3Y@`vPHM z>4d5Lt4XYs6npvtWohCR_(;%o18aVDS9EdnL#+QiKNu_IDWaUR^edM~skN3;c}Bik z#}4yvz881S@srC{$GT(8Xk%!vKLVMF9kjTwxE!g`X=h5151Mqmv0~Lt7;8(k4t5aV zaUQVe6!hvBzoW3=FjC7O$0)nL5>jmwE6l%}A$w?eA-p`%6+pSSqvR=hJgpo@lS>tf zy$U6>S3r=V}%!lzpj>EXQ#2g8H0%+HeOF@)_rLy5a4WlZz#z^YrgCzR$7 zcep!SK!4qC)M{Atv}X&$C2ikx6m^X)un1X3M65gHh$^gkIAw?-V=E zbE()He@*Z7znJ+dm*D@!rck_-lJ|e-FmO$MT7ZHE@5vLPrKf|IX5#)IyvW77o7xb+ z|92_wi9a>yu9kA`a{jp(5CWGtx^r*`Cys8Vp2(%9YlxqG!gy3}x;pfTszOp0CjpX2`ZfY$#hu;V@3N2>@jq!kfH>_TFf?@vkTXqW~+~i6< zkxW>gx_bIxoSO;H+Hk2=(NhHeeLx5|r_bZNw;!QErCX^dFI<;iKdvGRJ+3+_v=Y5R zQ`ZDO(}H-^PDqSI)U{LVBoSTZ&qX1h@S-9x^6WVg(C4~PlXV&wN*k%9(ZS6R--Q52 zxl&JLkK7M8q^=h+Fq0Q=xZZz_clF7+Mpufew85~&f@$GEVY zFh7j(M7nY8`QC;U_T7H>BA6KKqP->+7mK-bA(?sl!weeK+U6NO$iumt3f_F75mF)tXGhGZ)up7=6W7^RrcPd%Z_<^PfQs%RC7`_s`76eMW<|BU z-4$ioxCi60r=P$>_fFxr=8lZdE6hp8siFp$2hGKOw@&I&>WM5W9Dji~-i-&R+Muh7SDz&$1Cm0vGG{OkhKz4jC)fkB4pdy zgx!w$^oW%!^+Xmj)f}%k+`tVa6`80`#@-9^IZe}&aR%?a5AS{bDehYw*rU`Fxm48+ zU_WNGIOICgA)1|S+D@pH99+z-;fX( z{117+^+K+{QAAtp)7V?Fk}zT2N8IL54HaT?^E}qX9!koUe^iIYIRw(tPJM=X3o_#ZB8S<>AlK)qtP4V6$TtFxP zlZY0-S4yE222S^J>{Z;+Bg`piI+oFMff%2-RG5p#D=$`24H{uZMxum#gr$`q?pZzy zwhE=5NT9B5h={q15bQRZ&#xsCK*5z(e?;j8oRXkM!o%fP*LAm+oG%|gnJ}=AV#VE| z&>JC-?{V{;m_1IR)RRh9gEM1zh6e~s>N4`+b<#TC+E|AS@>R8DTqoYX9Jf)K)n}}d zUs!(k9k`lRT!XP{W-pkdQ0hsQpl6K-h;&Xe!|d6i3ZPh9RVbs`}m=z)Rh}9^qt73Tszwcn2;>6=1A{)Y%FT<_VJbMJ0BAb?x zHI~zebQV8Eb!iO>O8fG>Qqk9cuD8H{bqmN~O3&<@7BX%e1p}oC#}Ipg^R50EELt>| zFw%mjkyArKIxZ6SnnMSZ?|XnU5`|Jvao>dd#$}5pLx?jfFYds(k}KKkcR%H?`B&d* zG-l451Sch>9$jOQbs`1@gznYUbjQkjmteF)sV8cs%JtTN(riqnaj_=Bn~$E&l`1Qv z%pr7=41 zP1C>vOYV9Q3(2h2S2fV6fgymp%M<@{uOB{jCdS(LRq81x>0Zo6EVt~=Ecgcn57E)l zEkjb=Y%j;=-;#KuL&NWxm>n@zq0|%i31MtL8aLlbNI*gbUTUV7`iZPQG6F>@J^yqu z+)2{GBM__b%tbRWH^^1Vu$SS8%pGypLqtmB{pzd&WF;}yp`va*gP2d9rOIVm3s0g8d|&J)|+mGH^mBhDfy^Sz)~yM zr~h%!JBS;SP(hxegYb#DRF0H#do$@y@+6ZPA6jYuphdatZ7ZL^G%lf)MU{ksmGes) zI_9`>`e(XUf0*Sg9M@?z{?jVmYzs?V;R1f*ZYy7oegxeO5 z@1xYadU@0(5MYB0d>yK4Y9$0Usdn^0wr~p~kMsyuKX5DDhxOJIIaJ`j_*Ojk&*$;d z(>GH&C0FW+J2Dz*Iy#yYI$bE=TF}%c=c&X!ImuvcD5Kf_l zOI|dI3VuncCo&S&-eeSG$={1{JZmVaKzf`&?cxayz#1t=q{}jDYpNK^p}SL%ue4{( zb{bbb_TU{DC%pAI5tXDQAuBhJ^N0hUeDWSDLkW}4J91KilV3O%&z%8%&LU?x2Cmb< z7Esz|4!n8&%E((l$9^1c9i@X063S6>{s0Q<1L364Zd#Xy&HL)u?`_~mw_X!s4htbi zYV>IqF5_ZpPbepDococAmKhDA{IDS@p0Pt}I565^q@kpF>zDqjPO04&*tHg%h}&D6 zQC?V%!n{Jp>L^A!@zFOOJa@ZuzU07D^a>~|d_N5idMTSFl(o;ypBp?-AfJhB<9 zucK836`3VeUpYa8LUR!VLqki4a)k!u6GAhO1HL$`6iMf!afsevg1R@Roy4=wB1BOw z=M5PZ7h}4E9VAv5hqVDZTB3@qE%$A*w4thm10q&H(^!t-3%-&f9Bx7jGrcO zti|_fZbD&k1(gulC@v{R5*63S2tOv~b77O+UFX5eQNBtzSz?p-x0qI?d$|>rqFq!n zH`g{v%wuO6y%HPvVLFM0Us{55r%yAcky2Sq-;j0$EnHAKI(3mQH0ktkRMB&ykWB+; z_f(^zJQ^hpQ{W`a#ilc~JT|pM#KMKh+gFW@{A3(Ys)ARbE@R&0qd1T3s7e5)U-JUl zTgT@58swZw<^?@0m`P~b-W9ATk*@U^g!Jk;(UP5rV?x%f)q(PSTDF~JtN>-^m&(1G z)JQ3R9XZ6pDt-k!`Q}1g)X*RdityA^CJL%8=?TJc188n4N0Z>e*gY)W{h+128J*>| z*z?BY$cd!#_ojL9@f;0vLlIYo?WtJYyQmB*p(nEFKF#edxOjoq@!g8n?q_k&n0cs3 zIxDZE2PESw=v@xGqdV^(_^-apG{Nk#lZIK?%or zK8mtRkj((wJNKTID%3q*x;olWOvvOS@_n)(Z;Ue;xCW?b=)i5_M8?wDr2I11kFU4D z^%hWSf$lM-bZPKIj$H?jajh*4SJ2n2d~FvuC_GQBkMv2Q^^o8 zjzaC8O;&G4U29rZy8yq6WPcWwGd|kutI1mk6{iY3gID0?RnyVC;&6#>s>8*K5>$wF zrKuUI7YOkqj+Lmrsg!eFqI4xOk391gQ2%e+!nLI$KU3lprp}rGvq8LmwDpbQ=-~`m zR2Gucl>~ExJh#2J1gC21B>|_+f*au6ZED|>mYY=TQ3&E+Hb=P$6QkZZt35Z=%-~?fGeToEa}AN~5BoPjPvVgu z=<8__GOS-gs)jUj;=h)mun%fxN_>KLri>)RM$>IwdbW!$<@DgMXiWWsUjBj5t5cE zV}AEbu13DxoX}MuE=Xf@izG8`Zm2^cA)Lep1I0(|$1m%WpVQJ6F_&tQQ&xoB9P+&su%3;Jrm{6h zdO&y8>r1mK_$rg$Z_#ZtV4#Sf$c!0gR3u-=XHf& zl`a4PKmbWZK~!fM(Z8C6Chq@u!NA(snK6F}1$B(&!-3MlUX^|(UMjPN(3>Lcg{E?) zct=CE8v7&`wH$Rcj%@EFl-||!`4QFpdZOzs@c*F&C~F9ucLPFqp2g-Q&P$vxLbygv zpXj2r_a+=%dIoCCxGr(hpB&QdS*288?ltWh0>j4I9BEbMIGBXw&d#s+SBYN!6&VE0<7`D6+UF z60qFb{QEOV#LCs=x9L-QXA=_BXn0l6ekH3$SwQ~cyV>4)JBZ{18LuymF(kz>Ri6_A*8zJI84Rs#VdLfoB%bUp>dOCZ%fpd4_Ewn{ob5cNSb_Zu z(K7eKTwfW^pKCyET@i%OjkX5Ed`(ru*+Y$pT>ml0{|T&EycB^xF0isRgf8PN_p~P! z|5>>e+y^ovcL>!vNjQFG)FE2!KN{{g}Xhxe4QCYmnS_N2}_vkTMpbzE1qUDh6*Ue zGw3L-W&Z@%mQGqk7LbwIyLl6S-WP`^CF>92eJD0Ue+;!riCgwD1vLlo(9yfl|F@GBKxPj$z9$>u@ZU z9)dlj2JW}TYxfhXs1PE+87_g&ID0q)Yj-4J*5iR_D$d5m!c3{XA|74>YX!$ap>o)8=7LOWHz91?ugz~7{Ku3P@kNXv` zBV-nx>`&5QqBZ@QL*Mal=x1<^gj$c4kQ#jEE{573#=fByQg&e#jOxxHo%bztjWJ=| z7#N;4#W6l;$F6g@+jA@~5)WGNH>UGvVpKn!*w9ZM*&VrT#Z*#gjx2}5man-0+tv|R zd)?5vM3+(RdGDdq^}o~eil*N9__-VKPePXE6N^9P^nSd=KS2hxwpqSxA*N3Xgaa37 z@vLq$!}>G3@FM?)qpPUaaK&BGy{^0}srL-%qZ4jUHt(Xew1b zGq^C&c^vOnUM!K6+?pK>GMva5I6HS>!w!1xDCHFK9TY=V>1yxcXh%Y`@OcA?mX7+S zeFW(P(_FuDy#=nfz(`v_-P9fv932pSn!((6Z^6X}!{Dx6i(|VlN**gZP7@I7E`-KP zq@@WRdmHEsHbgm-I0v```zd5@twGJDR>Ck2H`VFvTNn0KVbET4d z4~1crg^?A6V^c+X>18&EqVDpQ+MKapu8iDZXEwO-MU8x*o~=EL%SJ|#P!3-iiSIvX zce}D;MwD9gRwaY(5#{MkL}ihJV=paC!9SmTruWt$M*Yp`8f!&uZ0`Su-H{r#v zj-a5Z9PvlCN`Hc9wQ%*v%}W+wYJ@**O?x4=y0c!(zv3Io-^U!b20h~1K1HfB9*~8z zHIOCv+P|JUTN@Y){t$lOv=c|pq)UQqQ46sj(_mBHKma}F zt%u}E{4i>OA*8$wv`o#QMRXpZ94Yxj3#$J!Ta2E;T-U`k)0+l#D>ynNw zZk%q7$Zv{K(Qph&h4T?&tA?20x3V-9xHvGhinndgo~~Un@0^d^2JtsEiFv1=sAuTJ zf5{y!Z4D}L&3oTn`KfC92=G+F4ti_0R?{Gd;tAouFf5swz?jAuJ#E=1u{uVNrxGG5 z6;%zjG|cYk3{4=2Sp(Y{DiJbymGmIMSe%{as=o>r$W55i%4phk)SZoa8l6A zG!C=2Q^zVOcH{T_lOGTMEANwnkLl{9*L(6A?4aCu$B}qccO#~;r)x2Gd0Q(vh0A^a z!5{L+Usbf_mC2(tG55X~sdW7gaWuTHIU9x7`6tMjVP|I2@PF>~Fn9_yrpr)MRUr=U z+KJz`?L%@wuS{2s@=+P%170ijZ)PtV+z&=7#Y;a&a-60odO=P$y%nGx_CE%s?b zs@tL9HEZz32*yqSHjoPbKmKz$x#j=y=DD)hxA2{ViDthr=0E{G-Uy%R?SpNbVz@$U z!r9S-F(d?jj-2HCp%w{lL=tchvO8~;OG}_l2+9#z3{rDbGGb|Y7b*)<@X0$L;7D>I zD`kPeP!CwsB0*Q17N;}`E{NHU&BSuPX5s=Obu_{njP?vbz!))rhggtgS`rkVi~~IP z*j7CK;&&+KI$=o5W?wHi*w6z;LrVk3#wIv_XeXjm@_MT$-d*wjjSb2obEPE2c4_Ts z9k@vqNmPw-<6RT5;gti(Dk5}aUM*xbF*w3oI;N)ZVB9Q&KWW5twbdhP_j}U!DnQshV-KcaV)7|h}?g`Wr5Sf7f_}0#A5>Eida+GJ4F8%3SA$YUg3j@ zcR2hEzvseTg`D(!s3o12^6E0^u@f0ieP25m>{mz4u^mXg%^c;luo2eQH%{ZC^#_xD z=(o@tKp`c$T=|`H5sQYAB?2Y{z|K&U%`o_v)TH2YH1V~sg&aa|$49n)ICv-)*$hBl zURs9Stc%FXyMV7h$UxkwWq9r%x4_X@lh8?Wp_Ic|#)WiuqV@aG@8mqr4VwMU-LIvh zJ$bt*eCLL(ThZKez4t(7($B)K`xcu~z|M5Hrl|d3n~yGPt);%kgpCqoJCMETY;Q+B zp~pm7GHv0v$~aRA(RlOK&v3f9hD@*t#xpjPow+f+B5B!WGnz5DwqkSa#qQe3E28KZ zO_vlq!`{1!BSNLNwRG!L2i(1W;d%>PZ-Fap0eUb8O&$xwgD22XlY)J5MHuB=g40!{ zlE8by0vgP8Pph0Hr2V7t2%!b5n4JgC8J%dXl6#>Ek08|%Z0fq&=v`0G=OKynf_y1x z8U)IvJ69)z-48X3b*H)3e+lL6=!UV@oj6Jqh1`?tux7g<9$Gq?)}@M;5R;%nS!d)G zhS_5+xmFClHkdJH*|m_Eo4XY2r0`lFj6|AdLZM47tfe)`H&ekN1DZ`SmxQ)h?-BYHa=*|!TxdF4G->yr3y`Mb18VE_j5Ks|~7V-5(}E3wbo@Cgd! zp4_y1SPxP|ZH#rXmf~X#a`XD7>n-qK-vYYkR?<5NA+xH0Yw1T~bjzpETFzTM8ALMN z!5Hk~Pms}IkTcSqzCUf$a@F*`^#AQlh}Xqhqi*hl1yks;NWN;2WkX&+eJ7=LNfn;6 zZ-C7=XHm-i+}Sgk@Mk>v&9POG(MV&IM>w2%;QBftlh_bVc=-5Xtedq`IsIQ<6Fy${ za+#letKUDtUw5u}zTnkb=zTsYQ4>{NAzDlr#9qd$kR3|ml?grA$~8lttAD8-qw@i4~TSUQ7V`hs5{>S=mJju7{lMlLcj z#I%J|V6H_;sA!}?$%s(8gAB+)(++mAgEB#JoshAN@#scH)zHtrIgj_= zdIPcP+|LkBKFFV*S7tOUrrg-fQX3h^cVc%+FP|{jldtrem|wc(;a8IPYUNQ@o}qZ= zZEGAmdzD7*b1ez4{&O|_tcEf2>#W1(Pu|6cjG--LhtBQ{ z0W{hQhWa{;SE`4O`XcPuc?=bG3hnBZyqI!Y!Q?9X{?Kx7%EQQCUek2K&MVFG9Jg(B z0up_BplY%c4V7{%4&jB@jeenL>x-w}dkM#n#3CiT2;~*!D9Fi0CJ`aF{qQM{pSggS zUw)d1Cn~+;#&8Ul%p%+;Wo<)_8y(MK=27KF`oDF28PWa!t;V?Kx7D`A^2L6L`ZgYk zx#`GAX~X{Pc4*KO!y`C?vC9-&RYH^)*wMl%y@oJ()e`o<=0Hpmlu$t<$kId?+M0ct zE>+b*7Ca8zp#=N`-i?z%entXfg;LLWELN_#9Wz7RU}{J=Uov5ozD(c8_@ z065h^t96eFxPR3g=!sQt2oe^es+|ouy5ks1DHu1g7>5U+T7e0SC1y%RmpbA7WQ@i4 zeRWbF#h$RhHXh}*S!$}Pn8`rG!B!eWGq9Yp zX`D6;bs%xiuaZ*FfpIDq)$QAjD8 zO~sxSbQ!l3^ikG!3c$S&EF%omFf|jK$q_;t3|s#JLUIvnX!x11y|sifDH(xAsY{4w zv^UnGp!t$~eo%^`)c^Tf1{_Q3V*E7YTt+z-xDEVn?X~47W*jW>>1;i$xM6HT!tpc6 zEE8K=Yph@(`bmCHgf!KohpRfpY3(?!mDNYFCpO%A^f1V%^%l6^0$0@nrXGO^(A|jqiZYzvABFRk?I_DGV_6=!ZKj9?rXX4@+Y3EZ z4r2+F<)fiJ2qyjJ6%j`vjcc<|Z|G=Pz?hb;JsxagQfG`^X?|A}DgNo~qA39Xh9Xt+ zJ9-vekGO8+lySiqELFec9W52eNEREde%^&c6?!$-wXHCF$!)kb!VH$LKZ%W}t8i$; zPjI%i#?lB+2m~aVfTW=Tj^NGO_5ASQql=*-6azz$-~q3N)CdiwNvKT`EA?bDjhvz76;x$vMHFocxvD<6yJi+c1S)?>{id+>j&3c z;J?2Gly>I>mSY)oj14Hi;;LOonB?LXc)E%eye~CYRpAmthX`_`qP#z;Det?xZ_)sC znAyKO)t~<_jqc5LMCcE(j9K9JhaQ9t*QFur)^A8QH`n z*V55P_)H(A&>CtmvLz3eQ40tEiMa2M*)SLq8uCA9-ci*PG8P%~7<8PB=P08$u#xT?B_Mzjutc>Y|XOh)iTIxy8GG;|-Y1{N-O z!+ukCXJ@|)e}2yy{t4_?raGDsNTZk9jdAOP4-x5U7=^K-Uuo)6{z@4vxYroJ^lmJ5 z84j6DMj733jeVIa62?m3k~|4R%(#Wm#OnD0!>o0^eX=-ykEGNybo9k-cie)Rfi5tn z7nha>k=BUV5nUXM-Er5T*y|s@g|8tLjuJmN{3{K(d)4wNhGQ^hJi|q|x}u%?ggQc9 zmKGOMesci7Y(IjcGkdZ5>@xVxbs6x??hEaeX;>qadW<<9JmV(ZcT*S~ZA>Hus2b(> z)yaEtGWz5-EA~_?n9VUr~eF z%5o|S-6>1%_aFlCCdcLTB4zP9RQC4FaWd$eI$-uP2h5<7wyv%Q6(z;EaOxN~ZjD6d zg`@a6b~%EUI>SI)8@ha%s&+%%wDLhXX$&)N$XJeepU}Um|HJ41A?)`5N(-yNb=oqF z;_GC_Md5?@zC%Sz8w?GcF(ueuX>X$nm+=8i)q%s?Pa%)NatBbwS-iP9otG&zqE(fi z!t%LY;X9p{U5QJ==Us^^Qj<}nu$)r5Jmg^;2fMHpuni2sf*Jm><0P*`Z#a3(tahZt z$>Ti@S-?ndFiuCy zl}}W3-LULNM}l4#Vc+I&@incm1X)j>348rNZ@9FP8Em;Ex9B&Vv4BcMCrQaFlzM_X zFBRZ|T+b_=_x{LW=H>@qGb>5Zxc&3@v2phaT1A;+F=Mv%PI@Y^^YSKysWzIMtFa@J zeM`Ch#j$ETE*v|8J^N3ixL%3j?|qok5!2f^&&X35gZiR}uC&I~(wUKd1x|o2*4k~N=y?1TOVSM)6 z5#$zE4{hzz`-mZqIyuI2ad_v?cz^9Fw3ESC*V3iH%NAb#!I(RD5ynb63y@h-EkO%d zx_C*@X6gH5kV(w_{`FGhrs>p{pRxbcMbusDJAVyPiNP<^8%|6P&Gk(vNGt54s28y| zFBBD^a0mlKQD7CB&*(mIx8iCCa^p|ZdOt&Ivyce4ftwhSjHEZp3625|n8N8~wom>5 zXS!A-95{i(G`Y|=!jF(Fv}~+uYL#LJz;h{<&h&>p=Ui5ZDfu@cOmuE%3kD0y0f1rfwX= zc}C0&zvJUAk;tWYuF>>`@GA3wjS)g+#- z5jHDUItdwPqnv#}VwtW5f~lAjGNzLBZ8(wLpR;Mhg`K})<&x!CxcF0)Q%I^$h%yVO z(JZSSKYg@&U`#pQDabgD%_oa_WCd>rDjfbOVmzTgmB<$fsxA~BZ+&@;j@n`zE2%-V zvY=Q+2jjx*$Ro&5U3wO$(&`6%f3@aHL~h4EKSRiPjv)&hJB*tUhB=d`!$o=~q3k8z_Hq>%`F$-4 zNNvb}i@tFDn;(%VmxK2yAnvyxkUW5o=#%8nJD~?nK?560T?Y<9b1^y44HhPhpG}Fk zP;50+mhw*+HT06Ug$?WtO{6_W`tRRBOmBUNn3un-rV-2yVMU(V7xCVVy4e;L^) zbb$2k?Tt8b<}6NUhzZRR<6Y#@jyf3|T%ei=I+ai;{j2;_z!-CbOo7Tt{+eSVM7fQ( zg|?nLT5C??;JIq*r~1ke)Y=*l8+nLvlrNyRrJn+c1}oFbbAg zv7Cz#XIiuO5akU)!8~ijk*HLZRu3WrQ>n$4&ws*+L>h+;;nm$!+%N~~1EuZ5r0#o5 zqaWX(V&sImPBbv1%qZo;5u}&PJu{Var5)LU1fq+nX==dH*F_#~NI8Mhu}sYr{(-cD z>nOvPW9J!zZeV%kr+>qy0%p>)VXRsw8j6bo+8EDG{9A==Dkz%Ul$nbTWH2O58{??{ z$B;||&OWx#Ga{N&eJu8#p&xwze${b_amOfcJ9-Y)O=6v4yh2MGLg(A@xDlU!8;8#R zdH8KB3sSM;w@9QG)?HJ$BsDrgICziavs$oicOouPIof|Mm z`FPfHaYx`pKg#_~pu>+Od1tj%<9y0xzJm~U^tYDb=j>*EnzPTMq)!oDs46N%dh(TR zWQ0-l)ySh->k1LGYX>%NibN^VPSglvX+jyi$2fm1Sv&)Fwx&`$BuD45<%XtC#)}ra zA6@Qy!shI~RdAfavpr^6%$5Z!UJEQ{W@d|-nVFfH!ICUyX02E&vY0GpW@ct7`rRb; zzqv_OUh|Nbt=ihE>2G@G>z(QDbB=5;2fG&bv4v6tvHOZ_(*7RPIygR07jL)$Y0JWv zG;f%-#9Pj8`2DO;y)5lWBDVP7gQmnB6nbyz+F@l+Knv7v%EJbv0?@}7;5)J-+#p?s z?Gi>v@JI6`!r4h5IRDlH@8Od5Td*Be2CaR>!|1mq?cSpKt5J$0K7rf-M==%r;LEpSgF?m?H zk{LE1kS^3GV|keO>kj3l4IO9jLgz-*G+3-*nztk7(cvz+_3;%*&xrDidzN6eMIibF z6bQ1vO2**Ocv&z!JKrg-GK(Kc*EG)Ijy7A@ienmJ!=Z#P3RGuCL|2U0R28MM0v{{t zuX~q4z_)evQNPsqOeZXehF<+TH_g??+|62#0xrCX!DW6jl&!RKb}6d`R_XbMH=gkI zCdToH*~vOU4kIs8cv2#;l{5Yfy)(*{pRei2x_T~U;cE9n_x(h8Kd9hftn<#|&G%fV z2`2dY9ua5{)d(W4g^G4QA8Wrp->q=0@ni}KV(KcT2q=Zf=N>KH=2`_ytKf~Ac_C3h z9%&^|^D8an7R9oi$XIqscyAAwHcaicW<@SPryExKY(ZpVI(GSb7fI016!yQ;klXf{ zl#YV^|4plww!!v)5|v zGS2$|A}i?c8nlEh$U5JQX?-6zX)C=i6+|$p5oIBq#DT$%TNW0!#dngh%Hj$NCPmmK zRK=;TkRinl&$h)XKHMGF!aQfB{d<>HjwWs@ji0wf4<7BZZ#Lr_fh)R8vD;v>&Y)Dp zbOkMU2;i#^WaO`rB>>kC#xttWRG$qNA?^mFi*&uqm(KcC=}T@Y4|AQQ9DlX zLl>T#;}?la@OPpz#kV$G&`s3D)x<(q@BN*fp`T+bl+L^$bW*J|{Z9ljwLLo<|6rXl$G5>y*5WO8CMA?16yHS0Tg(5X+#DHpQC0W^Q%nCxk!^ux=3e5Dw%IGpToP5Mdj zH`2`SD_ub;J+kq_`yU5}&Lm2Dqyw8FatBQQRt!S2O1@Q5rmR4oI_k;sl-O(( ze>#hJYj3j5hDL(F9-E!XYdpg2jYo3y#p~VR#^z9&v-923^$>&UG!OPPMsqbOBwm*% z(5!|3yL{<%b^!>Nk(*6_OzmR3RUFvQ>~&42Wm2VJv`Ks{?~BE6vn*CKMCCyc$#k{! zqPO+9RN3irSULFY*A;QF@r8iC0HKzG$&eBKY)ZtPR<(o=JaT zlFQ8Em6pcDoAdJW{e`M+I2=0*CrC^**;3i(Lb2wVaQ%PojQM>Ck*Ij^f#PVT}{WY!e@6X*T?FNQ%Ohkr_+WdD80d6?z6~VW}hM=I{6T{ z%^9qn0EyDP(64lsSF|i7#2+Ucfh2;`(+eU@kN-LnnB;M&WNmuDXJHCs9zwWn-X-O$ z_3meST{Bbu6hTOPtVDepOwsh88Y~sM?&!exc62S2Oz{qENJVaz0t)uHJfGe5EF+;T zBH{!saCFF-x!HXP+o`YB&!+5uq9n-M{d@{TWj9N}PR|>Y_NV12_sg(UE6;+DxqpUg z-dWEL{@CVf^aIKzDHAP|7L!ryrM0$9J+z3FaK|@DuDS;xb;k{DEOf` zlXQ9(%?gLxzY2u_b^!`6ESZc5oM8V`GOdXSxCTCdbwkgO_bF-)v?G}0X{jE56~z^q zp-&X{?ZSfF!PyNx^p_@-oo|*l(FOeq?uI5rwjBmkl{oISBwG}*nbKQ*ljMV}S>}r( zn|m+4m-p!f;p^9s<2n)pzb$+j5~nPGa^aGr*Z;f_tZ|sJ|Mg=d1yT5r30N7^6~4V{ zE}QPl4H!o8~u>fWe=l-FbFoX;i|D1AJv{g@CN{&=3q{5h6xmyQUXg$RF_cquCkiQ zhj$aCox7|YOOoL_O6whh)}&4eLzHsKS^IR!P}lN^NDr?4;bt2K$DP)a)GfyKKA`=CN4dqjZ6U3@ zKe0+@;D;)3RY?@y&7PMUQ>K+(=SX51`1Kx7LU|J=hi08@h$}u@{DUWiO)f^D!YcSC zXGhnG=1s;=1Kp%r9Tl5L>s`#f^LXxjblqptom(%6$_X7drFS0>HCJSCj_AhSBZkyd zdu!ZM4#`SYAk1iWt&=+QsD<7F^{fB3+GlvP z+pedCZj2x7oUmsLDW86ozN-`VQsKKNh%RO*!le>w!Er?YER-^cY09PX71L&i`+yD{ zKux$8{14dUmku% z5P}!M0mp|IxSY9R;379sA3j`b}e zo+Qk~hnf+#1Cc{(7!t7jQx!Awgf()0DF@ z@WkPTi|&v{jw=Y6=iLyVJ4plGe00O|1b?9L0FTb#w)Kf9-=R^Qe?x4}iTN9o*fFV+ z5v138RlZQ?Uu4TWM63&`#ywOY)-EELW(RfNMUM`wT#GplzPQ>24199Iak3P`_wmm3 zwsNjV=GS3go5tx+H(1!SO$jJC;`{Qt#}<|U9dIJt_tHdq3SP&vyiCJ^I9rq;?Z^L= z8f(QiF%HwxeJ1arRbd=)!`hb}6FsI+DCK*BB-BL=BT%J1ZX>$h-lPGTvk#Or~)AO zC;F5J3f5{r@bkMoFi|rR`dPZBv`G-IUP66JO<-9`yY42Z?FX1=j(r*{I25;h4H~Kl zXd33^JvylMRW%gN`mESCpHEh~HD9|`C|g7|%OlYC z^L`@@h!^-Sw7Ryo*YT0@a&1T&FK=#<7LY>zu%2LNI-bdS@Q>3bMh58=c4yQC@srQJ zi*|*m~3qZN>fwdV-;?@kqJJ4g#}*X}ZUaH;&MdGM9-pQ7O)8o({;;FO zP~jR50x!SCVr<`~>)lfw=Aj8)bV=B13{$#xJ2FLc3AE-trB%-!U=$!WAVwEC|XIUHl{8fzc-=-IhlS-9QyIE z-1TXMH5=UERwQ9BROzz~*zN<5=V&p73tFG=O<1mzcK?KMk>kc}tmWkZ zLn$?X%;}EZ{3e@^f3s_3zH8(6zxV}UIwSt2$+w)cj-3;2O1m1n`&d(sc0wU==ck}=jgD&<`hXdTD8L|kNpiIeV!hom6w6w2tC&&4o7k*Ejh?^f%CQQl{FJS zJ;>tLd`3Mzq$5dZSANROcpZ`XU^O6su6XR}Hpd-O``$0&^f!k06&Y1 zCW#xgEy?9Tpl-UhvoxdcFmkHEyRnu43ySv@)^|uTqy1pt$r`%+%44(OsqUccRs!3% zLDKOuS*XCOlUWPA+f7bqr2P$?fLoa`w40d^` z>6n3X8=mkN3NUS(pt!4%wIGWhHsJ})vOzDP&zu0}*5$EVyk{N%O;uk+Jk&L4pP|GW zlwVFOw^SXD*r^-KGvYJCiw$D?Ua};f^Y*~oN1s}Hit6~gck{vz183Kc)?As*uJim;gb17nq&48zX)u4ME! zNc~wGw^%L%fqaJv>XQZ8sfet$u`b_0^yq?WaNGrzwNzzg*pRn4d^m_Y zR+qg^N|6GaU(>1|1tFDYnA4jP&#hcsZjnFw5~-}LSJ1=53-v4I;A}fBha-4RRwY)Q z53=lSH7j@q>APp#)C^|c-@;`h`;P61lV3vSCUPa~^+!`Ny3LGMG{fZe`M2_3F0pk^ z@wvR=%zh_c%`9J}XV3J{_BZ!vCCiN#bVOUtIoQO|o=ti6m>=fw|5f52P`}#=< zZCZP@j}V;oPLoP4b2=c&QB^%*2MRtPbv!;hD{+#^rgj(;mgm1Y(8&7x8g$&*crmBG-qZN{gJFn07WfO*^Xuw11C4$y&GXSyvDllTQ%FQdyAZR$C- zH91{2jpsz)FVvs<{r3go(fnb^-VuePB=2tn(g9vR2(n=%x#e2Ryb1@1CsoEY@8SE+ zt=7>`^t8fRGi0ZHXwrZd@heQd<=xXY)SW@uzgeiUT>|9g6D^#4Z=v1kksaS1i4n=% zErCkwFjln&nMHrp_x+rJ&|%NEX4 ze>0I#%3j59MePg5v+EB^Y^S49e4X|@#%bM7`{;x!P{}~U#*StH8^R;b@)QrY?y`Gg zV9~ZhGkZ2>_Sq9M;GnK5^r6G>??_v6`D!-Swc=k))o+^VM(>1iTxrEQAcVt?=fsJd z!-L%(Q1D|$`eS9R(sS^bZm|BH+6BbqRFgr{7}l`qfp=jYtWL;qQkM<)Nj?OK7lN4? zkKo{`3bleY3zO;etqbXBs8VPY)_}sydiyO%)l65ENm3CIKypev+KX}>h2FZr3_7f;r;xwzeW8GWknln<#Dbz^now(#H;0H2(O-+&_~vj+O{|)>NeQAw4oIYX}g`U*Pw@jymqa}arP>$hFx2>Dv~!XlJ!So;RsnNFqOgWa)HR;ufK zuy%PY^l(z!p>*zpf`iQO$PwoQ$Rwz=3!tom8yc(^>sws(o91Ab*?0q~?9)}z4u z8B%3HsM-eHW#2KpW0nm3=^gwA@KyAPSV5AZWiTDEF8w{RV{It5VxWH-9QKC~x^a{q zj!SleYD3Hf`R!e1k}i+2+H6M|+#sSA`~{i?9UYmpWHd>O!eJ7hj7p%tpYO678H zu9c;5WlC;42SsKX9VntapsxzbZ_v;f9JXN#47$ab=@ET%@N*SVvGwlq#3Pu^WOaN7wJ z$`PKuo1iMXYtkpue>aLw6=F{gqPwuf>g*~U;%XT&J)Cl;JuQ0k{2>bXHOxjz6uIRK zqXa4u9yd5o{H0(ci8FdN6OLTSLswfiUYPeLCf&m^5>1G`P=7QS)rF(QZL~#zDmdW` z>$~EGG@Rs8H9G>PC_3V{q%xIBmD@4hU^A3P?UHog2QEDt>_CW^U=ou>ze0}!$msxb z#{rl~drAwEz^I%yrhm)xnbnjQJSDdxgG$0qUgRc`QMLtGJ9lPQNe-CUxDg~;?3>~H zl+_hHMQq+QSnH0=I+RV=E;x%X+l;tx>YV4@4juH|b2lg#yl?HP6jH{TFX3-Gy!Nsz46;2_CmrVVtk{-wn}As#Rx z#lS$JhrPRRgSam-d5uMjL^cO6GUjD{lSR+rM&SEVuW2&N8BI3D=2ul!8xp)_k`@zGC z!e5OAhN!cOM&>gbwy~#^>9=b``HT#p?$so>n~YNN(Ki?}miw+f1D0w1%LQKYZ$Q^? z73pD7MBZW1gR`I;^V-b?MK2>%nwJOkTgyhw0HOm2_rVEoR!n_kv9ud`8g)m&{;g#i zmjZ8sbSoajF5gz4BHjvN25`(vY!=QwkOndjQfo5M4&;|z&3a*efe6i=%~bN(}V!K5HOs=|^CuaqHF~m8G29~8&Lb94vtS@O4RB8bw-K||Lrf+MTXJ-g_ zGxM{N8k1cfhWIz*6#05*VleV^A<2yd?a$9W4Ncfb(N_kC%ZQMkP4!Zix!moAP)$*S ze4M@^a<+I{T8|9bX$mexiqnqz+w*vDo(&X$bm~?45Z;Wg!HgW6kP#O)S37N(V~e)T z*-9s6>AG#gsf5AEjx$ZTKSLsG>S6z)S&SJ_B0fbm4h`lUJIWirfI#p=MOL$1O)5De zOC#&hZ_h8e+0{Q2o$xldcA(9?ZV;cFxaboQmJJV`uC7e|mZt5z!y*#F?cu}>V64$$ zda{IlpGK-w2D0e3Hg29wn?qP|B8qiNUL=Tf`g;WP6$bXg%vl-qn}V08SGT50G{%6t z70W|zNN6wgolQnE0#1TiyuvWLK4JFZ=cmm;JQz!Ii*d;4lmL$^z^kp1aK&;~Wy(iG z3ZahaM*d$DcKy325ATVH-`+Z5HPZHS9YZYhghHRRhR5*bOMhv6$MpWK9}! zx+U}u!K7WHzXq7d(*x)j%+0(B3mFn$BTy$}M~0dj-}-poIBRhJR;n{^G!N(|c`=uT zVZ++I8Pw}WUH#@h<_)>Qvw>O$iWnL%S@{a4VJudx2y3JE=E*XU3h`$vJE2BHPl;9p zFE>0KO(Z(neD)7I7yRz={RJ?}`noO#*8X;ttlc^m-ui!X0r2u}9UCJD-8)4%k?~TZ zz#l7kOzC-x#(3*R?_0Bbf3`~6{dE6$5gxT_nbwx|Mm@V1&sJe11_N`)T(uit@q`l zQyctx7iY5#R8B`oJDB*<5jcYBhT6iV#a=_eYVsr@B`dBGg98(DSkN+eYFkvUgkrdS zc9Gk?97pc0*Vn_CJ@(W*@#~UZL)pBj*etRKXpj)2zga zFxgcvukH-&ouNE5kP*~u?AgrL8xP8?0dA!s)(J^99bQ8Cu$8qL~H^7 zhIMlay@Ez$U0N*vK42?DJfSB2pdKTYslrT+SDnLmsh>GFZxtLVMt4N1tcbOs{TFDt zT)mf!)H>jv8@8F8E~p@U8IQ|jNqNZ}oY73rvvx{Rc~aUnOCb>>C?xwc1z*m+WwOH= zk89`zWFSFd0A>lXnIasW#cV=k^WrEyk9%)BSw4D46{o;c|r) z>;o(EiQyUSbAnwg1P;+xrFO=sT>;s&F7gXj_dL6(k<{gjK&pZBrtQc2JcVMwV2%Yd z468{9Pk(n~S9J)#veO$Dj~d@W5$2~#7H3m5tAYw+j??f#5d%y_lp1{tchkN~GB>tw zE>!(f(xAzzRF`UeA_!k>TME^Jm=^5jaK#da0d(t~c`HCXtJtsmZl zmQPhwZdVf$u!AZUb#QB*s9spEqRALdWeiET5pX`TK!q>h=GmG&FciGQl|+%2Qr(Sn zS|KDiIl%qlTHn1u+ZgLl&uXHLtL4gmXu4yeA}BjuLjv&bX{=&tF#hO8M@Tr5$~1dD zm-(U**ql3vQ=i?z5brg6Usle6j^?mbOY(N@>=FpEaO4T@`?becn>zsI19Pu>A7Kbg z7Szm;6l`eK0wNt6Oz}};6!rgzUAM!RkmL2_XxJ*jbx-Gt^4ds3nwAEy0J2GMkzx$` zImCTh0gdN<{yTv}yQm0X1LNqZ%7WXXHfyjm61>p5jVz{fCiKcUCPMHl1cpG^{c)vS0G~1O0MXq}Vk|^ryn-&fIJUv`0Dj+rd`p14E zJvDs<+kMWkO{zu(Tc)Fpp{au<$I4ZaZBNyOr%3ckH)q}6X|hRJ#%-=BNv@EXH_A~wv;$(# zctF+l^7fKA<{Mo(@WjEhYas1a+mq3{{BpR3|?UBLB)9zz_xe(DF^QQPaad;A|%(+poKh?lc+d-$% zjqF4GeJuMr5v8`GhEB}UyRL_MKHe1`cmw0ScQHGI*_nhK)#9onS-z|o9MUh`&w0MT zhA4{#m{+O{NgI}wKs!IhNf)DV3VxODdp$YXD{qmv(@}-&jWLcdktIUa(;W0nKxJ;o zS7@qXKc$QcmmLp+-%)%hf-RUob%7l86^j&8ofZhxw_Jh@3VUk!&B{v5{Zkbx57)E(yNe0)35C=uR_dfM0$z;>F8+pCxX1)R;YrnY}MoDeB)UskJQj{i}c%q?c`Ga?i^Aa>4mAC>~_sbK$E?leP7nZ0;pK`A*}ey#P|eizTE0a zT;6b;>#AstXTK9mIq~64RvbaouVawu(vzjb&1_V6EAgYNX&BF$jzy29U^{%6h#LO| z_Ef{b*CKOG834Psn=jMuFk7A@IDSt9%%c<=r<|=4%^-H-7AZ4w6&=UIiki25? zMt|@&LfvOE4HGn1n4IbqZdGA=&FJ-_UPuS|Ry@Dyf=jA`{7b2wB)dgd|IJo$&IFgZ zD^iLNZ&AHkP=ys~XI(jCmV1L}iVQCd4~ML>i0qF~1~2=T&2Q7&ztnVHVn1_fOSj|? zi!wL&|1%(7JNeH0iPe{}&c*och^D!s!ToW$p`ifEhKugopsOMWlof`)yT-b>9o>jV zD$-H<{QD#(wl9jKue)b4DJ~0AM}LBY-bnG?R%|?c1vhmV#CiYUG>oR(hW879N_DMpD2Sr={vq+di|oy zTWtFgMEw}_CkI2rHH>{K7V>I6>!4bHt}RPtlw)!-wmLS)7I4e%CqQ-ozE7|^%QJ?5 zv5s3@gyI(S;lI#DDjEgikM#m%)y zMY)Ya`u*6$qOoICpRQ#gpp8-b=u;Ntk>iWFRvh)6Z`JJ1LxVU@{n}~VzV9n9wBE6X zZ2m7Mp|czFd}@FliX1el$W>AUbm;wcU~72g+E`)M!J4^my1owLOZL_T8S}`d}^%BD=HGg(yuDL%~xA$mmYg&pI zBFxm*4Ly~bVFO&>dTx4F{d#6ya%H4$0@C3mqL=*FiJCap;TSm+1{D?##a zY(;ZccA*$8Kx{GU;*eyiJUTu}+5pWrxJN%UpWjdkU>6j7O>{oX z#R2}GPiKexLOlfus-H%3_BJm+vpW+$HLGej?WnB_>q{YwgiyPKaq(Sy@nK{dKME>{M-$<+-QbMPjC=cr z0UbO?;H~X!6*?eZIy}4^!?FlYy7YI52!D}WbsuWEoUvw#E=~#htet| z2_bNec7yx(H48m4o`Wtl?K;Z;G9p0y11%m-0_t-hhzNlNw9g4v8_hjsm0Rxa&N5)7 zv;M=C-kP<7+gG4u0i*8hI=?5iJ0cUwD=9`Z5ePq%xP~UjV1~-21&Dqs_vY1exGc!8 znW&I{^|OI;W_TVD*XJJn-uuF#Et)Rrw{;7_CtrjLc&N^5f3n)4vZDVq?F86le3trf>-XvU;NlRCI9%B6Ji0^#i0yfab0M{I5?T(f4z{-4zIF?CbD*aM z=k7>4%k(V{saB^lSbr0X!wI2G9OxMkImfRVR3KiV`Cl{h{~YAAI)V>n9NjT0SoOv% zwYlVtp~`ef<7P4Nr>V9X)8{X)W zLLKpxf}#uX&z%VoE!Bt8g+%}Lc{?9|@_`?EmrXH=?&wmo)9q*~#S76o=Sfn%fs4@u z{6R{!H8Y{I=${MH-lsupvHezBQzhge2oWhom1jA`w?F&w!|3!)!6&Z(mRdaf_PJmraZlR;>Nfh{=DCGy|4lNE)aM#5 zz*)Waetsa{vy%WH%1uDKKFhr${?ct z&m#OcOaIvp+9Yrie!v9@@gd9q@yq|)SGrLD&$KfVfrre0-&g;;_y2EZ{@Y;oF+YA_ zdj0Q{;wUi zSrhFK20^gSv6fb-y(MQ{rIfWvo%&c>Dr`_(y^lJlS01_q`MzMTUWRY{tMH1)vmp4z zXV~0^=mm^qpug`JB`D-Z85qHKKZA=zbqJ7ShU6l}ojv((vz`s5^WM<8~UATf3Zfs%MrSQ_h(+JpM1fTj2Y87xH*N$6k9{ zwYTU(1?dNiC#Rx=8TerPw;afBbeA%Uma5@#_Ry0vb!^R6y&;N z>iskvCqdU0`-JSm8tD2O$i6tJ2OI;}34E+MDY5?`dxTpja?Lr*zt$?I{;v?mNMR!O zKmH(T;U;BdF(>6$!prNHyF$2m33wsZ7ro5W&DUYiRsDQ^t^RpGekad@K`5V9UQ(58 z)ODMh**cOV0J8~=Z3ynIt`;{#!{~E&aPqRd>6C1c~Zc zXqA{L`7C1+aJa_FiZX&HQ@W^?wC{)6QRK~o5rxzL2=C@`{wTR?&qo5UFDK+Wkm&zf zPc)>}Z{cP`O>0@YLWAM`zP9JfeW1KOlM9eLv|%4|4WvFjhGF}vaXM4}t*>LS>5h_0 zt4mTQU0KX)7bN)+ZzMl1d}gn>piGjs)4Mu~ij0BPEZ0-65u0*fI&|^5SbygP1ggqK zHRu*9ZatN!hh&OTzD*K|p3^C?HT17tbj5{W&M~kw`KP5Bz~X#J@;6%ks)+MbG^v&b zJ&XNmc`MpW{ZOZk{`XJc(lHBmW`GrnZB+I!(w|9bME@;(LHV+L`qn2QHI zg4Bt=jTxj;hZqJHMqYcJn1e}yoiPk7EVsII-DHJDvaXIs?Dp^Pv$Ih#O(?f9(gsFG z$iHT=Or_W4qh13IXRk2)TeA==Wi%5gZ6~RJMagR`KoXERaja+w$uR`UNXkUjRQ@A2 z0E;&7&)0$&rgeY+XOjPQ8i*mvCC;j$*a0glmVta{r-CBNpckO2Q?XtD-LtdJ=CwCq z{M=J9SB^-OT+dKM9-z_pcHYaJ3{^_AuB z(5Do@L0*F@z_qD~f${k9sj1cFU1o04Khm+OweYKp%;V9__XVlSPiD{KCK&k)p?O!E+1y=SR=y9fD`GCUb0S$?R$30I>a ztV1J8T3lsgwkvrTl7)Qfwp6+i>y8=y&Wf@;;q^afFkB>BeLI`jC zslm_FU}LcFpfLPrtdPn{Rz+B`G?YEDWBzrGqPov8X{8e1B(W{8rWBFi^IdsvjvA{3 z?D>`ZlaYnHV?KF?ulK=oK#+QuIavjlUE7n4thTw?%0=9buRKi;^F${S0i37II*(>% zl&Z1vcYFlt*^_`(_0rCSf{IR9CZ_n~c8tYopl7xe`o(q5ys~PaS(IYOTmE-71Wi?? zfsMsH43PDuor4~rLPZj*Fs@Ue>Wv3fW}p4R-VA3sFadIK_6K$TRSl_o#ZO{C2HDwo zSb~r|cHb;5s_m@*f%bL<`062C45RIt((5|G$6VK0{iWrG+#(^)>RtKx#-en0uJ{k%2mGgwdiJgN%8TVPdmoF8ZcK`-!Ya!dj;dnDU9|m;lVYhW7N5 zvBwI!qo592EuYqio$xm;>2wu^B2F3Ffv%8bk!6{G#axBD^CeZB}~@|;=jb2&MMbd6^6_i(lH)^2%9qfuuN?ZelI?R-&M8b zqoK&UPFB&1%le{OgyBAil4+A?>aP!b<{Hy{NI+FXKv`0aa(s-)>u7E4%4S(rsKd6% z;=0zgu+1klVPF;{HHb=TbN4Z`oz2Jo*^lrTZ*25usne}LKf%McI7B-zu z{`OTKDR-5lJT2`a4%;nqEeooQwIhtm4?6rDZ_Ln20FdVxmToZ8(>~`VdCQe8QnM_D zq-rcuxUexDKBggVg8NWo?sgxb=~lf;HNsIAAedvR1ELJVcrq7Bl+iyF&*doG3LFv{ zl3BJpdj>Rnof)?D!PcFwSdg^`M z0QW?3G7=OcEADNZrD5!;%lg1(N;^%t{$+D3Ggv1houu8pT(Oi5PS|L!Jk}I{@Kar# z8>#P4%&Lc2Qv~0$huAUHDH0*f1$$7<2t!{3d zwYOUp`RkLxAH?-sNSidX{5#GH2YqaA7Zwy(Cgfm%MrE1dj?Gta>C2LPVh<#__s>iW zS#_IbA*i2WW>I&0qZI7W;XJi3eRT4C$&$Y)}F;|HLhNrtnsf@ES8Vn~l=$Gp&dY>RL#6-f! zZxb7vEA0%dinaEi=}L7UQgkpK`sLo>he(_%L%4O~9J2%xZ?k-bp|ZEu_A;1$WK3<==Yw=uVdpIn8Ua;3UL0@8Z#kh8;%LO_u@MM} zD59`G_&-zvF3&~_Ko?=jiw!q4wmt{^wc#A$RWGj&*(d4p-G>@&2HibR>I`;VFrG{7 zS$x(amb>uY_g9vrTH}4-iSrO-_B~eU&)2p2Yg@0(rC-i!5c5it;Y8k#LV)?U)gl;m zK!%N0o7P+W(zJX`m}(>m1B70ONs!Jd5q?57|kWEUx$ zZ)0;OjGR0X(`QV>)DgX5+D7oBLT=1y?Djv7L;fcan^CNkWA5aIg*Pt3a64kzwg~0= z#r_m$k@woZ8v(~c5Sg5fdJ4VtD9r3PY&@pVn~NJK4{WO}XsE)uV@Kd0cp66n&me;W zE|DzUeK2L*6wI753Bv}uwNtRDP>>uB|HG$n`0x>g#OG?NM8Vn65#ump@(4^J@8)Q( z5lYloq+|KZuOL*_A4^|+2;=)Zwi%;kafk8ZtLvd_?TV-0e+R=HsbFa=!m8I^L{Nh@ zrca%R>R5klJ{(SEc_Xa72H`p8H(Dnc&%Lz~24>E9`h)i|L_Y~1zWq63^9qn5YE%@B zFF$<;zgluWnGD2N-@SmUQ#IvI(a@*2(q9hh78z{v4qF?Iet%pOi2v`JB}Do?@Z zZ-0Wb$)$MUwdYZOau2rc4@7D)jT&fZV9(QK^X4zajq}FC)uuIlpw6=}4ZHRSAkg0* zr_QC&{6HjojGH?P^QQMhUGurv9H!ttM*5`}{Yj^OZ#Kt#spDD>CZ)EWbakH^>v z6R=?JXwtP7;Y)GkZaneUFR*v*i>Kat6$9;Ci4dr8J$vXkeEs8I6guCI&7Uq*6prO7 zr}5tp)*`CF9nU^F5<$QIieq636p(TJxlYE{t6oDN%Zm$2)1ppc-@yQ!I1z-Hj8fK< z!fs0!44XI(lO|8W+;RP2stEj4NItd`A8pu!>}+LpAm(HM-bjv63=w_r{{;WL!MhDW z^{RXX?A(Ce+YTT!jzY3V3dSfv9zA6aY4tpe^7CvfjAVSisK66A%zez^W1&c;;8`Sd zS5HhF%egdZ5=IX7Y*Jj)4O>M4cC7gVYY$iA;ind$Aowu09XO5xL7z;0@%}1~PfrTD zWReJ6iwLM{5Fc_J2ag5gL{JElxPA$~h$9|@M~%jm+0!}Zhr_9ra9T*a3scVF@R5_; z^B>0PxTdwk)CPlxkHt)}c1#}M<{2UEPKpl4Vcvi6pg$7xH5AUm&Kbi-O~jaKGq7Mn zZy2|bBhs)sLk61c<-Lg^jFlnDlZW`_mamt zatddnlAGu#;5m2-7S5lF8B@lfhoyEgP@^P03I2QcalRiTkB|e(w!y^+eTR<3j0tnF z_~y|tZ$$=08EsNSKE8cr83GEc$m5AC`B?t?yV&h!3{yLAJo%s3F~mV*Y|v0yfD`-n z!=G3mJsYj?wz~TIaPK_;6UqC`oHGsmdfK$fzOyg!{%f%GyJ$?kVGi7l3K)lOJI-Yb zzRdvceFkIhE%)H|>Fx;LvI$$scb<*UqCt%?WOK)iTW`dz3#X!=t93JLB8;XJ!p`Cd zd9xEip*WwOrzzUR3jGHR$JD8#$zx52b8A}OipM&{v2ZdvS$W35)CzusN0G-G-yx5s zrU6WDQW!P_ox!%fCy-Sx)`wa+xcXrR*Sp0xPlJbzc1>3y@z6%Rxh?<=RwMEGml|GE zRT6{OpL!ov7EZYJfhEW|z8gCZof+_nSjOS7?O&q18v z-sDt7A}TrHg@U#3kO`PDjWmARXn5LnrZCaKa`C@(&B@c7$2iUp2Ao7-WFprzDvYSW z={?0Z`Z>3>KPT!CmcIHU`psX2ajwSj-?xM1CPUf!nAl*<#7U%q3o&zyPn$Xj z#cXE$c^o3%{(MDcRw!*UrSf&~hzS@wbp~#nHUK7V>fM0EkVDvW@Fe$+A;>IjddA6@ zw010J&7Ojhf>&y-c;y&O2tJB~q_qc*2O_ONF~x(E&tTkm(_Hkd5$a=^t~3H|so9mz zUFIhdkO=&35KxmzE6z-$g-AAbZCHoYv@}}o_QdE3qmh{ykBsaTY}@c1Vzct_`m!g` zi>D(*5*Dj5LBHYM&$l2kOKgHoF?8%`7*ysVIwlUge~P2PDGa$UEyH~yoS|=N0gJLS zZp_OOe)JSd=KI2?iNC3<%A+9V46=kSF)siSx%Z)mw}vSe@kQb&49qCYg+rwkyu4|F zBq|`2L_i`S5s(O6QwRvl(-T|2$MOvU$S$Qs!Q2Dmhq$7Y3a;4mC$WyNu&5X;du1ut zlBNZqz7QMV`v~g-!XUPPCJvsMG|?Aam=GKijcsW$h>R-3r^}y&v!X~+Q81H+>?~Sa z7wy3E;C$rPn8Dr42c_vL@ag9R6P_qZFU&<+NiKGLw;VZPkubd=_*u;PP{a5gLrPCb1w zew-mfL(iiqEgb8I-4d%Q!Rz|zy0tnR_zQ`+%%b4cwop-SCquY;`ok@ zh)q5ZgR}(X7}zOFkIV1KI-Fel5kCASh{6))V(LPpngKAaFF?eZvpBeO6HaqYcKGl; z4AlUr6jdCx7w^9R4bCW=Zgcb+I09aFdI;x+?T7EP;byIiQjN4vQ#E~_L~$}gDU~l( zg@e(ienP~#3@Usc!?Xd8%KBbYQH-S6d=!`YV|idc67uV~=^l<=>H_i?sfY*FX$+F-J=5d4vb;=j#;WlFRYZ z^9#_#%AhjsBym`dljntm&Vo1$XWR90F7y$2Vdrr+U?*ZikFxI`!F`M8z>Zqh z#<~(DP#BS8>#CfVY~P5AtVAT_=D>zyqh2}L#yc9z5fhe$G{YinN&TKD-%4O$??$0r zJ?hPD(8JLZ)luol;B)3w1PTSF&>)-H*~78E3We!B6ia^6!_JYXCk$cl<_j|l7K|Mn zc)GL_IXRhB(nn*;!$X_fCF)hpQ?Zd}Par8X8@(0{fTLE9+rH8l{a%OA^&jDbpN=72 z$iYmg+@)gNMz0JJr$caf&lZHn#KQK2_b}Q`Ba9KJ2BLYYF{`8q8@~DiIq^xb^XZGp zgRBr6a0=N;=SUy2kgYPon|F?HRw$_|vassAZ?XEf6H1M&oqAw+A74~v1S4?YCPc;b zf_atTt-B`l6`9l)#pC1mKEct5B*n6Gkjl<`#UH=l*DPio-g>H*uz zEU0}4!`W0j;#z0c^0^}65Z?R0?+~IW2aG6C9|30*HTy3Vft+(8r_Unp@t5%O?B1G( z#A!l~k2+et=kP?Psg*5H54!U7A(i|+EL$=bpE&2rVs_%Q^E?q?*n|6$VW`hdKq!^i zN7;YLi6waZ{U<3*)IL{_sJ*}8e_yUcWKk{j%`DMpI0cndPzP}??b-YT&YXzCQ%nDe zX`{R$4yB59t}rVFMLezg?BUT9D7EYbZ<}@&4{d8GzexoCL1Lw3L@jWTbALMF+vl+;qx`bhok? z;kkLx<4K{_Uwnm}^h~(*9|}J^eMBBTjkL&9Sdo;5LSuU@o!h6S@ajB#{o$wh`9QFu z^mTCcqM^DAifH5%b@mvd4`CGYu=BA^X3RN#(S7fA?Vo?7cP}gK4KPwHzxiwhz z(I=cQDLk#|i*e(8$;(EeJo`L0tV&gsp->c0#2sX0-pmPnd;@@zZ`YP~R`w>>YZQJ8QR>pmx6ADYb* zsyeXj;fJw(oluk*fyjhV{6O9?DZdm;AH5UK=9>H}xo3?n$VG|&P6QPeBd@xK#unqC z!#2exq~hfMZ8&=_1?qQR$3j2b<|>qAp2J7n#{@;EDD%Z<@MySO)FI}46oL+JLr};$ zBosV@rMC@)7K)6ZEqM2%HHgTdqSnY31BZ=)tx+jX2SsD|F9{qkA$aoNuVb2@b5l7q zstwwPk3L$BkSy^G*ADKNJS`h=H0T`Geia8#7bk_Twx}wbt0+JG4g^|}(%+Gbd{iPJ z5x6c8(4m_iio}qZlnn3r58;hFroe%gQ`N;;`1R`#u`w_W38BH*6_AXl7WGmDZ}sKL z`1+%bNGzy?!{8ZM_Rm|;$DJn*C{Rs{Jcj>$^%IgP@Z7rM6O8eG7kzAdVD@+i>>-0+ zo*Rmk{99q;tYPH^qj91@3v~f@1!rQ8cVEp9bp<#SBm{1hYnhEgKesM$znUlIUx|Q3 zKq4R!_yZ9rj1Ry^KO98?wJMVq-$!A}3^>`EqQ0^SC%1o(Z+<<6h~tOw&5zUZ-s7W{ z4My&V) zn+sKGKGbeLcWS{`@5sap6k+ zVAt)#U7F{b{?=!mzTdwV}AAB25utH;_dB`*!swD z%~REx_`eS~C`vu6o<6wiACF?jFi)Nc(?QPS2(0<$OZZa=A(9pc`V_*zDHC9_=_pF6 zxI48!0*m@jQcm!Q`-FSG_>^cw^BSC|;g;c!;#XwQlJ>wa2Q^APmsxoKKX<^_#R>+r z%)c)#5U)J992qI02tFN-(S2sZf^31nTa;UxO@%6YVd;m@VhsD&h}-Ry(0y3;(a&@v znT6wrw$j4>1$bF#Bns#EuA#t*Cq5fZaML4?KBs<~&lD=*x3 z-~G6CdVj@Tc}Z3R)~@^>+mFRy)qjuT=52Q?3YoIx(^#`@FA}KKvvZt?7vH*vmiV?X zY@{M4J_PT-_zB`uPhsWWLFnJh8Fr=yP}LM-*Y2Izelm>;95v=X@iOk6(jPVyLRL@- zz4MdjvF2x>z<@=4q=ZxpPk zxM@@y!Nt)S1u<=()3fo$V^2Lrmtcu_>DHx)weiI*4?K)1gB@XLWC1H7)Z&SR5%w;K zN-aT3z^}--XDB?VkJqN6oMfCO?+8w)MRWQ$3yQVZyYgP26O6akAEVHV*4x7-;gJXK z#vpG;=2efB@MBoMX{Uqz%fYGRs{S&pf9y@*E^4Wp}F0~FmE zi}hcBialZZII?~_k`|3ZU*+UTJhM%aGB&STk8OeHaOBsIuxV0nJTO8C zpGbQ;>O{0pZQ6(^Qv( z|6BSwLNZG^=lbLR2kynxVdAu|8hI&H9;{i9Llksw|MX2v8?+9iUG(8Kb~c_LGzn>Y z-p8X~oq^|op?Ko)htQKkSWDX$;g(V+>0m=c0jvkk#^X=lMumkHg`JfM+P?)Kuh@?C zxZ~IvG7ketk436T566$&cOr^PJu{0@c=`PYF~E(k^hlp_lg{G(Xa9$otTR}<_Za;8 z_JpgIaLdVm#DHgoD44Fopv8~kxtqp9bZxzX1v~^1lSv|Q4Ieeuj&|G<)uxSNP{Co&MYCGz4!|>b(KP!eoy@rj&Ll54M{^Vu!)HR4ZwIAQD zTuURbFueWAA#C}05jjSlm5V=vW5H1h(vu!ohR5d)fGtm)*OujB|H}Vh`Q8ld4LFNQ zW5-}*7=k8 z{M+>iOH9Op{mJm?H5D%}yAQpc%~3;FemmE^ja3J;a55kiNwdep!Pl02XaQ;BCemRl z_27ah-d%<%eR`0O5q+A9E$_dH4d==bbaEf2%^OAoDUIwUiagd&Cu0>J>&Ay4!|iiM zk%u6Um6w8_Kl={9osFS~gw?s(ijxP>-XH_%mlg9&c<=f82s%)wOt>LV{>K7ODV zh&@3m2>oRh!f&5IZwI2u8XjNUcy~mfp4Zq{H8HA9Ic_$Q^O>gZJR(IU}efG(;5@GCRIoi=U5%;`o6z zSUYPho}TE+OdE0P^A#GUo`DS}&%YB7u>C#69)bI%BQ$thL1Vax{hJX+W$c6=3S&&Z zShM#j$<06yuSr<;;@#-yWP<9d2K4l{2Iv(_vtKEpQm+JK?|TVP%^RwW$cHos4u4a&KKZFQQwTi#+DDi(%uP8dpH1HV%@;mLT4^5k z9uLOYF$2+$yq${1StpO~#?~gK-tEu-2RDwPAwPN0^1=*kdhcCqCa*72Q_0Hb%lVUg zbPd)}sVDZ1qi5cT`|q9$FDLFDYb)q!;8%RR`d1`|?ZTGu+wu5F4Zl;HaRwi)^H-F5 zE>xsG^3>B9?&Cz)?hRN%MfO+keTs9<@<>gsMCgHuQwC$>v9rjnppu+?=8(B9<3zZ& zPd*nyr5?9FlV)HT4Jg(174-OU7(34hr5-Tn@wf26LDXeLX=cZ=?9y9RiE4OjK#CcR#j?jG@@X>mI%21=Rf0I8ZzHtj^f5}u2 z|1aK0V00d7tvCMh-2GTE*^eF%RLDy`kL6#j!Rg2(>|gg07S8ydN=bb*6hz^*&o(2H zNA2M$EVk z|9ON0KU!I5CMRQKKnz(JeGHvClPn1j<&~BoB(cej2ft@>@v9Qd3IzrHkCQusQPEUR zO->RbO2s!FIMa=ReJB3j|DV090I%X$+wY0HyDOHE0156;Tw0(4Eu{i2g;Kq>+uq*) z_Ev7aRH#wvNDIZC;u>6I#D%!K|L^Qc$T`UY5*nz@JWq1=?3taNeRp@}o6p|H>heP( zAQ6xVNCbWt1gf#;^S2bEUeKuNcx?6z8WlXqFJ%W;uK-M0@FMQ=@u#haX?Bi$&opjy ziBKJ^FNKtx6n;R;N2u4r=^oKn}jU zo__;(3=e^~hb#O7{Tsc-n`^1`Zo#K-+>4L^Pi0>7@b88P{`FrB;$&0DXq(FN^2_sD z^HI9DjD|jo*Jh7`zn3$))Ep7ey+2-j?I~qmD<{`TmEfLWsV*(Vg+n`#Lw8lZp0n}E zlM~U!&z-z*#KJoecRl+Ko(=67QQgpa5Z-<5Y1}$_A|879VGQZ%ukei6Iyj;CEn_jL zM+gmKoOdIVIM1p}`pGRgEz&L-G4<)UZ#|3K2X%#qvjh38+|hgdgZTG& zYvnnr;{7YDYLrUqlCkvTJybZUFy{W*n144@zPdXx(#He_JXqyT5J%$IzGFp!~2p*8GVZ>qfz39dCTIIP%@Zq*prAxUNkK$}> zv|=1{y5%`Mb=M&Hi1YB?dNJMXLr=eq(O#{wYlKPXN7~ta3T{E6opCD_VqWB=w+t)* z06+jqL_t*T5_~^i?DvEBJ&1oi^E4KHG+rHL)xiF5If6(=G(yh-FMs(mM)nMZ8~M>3 zD1U_vo}DA{A&{SboRI{I}I`9)~Q%9`!;m*_kg>bC%lN0p^nF1MgBT_auYg} zql2~)7V;@m>Fnf0T&+!&_e*8ugi+%=!-&sSaX}pR$7pI_Xh6QeR7VwL8wwe$c!#&8 zhb`+EIx)rUn=jzD38Qd76}7kY4uli=CT)pPPo|b1+P9B#53-LY(G{*`(L+$@$#`M@ zcm#OSn96r4Fti_L-ZKlG_&C&1(JVM3MX4*igso?zkVR#jf#Dc@^wQlNpEy?9*}}!c z5998C0nbitbZ%>Tnk#&-PCJ5EcF?G&vcROtx8dH~2gBFRQHgZ(3GR)@sqE<-K+o&i zd~E(`4RY08xk77g+)d+F=i8sf<980{nCPxVP5ZP+v)rS5!0DDGGf!-RE%VFHZUUJA>UDFz9*(M z!fO>yD^?B;c>Fb{Dhu}In9p^IyC=p?oxunsKUS@zaaL_IrNs}Y_8h~RGZ|>mv&VnF zdyQ#UJCjd{?~=1Cg8Glge?NYV?-DbIY}tnMDb4GargQ2IS%A-;y$eABUhwqtf}blX z3v?(+o9fF?iNNoPfWDI#hy;omjspf^0*x%*E_SSM4NqRjqmRwTy^lPD zj~BiGHw~u|ahz6BIAzJTnLM-hw&rRMPp0c!^4>lg^#DK0V2?aL zi;+B@${6p=F@F5rvoK}kV45VVk-FvIIF_ZB9ktGLg)bJ}$Kk12Q_#hS{GuE;LxzsU ztUe)1gq(q~3GRI1DW=W#R^(mJfX=vc&OErYtt!D+Ncme7LkmZYoyn-okpu7uqkrxb zGPE0IKt2ba{yp&UGjmvrkzA#vOwmVg2~p}&@Z(3j6&Y(Z<&ydLj91H8lzp0HEN2&6 z@|%_+VwYORQXeSHSSE}voWy%HN|8Hkcuc2|ez@<}2?!SUzw$&z6t$jJn~vB1v}Een z+c9_gQ1)3p$Au0Vg&CvA!Hv&H6|;RXBcM`h3ls3!KBkGLLE6W23f}m~Z3yypQ`RMJ zOj|zvzS)>Pu^UvCh4^sc9=eWQN2yE4l2^8(q}~W4?wo=-v&PfWoiYID9#3YCnE$W& z2<+;`hRbpAtEI?NmOlFE{lv5Q&l6KIX6PMwin_NT-cP}+=j85-F%!mOKq!q-RcXYf zq*bR3j9l@r4_?NoUV-p%_dsBv4-IbG)$|5FiFfCXLx8vXIq?tbgV#QIQE}9%pm9|2 z!L_ZZF!;r9@$iIR@N#wJ`{;u1gQnuGm&Q;wtEUWJu70-ISVdt5j;`5C?nxCwXT5|c zrwxR^r;8#}dv^-ObDzC~0Ub0yIuDqGH(q=MlZFrC`*lCUJ2$Q!xlR~5eiA}^2eU_9 zJH|Av9qCX-2ZiK}LPiT(V&W68Vq*VpbT+eB)+6B~#^bT49)f!dkwWMnJ$m#*H%luT zE^83CaX&M(G$U7Dnt@|G z(cJlBz4-dU-Ymh6E^MptHL-#y>a@( zFtc^WtY=@v_yKAh?71%MHDWU6%$x&nu90((e}%0H;yI~ffX5+3U5H10g9;;`{}gwR z2!)4OFL6xo#FVx#e()bf*44ZYx!o}c=vX#&OvADjiGW1luZzIMTe`u{%scgPA`$ zmMp2@$j(DLd4NiVe93e-ziv7OeS2?=qd*kkNspuW?TE}1vnIp7&zwaWNozr6#gyLO z+$W1fKq4R!kO=(I5NIf4)Xy>Y%G?XRLwdu`h|VCTWg36_82``!rQ79I1toKk$yLfU{uf5O7PsN5YDce zMc>d3{S=SOTqKd)U3j6lrOdx?V5>+VGuKc|;QAYxDZHja&RSf)7v~`IbUL@}@${^L zieXK=eM5{~@c89uH|^ir`HktPx(%9yf4%q*4C`Wxn(AtlGVOJK9{I{r)0obZX+@PL z4b^?8GCS&U0;8j9U}fQt*;7KAqwlw*a#q^9N$Y&bAv}78QN*Nw? zrQ$jg1L#fLvh*G|L)leOMDO(+^>b7j%@l^*YwXq&eZvO8x+yPE; zH+Xe^7^48HY3MpmLtiZ%tUF`k0B?-eZLF9QdZ9@ljRM9g;VBax#>E#zOF2qrJm&!_tQNVX8JuMA+ zg~i(Ml139MDz>{#9R@os<(eLxY3vCI5d%ar`HGGZ zySM#W=h$<4*E!|1UY=gKeNy*UBr>?vhF)j0~LTaHVe* zhG)m?a5ymQip9#@u^4wxU?v7t1LF5@;N`2}KcF}I^$2J+pLh&?1gn-W$Lg;?#=YTG zR#)ZX*ON4>Fht0O4ie#Rj2zRN*J{yiU=Z8`#8?VkOcOJp9?WbkU|=i;yGVTZ6ZyuH zGbl4wD2_KqW?r~w?o1lgJQPP`ah!pL4b1tOb0`8^x9vv)9h{2jlvT^g)}~W9=G{+D zE?<{c(29>`Q6Hg^oikV)kyDN`#Dckl&5ndQiq1NPUs^+-~3&P8n+nHm>{*uPvaA zB^qcf#x_<68Z+c_8cOwU6`Z`?VDGNJr%iRQ^=ay8gDxY&V4-z-BhIrvaPbz`9*f9K zwTUh-;R4DF)lNXup1&0ayhhFEs#@OyL0x;oO#~$7UqVikOmVGFX#8Hr2-IEtZsz8P z5Jth7GFnyh9Hsism^08<35u((Wtzou^+}9u+%RFrV|eYg8C;{(p}LZsjfF*|#>qlj z`X!Wdophz57kC$XxM%D#oAG2NIt}1@=dGdajkxtx#O-;uH40lXH$40K1FXrt2W|^7T;%Khh{>>EwlJbDzUovxlIb#<(gPQ;V5SJSRJo8PZZ4Uqz9f zM8sig7q_9k;O}Ouy%RPVen%(nt3z>R60!u}0436dL!0?Y<74-37&y2GT8i8*eeTBB z-~5R6%fG{dflj0`Ex>_u<%;p!GVo3e^)=LfUQ4Ut-8~S2z0^2FrEn~$Ek`Urx$L9o z)U9}Iq!>`Ni@iq|+%i5ud#C)?AqZUkP4AFX$VMar5`n)50>-cwGFMVP<$Edugoco5 zZcfMZlY*(eQx_!-LMtqc958&aJ=U$wL~#}WDk(HuxZ?JSKG?IIoD|G0o>M#m#%1TR zD;==wY=^<4MxZ+OEaFn)u=`X7`p^n^{Co!Tib-VaGY+Ayx>UQZ&MbFG1SA3y0g1p( zMxZ7)2~pylTs$4!wGJf-w%UsoDr_p!cPqWBV&qDGN%Nw$u7bvj)8`NsM?+L;5eo7P zP@GqUG&++cq^TF>SG=6-Scr>7l)1S&wNPzcU8A!8if6d$4=R-jNXx`boi)nhO>py7 z@p=@!6AfqHj6%{?%se|In7Wzcx_)fuzB(?t;?9$|Av%%92YP^#$E27BiHx*F#3yAa z(O{R=N_7?Fb*Mr$6+9M>y~)GS`eb_Ko$#};RTN<5Eq-T>IXJ{@sq^ zaAZ7Ie(*o05$cH^UH#y|C_FFU0Q3y$40}rx<(jI=`B;%JOc7M5oZN~<MROxdIU zS5cggQ&Dt?V#zLIL@JdE1w|Q(0mHd}IF(q2>WVG?TQbr~$K%kV*6;V#z%CFE4Rphc zo0E_lcN_`1gVCL2dgb(3-Wf@Sqg?=cbP0f|unJvgXU z$4{I_Mn31Wf&y~3QSKtoR_ui+CLPx(oMQ_aVJzk{r#`*lYN2}_u040u;9`W322B~k z>P{{?^?a?-Dqm1z*lTe9*3ud3Vl6f96reJrcH>j>H8DG{Mp2PtGX^P{>fWV^+~YJm zdG#5D@UDB2a^w;=e*O+lZ0&-u&_G59Ix>Pb5Wyi`;A(HC7^p;JcHX@)Cd3!3&nDu> zMgPNrJ$=xXAYT;S#7kI){S;bg6&zZyMh%Vxk<4`9b+e5Lg>{3r@1<(J|;`jDk?@88jC5vr7uT)4f4{H$YmXg?7{-d z2!$x5++CQPi3{h2G^bti;?**imzNy^e09rMw#>%Ro?>j}z{C;a7_FzSJxyG$Ys7ih zfc$8ph%8f7QX!WPO%)~TaeL3oB}f)rW?IlO5-Bbg>58D6$mla3)=s~mg5+p%N>ni| zXOskrz zWLr?0?zn#{Q<#fMFuRuPm0E2mu5uP#5~Iq5ZHZ&ZFW%7T99f>HmNRX;dBRD{_}tvM zd&p4AnMay;{Dv)^YscIoq^8CrQCWmFH(p;^ip=UV#lG!vO9-r5yeFcjnVl1C$=|C7 zwOr7Av_68y-vQSp95XZW$*&%Zf!@97u)qu&E4Q(lA$ktz3Qspp38S7o#2Gk!;v^C? zi%>$NZ6V*YLh?Z;kkd1@x$!n{Sy)&ob?Z4U*EMn?#~&BZp8?z)JmBM^Ue{_h$cz}a zPR+8O;Hpd{4&n_dNj!khK22#c*3t)nDMzEpuPjP3Ih5;bjFDSjq-3hF8_^whI=;G^(3O@_%I|H2DcpL@YgXmS9 zp(s-N-9EaN2lAErDfdeRBmxoviNMW9pi#lg=$Xh<`}lX|^DE@+A{>dOwm2D|ytW93 zFUB)PN`A9Lv7r%DBr)ed>ol7Hl|%lp|Ag{J%#n1~R7bJcn8CzI>}_5242<9)^ha7s zhSv42xu>=W8{dAL9E4oRre-O84$3i%4P#oo3T$6Z^jrf<5rk&{s1WnX^4!@L^r1F zJ+*$Bq898Lr@qGO2xjeA-O5;3+kzDwmXhp{TA zsj}|RHU@UcNcsAu}w)hqDrj)QarilgJLMyISi;7z@7ZmUVZY3?vkN26#Q za-~i6tIeoC8t_^gnsgr{UPSRiHXuLkIM(p*s)Y_&qL|RR_`&&)!_jLS)@|C2jQF$I z$Ujl6?cCuP+zS)$yaVIHJQcae)W#k6KC=K;zpTdEUymc{!cpZW&7I-=;)@kRXIup;{0IL4pYz7ax7y>e;0p-Zcc zMqKJj^KoYHR#1g?#UdR4n&y$>#0b3l;c{GzjYE2Vv(t!yA>}@%MZM8Q+(8|8`MOSf zS>kAXEKJOep>L@B88KuQ5Ak+s4n5a%(%Ryr#whd!NDA)M4v})Sr0f?L}Jyk9Kg~WUAmIr)4cInZm2{0=|lMO$CcQ3 zE{4(m8iJ#lJ^ZX1A4*-FXy-olJ*0f2XQDIJee33$##%A%oWh#bS6Qs{;0DD=-rS0_ z6W1X+ir^hKHa6Fmv2@gvyV{Sy4IClc-x+y?L_i|&7ej#Zv?BLc*P@c4?qa%XR^v5Y zD?NfQs5MT8%>857DA}9)kgV5rAvV$zBt1DEX<{X9Xbh`vec^0ohTi={VX*NW3GFxI zT#N~FQxn;sEAAZb+tjf9ln6)!BmxqFKNc(_ox*$f3!=!Qh&r{D~r|= z@>nzxA+)GmTV9F;5eOojm48FIwC8htu;~!WI4?VO>4~Wm$D?PE4;PpgFfp#cjzwQ! z$@Yk5<0usiRqUToq1ESPDjt}vmbmof&LqDM1MPJbJ@}27h5#D&;*UlnIVBrjd1vu! zRyB+~yE2tc7g#jqacro<@x8n7(~2#~Vw8}PQ8?y4Jr2SCIKr6q z1x=cwQl!&SG55Dpzi>fcP{}BWcG^umy7t4|XM!-}L8ko6O2*lf=dgPhl|;#@IGUA# zxQj+uy5Uinu>CsTA5rdn;Vn!HcA~@dl}~2l=B3nCw1(WOWqL57Tn% z?2x(t2u$_=06!$e(9`}L%6in|`*rjzHXx^+-vD^oweXg2W4oeS=7DeU_WA?l5MmPD zfFRs6aT3D2`@+G(5+)`!*#F(v_+s^56t$zx)>Rvnv}fvDM(=ip0Cv(5AyCQmcqMry zJiOK0*}6&YQ9l79f;Ed=mE5626Hv%hd*Xf>lB?SK3I;{3#_Ot(&S+rKiw-G#cPbhm zm)e3-#c8i~glHYIlaf%+aZ^xg8&k>IF-4qM`&ku{taVgyi*nnO|A!g9eQEU6WffGa zz}m$_y&?Zg1pcH56vrRN_Z!yZFnOm8$Xz*V)+~$-XH*-b+0D#MP{;_5*Pr?j7d5Z1 zaeAq(scQ3hugsC0DMgffoA_GAoXxRGMMK!Id9PsZAa|w-Rkxzqm8NYgrnyrWraSJ` z2eak{VbWa>AeWg0qRySd_AMK6DkUAKm>xWq1DK1$9Q5(AVx~`hcm(#q?8gH!?Y<(` z&&2t&C$VkkPMnWR!Li&FBqkTZ;rox!O&uK5bjo&p)^Y3<<4ql}DX&CC7q)J!wxkfT zHR|z4Wo@R@n9#11wxe=sRw~wi`8oC`lu_of!o>ULVrb6*c#v<*)Z7$?8HX|9k@vJ( zyewleLaR+Ome$d~$Cr9#9$|13O90;f@_BgC#@g)c)Ur>e@M*2gYFVk(_Zp_&6vZu| z3m$pwNd(v#@=;b!+N!RBkujVdXw0q4#TRdWg*|8DSjQ3};ZrbmT3`5kIKzrYvD%_! zeDU!o*nUzSHPx!2KlqN);&P@kZyY#U^tCE43mM$k97EQkLYuyF?YK~^9b=TWW2YWN z$t^q*J-Bu>;kv@Kwg8`h_8B(rRY#dBO>(|xK7C~~C#!&;KFYS{zk15YB~|KZHqHIY zww@_Q&X`QY-eMe&q4QS;qg+pIP-6Q$yj{_Ye0WXCB5F!OKdE0dqN8XQPV9HIqP(+Fn$8sC`D7>eMre;QToXvrJQFYwinMQ?=)giprOMBSNroX)GcR zY(oLl0f{t4{rh)Pyy;wmdXji{1*!S6v2NV~#HZH6AZP^o+FwIS*K}g}DG`teNCYGT zHyr^ZCqH!IFdbc3Nv;lk`1t#^isr7brLl->J)x}C7i3ShIoN+Hj|(%VIq5SMumA56 zxN8Kd)R9BL@CwlP+F%!tfZ(+QR*0(o5Rf562Z=< zSS4;uMm$X2X2P!}IR%w7)xwjV%0Q7)QKhP(;m90zly{1=&N8KG9n)oMUr>Ej1=4cT zkgIJ&xatK8ew!L{{plH5kdw`tf8OZcdkCh_x(6G-_!J+l*n#|@MqxfV~~=F{pP34?kK(}1Z+x}$j3H`Kz8sU4jtrloFXTA&4!(cQRZ6hS@83QMRM z&{q_D#thdKHaq1qc!~AeA|9`&ft*W>0MkbKs@K^eKk8Xv*j?kW^o=cuxR{DT2QT3y zA0aCbdxQ=RMhn`XcAT;n7a~q7Mm>*CJ@C@&uOi%2LmA7|tp@bu{>`r}M*=G+D(_rr z(dRpM@?0JU4Yk)sQ5c7_GRd>k*0UMFlS)}^OG6Q5=akHHc>1=uzijhTe6pgLH#Iha zyOY{rDDJUR7|AE1`7}!dqc&{o^_0wH&G7`9HQKptXpJ5LCRj^QTLa9Eg5YV>TBRYLxsG`5jF>~&#@UB$F&$cJ9bTC3-TGbP7$A5{Rg8c( zU=*P&5`jN20_0jvB(H6D0Y`5GJo)}NxU+}0Yf4>~RkIV2xKI&s=Hv=z&XZ}|P9tI7 zt?1P%m2*9L%t}ywdG0WTU;nP4l7L)z@knO~plyp3aUx=@SmdD`QP(b5u~l($qDm^ZZQFJ#wrxA9*fvjWI~Cik*mgSa_u_uHyYIMt zNB`SroUzy5YtDte=A6&-FnMd)i?B@N3jX?>u}xoE?)0F{urvo$%&`U5a~;aL%7F(z z4xS@%2h`*84P-7Y=2KH(KR=6@uZ-w&>r4kxZm+9cZdGGdbZ7 zHE4YpdDlBVgc1x#1Syr+6LFyGY=AZZx4FDL9*0Qz>Q67dta%@@&@9Ekz#lV0mUMI0 z4A0et#01Y(Qo34ch=0>0iFZ}<94auEKvin3Unx5HVw(Ia`>TuBSr3O36E3@d5)(Z* z2v-t!JAvKRRea@e99n%bfv<@!okoE9Gs@$oWv}&fr?srJQLErqZy2iwetWKj$?l4M zwr&r_w=Ae5Y4t)8!AX@eFQLqiou#oZ2w249B}0SUi(jNO5`3GPNApBUu`vg&NwY`- zutdPhswig(tVklRe2O|aixl2uBG#vyO)*QfC}y3K5O4e8f}`@+gbQWb=l7a>Z6)o0 zt)bO1eiV?hC&QHJ>~432h?%m5b(IoaJzrYVT_tQZ!#C2httP5=Fem=SM^5O$ZcA|> zcXH(e{&?+n{;gBd6P!fr-7{1xi%3j7)C!Z@d#{N2itJxGU|2;`!a`A&FfBKpV4ADAA9%UEj$Ps%Y{LtU zQybMQQ&BBOq{aWU4rWlWLqmmuK><`yz9F@2x+z&^C9)M?`Vfc`0QdQD_}P>tA?RMP z=HV>^p*VToZ)#gT?@goJqvklHHq;8NJ6Mf)57D&8DBfte3knp4mA0sjn<_0vr5L4| zHBZl-%hjjUjZ#p=2((qZFy*W^nR@b|1ucjrILJ?0eWXGIqul0108~8XUOruObu{jL zzl*BcnX_hIr)Ehe?C35R9rw3X(`%?D$Y-X);d068?$cOpUi(20=sb6q)tc5UFrc=G5;dxt`vJX~e0HkZ%uc&lULtlNX?eO4M>jPQn+B)0Csyfv_hDy+KVGwVu-#<}f+`taD{8>e4d zdb7~lyd$F8W`&g19~c~pRR5JRcY<+I{nc0LNCiA};ynBojqPV7G<&__cAF)4wGn|J zPGcLdqHH;)-WrGmXxftSSfb)pm#O-x!J2$f#t5V z!u=pm7?4wfCmnsSUIQ}vY&}O;>Zu;eNZHF08J@xS(vFjtJ;N8TmAor#0rNBBKv5?- z_@P790*%)4frLyj)~Nm`Hw977168h z_?L82^X?3S2c^`eFXywcgY1>{igU6+7qm5}7KVardCzn8$bBq4OEF@qAPCO>fJ?W* zBJ?MFEqFYcj7w!584B6+tw6_;+4RZTlewM|{C8Va*`Ty9REOzPQA;8qzW9i>I2J=E z&Mv~D6g9+J4BVsHt@(5erO}8>w$bAK#RdF&YWlu)?tVUC88NQ0#NF@S-Z7E5ym>hQyLz_D;g`MXqT$NpDatUKmAktKG%okI zhauIYl@&k+<4qhI5Z|&XUEfUIG?g*f=&iN?$^2Y3Gpa=GG^Y|PRWomC8s-QmKBS&P z?CKH|j=@@+{S1qg3=PKA<*aA)#|EZ~PHGIER%!qP5WK11RDbh}$q7BF)8p!R6Ji;N z8Ey|%>ySk-gm?rahDvKim)&ncTPc@qX^rK4%!@`Gz-st+$0>93>L*4^nCV zqLU}uw~@UcX_~hY!Rxj@KE2SlwSAinRz>NJjd+jYmi7vbx8$xl(6WSA@%0OGFoM_LbaD;IMrrJJ2q9K#6XUPN+xG<}4KUL_3tr z&Ih8Kl4$-!Qz~h{#p{Sz*iR$b5ZIC z+ZBHyY3IdC4D-`_-Jg_Nl3%$~#r@=lun-TFH$KsY^w(vLrzxs?3 zo%~nESF{T+gKA4z;6)U~QVm96@GRg*UOoXjV47I<>XfNFA?9-Tm`ZSWEPESpJ z_LYm{0Lw?W3lYW~066cv-j2G^+zT%&n5JYpNtA4N=A--TcH*~_Fx*q~H(Jf2afg7z zT<|rB=+~OhS%h?tXN`|vKp8Q8tu1tA)JZ&sC(h|&&2X|1u=a{lAcyFEdo)Z(jHqV@%~)EX)#wnhQmd(Xp=hP$HSaU7i#@vu4rLs} zz(5Fp$g$(TajpP$)O-@p((bp|$~67Kfpv{=r{)WMAKp9L_IN)~z41&-86DGbj2wkX zDF%-0NA1Dsc@%V6U}HSN8x{wha>|bDmj9LYust^qf}U?JvilsFk=@iJz1j@2Te9QM z)G3NUfp4@1w1-?OI8uS8Hn#_Kh7XVb?29xtu&OtesMoGAi;`)Fdy(mH>2A#41q5bs z>bw#QA(sWxqIG0|@&3>3*>9*p+=kjUfBbRTat!8Gc!(V*kn!!#h5cXl_ZW>N?dhd@ zij(+y8@$NRWX`9H#K_cfICiq3#0;0__=q_!xOrc%wALLD?VMbi#b^r3k*sw>^4pBGDZ z?2kJjfaQ*OMpg8S%uRA55m@X~u|o8u-#Wo7$ed-y)ChCtkwZ3*|~TMM8$7H`1mO16}0w0~v;x7K9OMp%IWU2zGbITq2%cUYR+?(+kZNr9`8FSszVRN)w4u7%>eM8&nV&C?e6>} zIAqK)GjchC<0Iaq zol~RlXp`oYji_2U*no+kkAJG-Pb1_>|$BeLv$ zv(ZxzuN`xh=-mvSI?yZ01@>^uMdQW{0lY9>3k<&XA3-fAZcxakEc(wpY;xa6xJ(X5 zaJ@oOqsT~>{>F;orytW zD`<3tjHAo`U{H=gP@Qz}Ge|VkVxok+8$tCb9v9GH=h@n&sLb`>|CMy;CIFr6ZQl0B zW^nt=pkeQ_h2yBD?dhLX3~!K?A|un#_{l`KqW==;+Bc`2s%i$m|M2MfCD~eZ`*e^1 zZ{_Nk&)AI7npP%0rNrg?=A-76Wm;^wPWFos-@6d{)7_VX(V^gaJa#qW1v_>^>>*hW zSRcGmmp&F^1m$8dH%bkE!Eld*H!akYnuaA~y#DaVsq*Co4zO}ECc;eY^N*>qI}nc{avbbRWaNLfCa z=zPNnVm&9A%cwx}VKgLf$Ilt)+xdsLkS-XH`_t%LolrY^%J1X66FoqhJG3JEazEI@ zyUdtqzhj06qIDddm?AD&gQbuhWL#%}i!_*SVtlWse6GIT+I#F?vaCg#;MO1X+s|tx zQAFNo>;0wYzn;6ba}Z6W+@yTC$H%Q>3_HzJecn9QNvBIp!2bC(CKOzh@bhEi<+fd} z%*}4phka_QU6!Kcqkgs)+_41Wq$n^@E}3UhFyZ^+F;J!K>_{Od-QLkex3{ZjvgN8hbeg9-7MDKlS!1ty#Jk60?UEBEs@h?4HC}&e& z;D^5Go$ghixJr|VAx&Ib{x+G1amR;6OCz=4mt5K(T!HKs2pXv3C^{ay{}G>?Q6ih` z`6XuSu!!QJkcxf+;uGEN;oXWV{j|Y@cj&qTpVRGr)w(qpl zpO63)5wR0lU{arZ^dF8NZkK`N6EO)m!JWWi;yf_A+a#4U6~jL$M8&7;l+l=ikLB^G zJ3f8qNgTHLC^XVS>LR}A3A$`HoGY@be7~RqygD;cyAur5$E8*;*1<6Q-YQiv3XagA z(ol(%+DC{W|uo zz0v={s*r}?=!@H`ZANDN$uvSiynn!z;|9spmq|-tegudbz95J=?B0l!in`KiBSCm! z5%eEyXcT3=vU3rYRaJ#5irzJ!Sijzfn<_YYKCC4s!XNMc$P_gcE8Z?4cB9>z%QRHI z=?^d^5aFBX__J=gWeNJCVbhD^AYZKoOr*G%wwo-5!;tMnG_DNrgJs?9yy^j)v)}F3 z%O`v$@A~)wxyIJs8)RfgG*?vgoA#S%H5Q9GW1D;F@VCG6&vHMmN`&|!LS=VaiZaON zC@^-eUeLw4of851A*HnAV$y;kyQM_M*fOCA-e09wIfdC=i&;_wO*y_jX~d!IhxQlS zT)&f*K!xD6qB0$*UYzXZ4-nDkE@U%3`SK-sdp>Rz)bFiHC(m4s%YU6f&oaghCr01f z4SL7&Kz9#0wVU7@JRN@W1ej*O0>Uw4jYji-S-IMi*s9<;l&X!4pc(~YoA5qE2Uw@! z$E@2mM-hbUA?5TRH#oiBEs#Z#I2P<7J43i*Gi%ykFgy2b$M+-e8~4AwK&P?d_RDof zZD1AvcPr%V-6Bz=5OCy29_Wk@sm#S~Gd_`=n!B4Q7X~6kJF>m?eOxdn&sHMPCdtH+@Gx|0dw+@#7Q;JMx=1v$$^8sD)-Ss zF={{HX;-uq#77yNodE;0!43<%bluxNNOCG?NAxtYFQuf&rOpm^D3)7N@rZJVZWc+} z8$KrkdhW@!U~P~a8JjqqO{(n|X&v&~YYfdU49RSWai-$WnQYq^t9d^jI4Vc$6d97~%~`1EN1Uog193bwtQ&pwybePM_2X z7BV#&m4)Rtc$nv!+FGq7@&`vv$fIV-T8dfJ*nBZ5({% z^m@p8GPvXtm*9)7;Tzpwok0na=JIW9ENHbPKj7m9Bh!6}GLl5=diO$cv9b#+!+#D! z#R8h|MO5=LyD@p!T=Z2MZGs$Op9C+1p-rl{MQ0Cfg9EdzX@{h>R%$-=&p4XUW}sYm z-}i-0w&<0jY(~ql$!Re6A-v$zky<}!s)>dseZeuuHhUJ0u9sV^VDilXtZbzstV_Hr z`xYjt5C^=U?DxPE`-%A?uD`XR@ecz~EvX)eTi)3rS=F@kdsENasKu1r9;B(9rWhWa zVNl5x&Yr%~-Ji68D4npx-^yb)dWKsuRS^XyE;k|$av8)ep7g(`vbsMk){kzO4QPf`D&Mp|9FHUi3KNc{PyPzJ2+1 zz}2lq1VqzyG4yNMc17|qaFElED4f6*je65@L(R=;UGFUtD_85w%2SXJ~g^yERgAGPUR^9sed8M0=S}-N)lQk>izuVf4)Qk!U@@U zsPhfv+U1E<;vze&SRlbh)Lg3!&CX*@`QEV(F1`I&piu3A;^!jewkCo0NIZdEDk$m$ zXt4`4s&TBm@7FJndIl@`u|Th3Y?7hUl$D`)S`#RZnvX_JX z@$-|=^KN{hA4$kc7p)>x=+SLWur6Qq@4HfRyL)PS!;7h)1HvpCj5PG`1h$58&oIDG)ZJW$m16lqB;Mc4 zMl8GbRBd>!`uZ-XZ-;HKC)zfmoTvpI_9!YTO1Q6j0MfYluoVYda8k|49dGyF@19;z zy#na~na@^!r&Iu|D!kR75qmAEFm^M(sv_P$$OrZhb*nEV z-%RM-&U1LXGoL3Ycq2S3g1e-49xo&@A1z(*&Z4t84 zAZet~_V$h9I(`-Q3r)xxEsl>TK9(#vN6cA7A*9fW8wssLOzbIph+_4dHqCDc7jWq4nhOcm$G#eb3J^oI-AaEac0gv@_#v3ilQvp$~oY2zwR2RI|VeHjdGZgj& zZlqLnR>P?$+lOZ}osave^BKxz7}i{3en62YCinej2DH{EvF_V1u-M2}F93jZ@7b3( z5a|6zJB7EapCaphedDX!pMKdr(5xl9v-`J|;Oc@2gpT_i*&|M=$(?gCbZOiC=_r(+ za=p7RB+$uYL~UF!AJ>QHXqwTM7825vB?iIC;dqwd(rbSNIyLyoP>OjMXV^+%Yq|vv z|8>brRM)j&?)S?^1iy%M*sUylwLC0vBsqucGe~McU`;-jFhP9w5>VDq{E;WgWUuvjr zohaIc_Hu4{nkV@r?t0MCJ-Uhc5X{?=_ZDtbxCH<7Y9+tuk7iuddU0qrHsFQc`6z*D zn6YnCvVd)A*@|HcHPoa@%B+$E2R=hqAO%gj<i(We^M#e-suEJU&KB?Q<3e4Wo>W zjQ|n3VV&EL>El&QGdy<496E+UE!*zcPKK4RLa+pK(kM~y?VPFJqkW9xwWw)bu5$Pt zE-y;ekE2*QzBlC4*rFfF3c>lQEA0AT^Mq7y!@^6nKN|uM-wB<`lg)_?>DC430d_l@ zixjF+P_f81Dtqsu56*b;EhaL2$=Z=eT*DZS&hJ}(&DjYXmY zk*?H4zZG}j)mir@e>1w?-l=maMZ2gj?+DK?_H2*ednTBs(ZFO1eGW+98^>?SKKJN8 zo0HeArRE4AEv~S=nPQmwZgPClw@3XnUf^A?i*S5iH?&=wc%k64#>`1jpRX7Jl1G(a z@0(VOJT_|pU5d52Ic6^;*I6>*Q5q2Lu8H?u|eTAvMgbe6FVJJ9Aa%%jWGw|;2 z@39ez^BLPDa4^=`Ff&vgFor32(zF#d!>d24B|mL!VtuCS7xJU_uY&jK>32FA>b5)h zIc_B%g|8%^^R!d{%>{t=7c*b-ifmzqaDN-To(gN(XR5;DwsllVeyBkhVaTN8V$o5> zd=Z=5>olR|XdTnG`++3H*>#7r8vh!KZSP)fv%j@mSH1Nd?63@xII|bucL1|{k4`o`7XIpaXF<)&v6 z8V|H5`C|LR4o@KNlHn&lAEtOzO2mEq3 zus_n8n?W8V1Z!bzn@``!n53^SbCwK#tBrH+<8XIE)pTCzWtJYr*!sGo3edC!RlBv0 zwQB{>a9Eex$b8L4P6M2zkjma7F?IBctH;p)A+ zW1cI1)a=%)kGJ+s_rZ%UwN$gZi0nL;8zh=4n6qsC9O|Jj%B(+ELHlcM^E9QwKwJX- z9Bh2$fPRyd&1eIMEv4h*`&{0o5Sv7Gr!H-8_ECa?W%I$LIQ&e@)}1)T>AZEYXlK^n zLFA#Kr!?3q^hY6+D|K?Vck#x8)9i2GfTxPMEGzLC$-R>_aQDe!!9BW8Ud5i_W~`io z?EMvERLJ5EJwx}`#26h9f^nCloL0p6og^*|4 zqn&GAKLs(JAw9Ix~%! z?OW&07J?3JOysd{Np7uslOL8=gV27RUtG*fjyIc}Y+A~!SeuRBs*n}JV>s|MHZvJI zwdsT$<*J7_I;0ITT2}9Pc`Jav-Sw2DZ86eH-utW8Enxm6ZRPXnQ`~Q}Hcq}66FgAU z)@dnLoi-N1u6xAt1yjb5m5q8jCD#x{^|~21Ww0*6q6)rEugYSHss>`raCdaBv7i@` z>o?(|;*eV6!awOcXt$p@hqHiFFt&UUzZ1Tik2w#s#N@@P4>n42Y?V;X(ypwooU$G? zH{bg9zrd8WmX=m_mE%|3Yu&MdOX}8rRruQ7A|u1nFr8*N@lu8(JpT` zN_IvxiAkmdV2z+!I(p?GzXSpFuRa$<9o!d8Gv(J?a7R zyeCu=hG{&bxr3RWwK!ja4YmX-*5P!s#!4S0rXPVlcV9 z4jW^gMUuXqr?|MhF6>Goq?C2~T(}gTM{n2A%(vu7>mDV%?JCMIoXOGN=(GHJs@zQ(4BhknT&{!|*?3P2`vfQE_jjQz<6;LtLTgJiD z^ibnlZHwT%5XJ>WjqF-OYTh)WH+>G zfor;_`Rai?(4GxO=!BEg$qbtpyHOGk&t^R$hR`lpfkp4PVqfpS(+|vNchTD_6TW-T zB&hwf%ICi@Dqn}*A;Re%mOsx4Q1({?28suTgnaMJeIEBg-3dSIejD1I%rjIzCz<~k z?Zuui!{t7oFV0)FaIj@8>f8=NguDASrr!F{%HS&A^kORf6?6WGa+P_|_!rUAzaKFb z{p}gOFC?b#u0Y{wr$+;@*@ZHUbRODSL~)Y(K%Qf6aWe9I7iF2?af1itB6`LPK^j|Q zLfOp@PNe4BBULfYyJH}+Cml))A~?>N{ZomII&hDwxg!O+Z7tG{oK-%(tB}N>;3P8lfV8$)8OBbRa?O? zNBVrNu=@Wo-ar1TLXHdRRE-L$Z^C+*x39u;;9Lp&7YoF{TKc!?|JOh5 z3jWb3Zg8cQ+JB7sKQ;faA@twWa{uYkY5y-Ymo8KQMaDl<7XP1a`o|<(CkHPyxD+L# zT1)zOhyVAl`HvgF${@aSB>wti#s8~!`hU#vf0~pH7Q}qHU14O*<$oHF|7)ZCyB)kR zKjt%M!qhs7|H%*k+phLR|HFYcXmxcu|7SPyL;WB`@Wg{Iwvzlm^Tz)lH)1*IYyH3X z@M>5jHxGXh0%C-^IyABZuQKZXv&H*7H+x}GnUItfbw(qpv|OdPZx#*A!=txXctOB4 zZsI>?-LCQHcNT$Rr2YFXIp)ftE+~`Y6X#Jycd!wqlzK$OK>+Uz&w5+m-0K|dW1r<@ zEW)0b6>f&3v2;$UcV-kPu${Aa;HsvZ^(kSCbH)2BA!i`>uY}xx9`VmnsZ9sk*kVV5 z5h-^dq$j3}38Ml29%Vn||Nl!Pfs$w$~^O7zJ zSZ~7_Z_er&X+Jcu`21G-;g`G|^1_+P;|{VVh6cuoPv*!Zba%Gz*SkTHk%r4EBxc%B zI)X*LniRT?zuD5=%_R-*1id^Vrv0gU{gb=yi5r#*7a8p=*u8+Ow_i}qwB{4ux0IO5 zfp%xY8OR>5F8$nskKkp~(dI@pKPB+BImo+8x*TZ!1Bcm&{JZ;KILZG${gy;?o8Vu6 z)C2kIzAdy_IL>sOYm;JBhYu^&zieONKSVd;#HA^Q8O?B%(o+j+ zB$NHCc0m7PpEL$z3(DMt*MDKkP14AV@eOH%e?uDY268s$p@qie2$$RPKw>Vfx3tg82k(!Vk^K4#hT)GjKMaOrLN~ACn9bgb zFdS3TW9q~kRO-l7l+mQ?=1qg(WcnM(Q?7bQ)Q8o&^NZAqm25knBCn;O1QqR8M+nS; z$+4D=6e*lkHFa|>I)o=nroq6J2lkfHKG|9YOViMf&NlVfGIv2fx;&9_09X{z;w62g zB+{R)fxG&Ss#^9%6T&00>{0y9*vm{hJg_JawwJewzTL-f4M|vtgEmt_K+~PZ>MRRo zaRp_`+jGs=#eF~z9OU`Wn}|(e2mJqit?9Xp_%^p3Hozk&qi<#v#2}zbZ+(WG6>rZ8 z1f}?iK2lRUfUAcWGCp3w(5GSD%e396sX!+mPSjM>Jx}x-vONXSw0C+IbSreaw#0y> zq2770FxSb-IJt_b-|J&mlxMmF$GuCD;pF*+V>ijuBR{2{qWtuJ9=6~Vsx?b&QGYL; z-Ak*oPsmlXXE^?Ali9c1oQxSg@zn>nkgTgUUU_};(ZjfK--9z>eg)g2T44$A(Sra1 zGje25ZNC2hzsyeXsVZ^}vDYd=S! zzw-svO18j#P)-Eg-!lhw@Erw`!@EMFc?G$h9{lq6u^GB++?6GQTUa29D4U&gq>M|@ ziwE2 z)#n$5CY%Ie&^AawI~vXE4;7D77td3{4clN!Ykxt1#Y%pA7;rdbXkns@3(gt$PhKVo z($Ved$IZ@qKGHv#_0Cz|54xW3Qa&3=Asg;r-~CDKB>Nf*QE=nx;r^kzBm z6zA#OOU*j&1b>zv=F5INccp=-=J&_w>Tq??=n$-T-LHGuB${Iy2y4F(ISOi(Kf>nM zlYtYFOQ9VwHUv{~YVi@{bS|Mn&nQ}^tZ|((a53-1c>n%)Ahw&>o`C-6#LuTu$%RMy!1wGN1dNBcrrbhuMY9Ql|ll6B1e}N zh;S_HGDfd#ZZm##Rp3k5=JHpJwZm{Vv3&{qxCnRmH6K23T_Ty>>=&ni2ZVD;<9Syh`E1OC#IlSNY9YCd%3wy9B~$H8e&e2sCiOm=oiGR zbryX(&j0GMHrPiCcPxEit8G>~lhXwelg^ZX60Dp3%<~@KpCPYP-?sXKUtFcQ2xNug zn%&`s+5y(GFfy6d=7^0Q?cJlz$$nJvw+lX3-y90PJ|U>JmYvrp$iRWhZATf>lpkRf zE*m(a$xA8CxWE7qHB5FAjHjb#c^R*uI99N>q)L=6Y{I!HjKdLU$CS1}_9PgEJK#nC zY1_xHz6-|sXsB9|K*yUHo&e47?M&0lxehvs;&~(wimcmGd21k*jR6RdGr^NQNpfOh zo$0T=kcEC|@&{(S>3U5TiLif93ms{ooUea=Y4fPuzz1_(yquc@6Y_TRvauO3=FLzF zZ#N>+idA|q+XTG&n;0>PowuCdbfw5F~W zIs#|Tt_Hu((t+Di#feY|{2tmr8*ix}B|n6Imj~G-otdh&` zW}4qaII@I@>cQ0;=4S3r76xyz2JAvvjITvd|29e;C8Y%!D8k z0=meuD4djg0lm}V51r-W@QsA;8-a6L|6vVYT zdz^Y8tc%!E&$mdjd%qG36lBo))Kn{gq^0tNbx(q2&xr2DRV@Z^AsITK63XhQgiB9NC$t}v#ksGEWmieY22n zu2X|{O~i^)-sr!L^6d!ExXX*EUhM7=`H!Xru3Q(-_*G}-{WAC2C)=4uDlly8c+Ye^ z^w$TF*@=<5FC&7&mA;Y2o|a^e-$Oi|K7y4t0S9xZXRex5CKbv!KFVmt`@60j$!%(T zd2WaOVx?#(`nTBI`3>EQDBs{&rhHTH6zyn{MLmOL3S6K;v(!!QZi4s+`$WoBA zx(+?r7fe@VpoWbyu@L|CB8wq8AWFVfEu8bM4i$;#3qB@2!sNvFL0oc!5+7?}m>d_fx6=JmnbG_J;A@3f?vHAaNSV+5~z!vbt)E67s_r@UN2AN4mn zBLiBSEE1AIKdaD6M>?X2O(%C1Er76pY6Z690~uOse4)mlR9_mHm5Es{KoJ3)p^_5c zYM?xke1i~igD$GmiH|AMPvcX5`_&Tfhf`WSeH{0!ucE)VUff?h`sd+{=*43PdXw!c zP&oE=a1e7<%LVvK%jGgEkT`jT8hD7am1#{h7O}hUG8;PH(_d~fkn{S2hsugDRbxFO z!Xd5KyFpT0FxuZdnWLS&>}+Dc=ate3?E~qB_1GiJS4Ei<=$4y5O!wV2d)BLvND)uM z+XC3nIME=8$!w!w@xZxmW#7JO(M9q_MVsCloNfo?2?_4ZrkR@kTyOkHNg?K>{BCNP zpzwop@|VS-zki_6G*^$zHkfr;{OeY*5)_J>d8GMXXf6d6#%qkdFS&|?#{&o$gZ&wD zi_5aBRwR|YPGaDfI?3+Y-yi0kto3zbBVoX!%c>#xMw9O9&y09q4h#dbDdPGTK}rLQ z%P^wA(1H@*;Vw+$fn9kwk{^sW{HiE*>q1X9)-2SN(yu4W2qa+orI*H4k~ z1e}sfGcuZkiHBlvg1fpNaNPC&0y@LMLOH}BG-bYGP5nqhA&8<#&6AD|AcqfDB8Gt+ zf{?-a6t{p6*jNFu!@!?8uPqYm8JT~o!y}pN@%&NB$aIY0qSEJ)jD!EW?Mc%Z)=?=C z+Xjfd8i?|Eu02BDC@I*xv*)b&eeV@xTuH|!WFCPL$X>KIvMHzSDX#S@OiUD9RXMe+ z^^ya0jO=l+uNrz0n3`|y|18uOlSJ*{#-pbk0SSPp9FZ6%IIP$?y$nJ&#r`q*mnc71 z0VmKO#QmCZ()3ex>1B*JHxHic!8q@0P6c{+682Q@W$P$u2D~dgkCZM9z9+QzOYYL( z*dKf0$nKA!V4Xn_Y*+xoC`CnCphWp+*EVqWJllJ+Y5B_|Z|t_UQFg~5kSGJIk{C#l zuKm@hKFdx-mp;?&Sh+zk$4}x?OW<+hoHBEcZ&ZtBUIHzfaSFx=Fl3@A* z=t6=M5JK+lr@V2*#PxHB_6k-6tc)NNFnALtROJq+uB;sZp3F_z)BoEe9mg6HY9W~eJ+s$4*l zz>75Z-4K9v(x150=UOdq{2w2I|M?nhv4JJWv9z;zC9Pa{LQdmePLvR|z*w3IOnc_F z(zFdE+xoyE_6{a&!^S52=%e%12?;bs4hVe^k+V#i_TSdDIJ19y9d<4yON}#If+F|> zZ+0oBgzfWbi146xY|Zs$)fZy4LQYO0CRA{9Wu|W!v$4i%cmNh_ zF#3g2tPG5xz38ha_>9@jXG~OEXrN%uz~>FFz4BLoSuQxbG+A79QTpQQcyGg4qX?R! zp%uYHy8dyDo4Q@2!?iPO1iR_!$P1X?-y0QMGWvVM9W^r26VfM4B44XSlr-lb{!C;= zrvLCa60G(hjm^HLH1$3AN*h2;fz0Q1!F{3if^33#fBX$AxiGU&Y<)Z$5riwp&N{zR zAPHX0XJS-ogl6LWRor?6&oyD|p)@OLEOZYsF^|3tp^&Z*kBZ9uTxqv+2+$vrpz8098B8a()q1Td$El&!+ zX`6;ogSw8cEVO7(^CPED)aE1~*4s1JaAm`l4$hrEagX834}NpsZR+x;m90mbl0^~@ zvwY=Ne5if5jqHF7ohs3HCIOM9Xu*fZQQKbXPx?(&GVJ+M=GRH8&70sCi*!g>BrZFx z%%W~_cEPj|DksdEoi&H+z2Ijjl8~#%?UULe7^op9sw#g`faa%1CI+}dA(_RgG`7*h+R@fOR%2a(+-UGcd4nlc*J zmn+B@fQyUBNn;BckB?GQr>>(qdU7>PSVQ=4p`tK6z3}taNxVuffFmb2w@;b$=C$K4 z(j6}l4aJsTdL#ts>kNc@KiTg|z?ahs92_T%5Q}RP%QHOmdCXJ8A2y%#p;cJ^ ziZDDL8-@>{6S=gr7IOWLX=T^^z>|a|3E#&rERY!`dZ(Ly#Y>_k@Q0I#FJ05A3S<{f z#6e^9(h4<2d2-CN%|)l|}NYzLT1z+-WN4pQG_%-00zjm4*QmX9VVE zhWMgwk9~DDdSbcJszEhUqJ7RzBBl!is{SEkPHWhy{muB>9$PS z4Kjmz3fkt9@}aahzl9W}P){opg!gmFB@9>i6-E{=fhAzb3YlAxsCsupK7XmaJ8JTc zj78X)ol62nst>bQsj-pqHh}0T>$jDnr%b9ENb?xFeGw|Bxt8;&53*fY0^bB@mCQ0o z9RM0YfFJ@z_VD16^z1o&jMEalFf%?uY8 zI@Py3sL2vc#a5Z;KlkwPK-Vo0YHyS4==R-8x?X}DpMVL4V{&o~Prm`I+Dp;pW=8@@ zc-&C2WLm%aG>7PkZZBBA%5_kVLx1M64)V9_@t;Sdl?G=gfjzQ4x);IkZf@^CFUlau zYfzmNN_IXRthRXJvp-36GdDt+ns^qHe;IDnbnDBtq!t)w76d@;0)FPu9UPTH=?*0% z%paI})B3#MsF9>`?PkhnOLT@6=EIuvsy_$!_)bGVp)R*S7sg)rM_r5R(^0j98m~8J z$}D(R>N>(P(iQE(-J~+KHj-CZ#`BZJ3;AvN{?>!RJ_=>KxJe#b7yNcq5r_>gj;?HE zP@ZC>rA`?&x(0u-7BsC7a>hbvDE32jpMu^e6%q2Dq;|NnEI#FGZwE4pH2Q1v-4=8} zk5K8z*^Yh?`)jxzXX9oYVY~LauAUk#hgdAQH3T@Ijmh%kp}Z76)Su{5E?hX>tA?S} zuPKAoLMqvrTIUQ(UKAv&MBfc85mlb6Kp`x_y> zUQY!RVIAjHcThl9WT)F*$iw;&U96EyFQ-N@MP3Q{Flf-Ds;{l&XMB5bbNM4FIjE+k zXjiQqV>qjpj>o;6ItMYhDwO7CdW;OdRuTTOB~85ow@+y^b2sNqOT zV7oB~_nx`jT`W4p`KO01bTp^*MIliyL=P!3VQ*Sw>O=R6c}YN2l~(mkLLerbra1PB zq;{!mD59$*Cw%axzwv0H9lH}0rt;b!#(3nt{r6S7Hi3C~n({}b0HlB@F#W{t9dB`1 z1@r-yY4&>XQi5IQ$IudW7P^Su1hjfJUuL-4C0sAzk}9c0!%+P|YrUI;8bid`slw=W zS(PHSG!%qK+`AJTuUje8~ZUB81>6(nG2Ns|6Aw$3p+ z)1Y0m9ox1#wr$&X-q?0JwmPk#&B4GidYW zzSIEEk%(m-Yibm5Sw_T-?^W7HQxZwqaP>KB3F>~=;k%z8ixEc(s#M&qQF~~}!(hh| z_2Y8NNQ=4N{sX0rLbX8^ZoK9s~|}? zGxW8^UCQc(rJ7xEGwZ{QboiTve&R3o7wwOH=~sqwz2Vs=oHV{x6e2SU1ttaG_q^ms z+cjeS#5yHm3B%cho!Y*H2rg@@mB9heUz4km)^1z#ayn6_rc2+KJ11N@6-%`ixEHnV zu!SP$Y_-|?JF1bmH=)ii3s_f^gd^1{1_tJSAAruyI;53`(g9~rC3;2kh|+?JBoQ?; zKn9jY+}E-Vy6$gnCHBe$v@+DD%w5Cp)ZbiFfoyTpTz}Gk6T#Ep5>}Yo#`Tt@?3G(-O3WN5^M-o=R9%*C|IM*_ub?P8QW zbvSp0n;a&&tRibhm-q2JgCo-jig4B3h7<(a(xOa{j-PzXFmaoZ5PP8-ld|&77{MurJYj8As3*H6T@T9; zm>bfl)oFj6;yMk54QEm3l|WqhwT&_RNL^l?+9(IIKNMVNHm+qmqQ2 zr<4LGbm23_aW-R=n-*w!qp9k%U2y`NXMmznqS9kiGe3XOhpYgfxe*y~4-25~7i#!- zdR5|-+-x-3QG86x(6QqT2}R69ibh&Tc>iE zmh{4u#{&In#@^n=xzZRl&jEhs|1O;1fGP%XT8KHjesLXnlLZEtlQIA)d#>fSRpWBA ze+97mW(%-2J)^Y)rGEDYrpi)tA0$f|mz3pd9G&XG9ysc!!37#u+TCiDf z3%gsMZk?E_`$vJC->e^Vm+=sa!zeBx^XvJs{7I}_eRI*^Ln$-d;3Fg`T;Pzr{)1lmKoyN8xi!&*%mu69E-)! zoJ{mexjldAjTWSLxoy$$+rH_X-4Ly;DCgsUx&=)-HY)j8o2t5si+X*D!RqC#2RisK zgotTN&Q3<{UMk;j&R}AR2bV$r1-BiV$Qcf_Lly-_RVCxz(Ws#rV$)D zz-6#IFifJVj*dwTy}9KKK9AgU|D}FCnfa-|kt;OAP-5&uRVL7hAV-%Clm_5IJ2lwf zgtpW^#Qs*r!lFGM?HD_smGLV*$Dj1cCKMs1y*OLqg|ttw$eouj_&{E++>FK5g6H;v z$m{6=$t>Oq+(u`jC`Fy4a2Ern4oI0TVCWo44aZ#hnGcp36bSFV^(rbeSdGhk0Q!HW zE<}tBC-?;zg3x;B+sBVn=>%{W=SRK}=4W1FFVMO&=-6nvHN>n{KUK8xfD!!pMB$|# zB(5n%4q4OQbASo?SNlwu#`^nh>CoFJm$bm5FKgsuT!Pc^%4Y_IH@Kd~FiE;AlIt2qq+9Se9aRtXCiByLWv`oCvm2q~7^v z=!b944{eyglhNHQ5E`4iOV@Ri4OxI#59 z-W@F_sJnL5)ZRwFvVmy+2te~~oSw5yo#5p1oktwjLw90{c6xyF_M)k&idyB!Xp=`Z zGut&6;SlVST6M(cCv3xh zRS{w)!@M6gH{;o!NyI16;;7;5UKm>ynqZ{FN;B}3myXJXfibZb!JCAvAH3jGOBJI> zl8WSviFNA9?nGFtXX6tT`=c#KUCWCR;lNNG|B;W6j?gk9!Y>?-Ku1XbwQ^;CA)&4n z8}S5ss7&|5B`4xP;UdR$*Z0^tH^^gC&cu|QpR7&$+7)PK9+#pYi;s_vn~=7ca<&d` zz2yt#*=cmaMou6+j_Qp^s! z;Z0*dTd?`$pEYq4X$^J%%Fc*^og8h%zlW23+n5rubwFhTez~Q&O^R}WR|=kpE4NqF zBngtcvmImn7{A(Iae~DyXgfKVK3MfMn6h|{T|g+5&7RRnrr6aEXx z{cSvJ{~?miIcXk5hctS7obL(?9dEj&LPL=IrmZ84)6Bm6LDy_$<H0y%$ml~Y&X1Q#{(;&OqQysgM4qbeCFn?9MJf}6??FVm?>`9s=I6O za$eK-&#}EGQZB?h7bHl0$`slIATQR$PS65Vht@DRqZHPx{n!1B8(SN zqfp>9xXRtu2f8UNn$hwInCEksheP`Z0wmvBQ;sowCD-3m@W$fvup$kq@kN&#lysyU z5!927q|kNZdFX3#H^&Tyw>CU#w7~q_;C`_`FgZ*P)QWLjh1z2R8p=9n7nCdQce(NQ ze%=<|ZIhBh*v^Tt+xf!8Ax4SAY=H8bdFZV~#5k=jo9qK=2}d*KeFN6Z?h^$XVUfTiJ0!`3`MnTAcGW05s#-kJ->m=HD>O>HmGT1O7#B6r}4Wq836ludK zl5uVWR}R11{m#%%#F{UV0~+v9>hd`gD%ST3?Q^=S^o>10?q}S#~X!=wSkd z;>^Rvp4;W^rUldtN#V051R-@1IPGmQ=Cf-2l4&xnZ%;u!85dQdXk@V!f@+$@4h%eb zkz@V|zZa-LK%< z{YFqCJI3lu5tOB>J*I0+?f3KkY0~;UA0G*+C}*TovBCGIM%PD6jli4XcQ8G>8-akE zO%4pdtLyxT^3pVTj+dE%mK`2odexiUHRX_lq7i-pBPUkN`vs`>4x^L)62{!btGdH6 z2@Z`wMi}!c_DS(74D~N9U5$W`2FY5chJdzjRPVz_(RDeT9}G?co%=#pBtEB~5R4>qLpAWq0-*ZEE<4dWS55OOkgSHeA{SbE6MN3gcJVM30 zO=j1^`t3nvlOdW7WV;cGuA(dY(`)A>fUdcn>IB zMRLkG7?VCP7VVdtQITUh5GVO=PY3dVMk{WP5`b;!7jx+DZcRz;w*d`)Ph^A<=tp81 zSjqsrv_7rQB_c~ZMie(4x9<9dr@yxY%PY6mP6oDSztxG)%i*J@`J)t1PUr3Yb5B@U zJWC<@P@ZKgeI+axo6*^BOv?5*a6;KG$$b3l!tHC%c3<<0_3z^aCdw+{1mMALLDKxL z9z!=K0#02!zPtSl4IVNHD&d{NO|L(LB|TSy-Q&nw9tW1bP%K#pBgLbbRFl8>x=i;j zW54{bnx+0_i{t;u6ejfg+YdNb8%=5oPnbGGNqF9(`g#H&47RulNP^K9xjs$zgOABv zYunFf%j58hd7qi|3g1=2W4nCur)}P#=|8*(sLWk3ofxnIpW;#uS&_=}2AAXZ(~2;# zSv8pj$?;DZ$L5=g7BSg!G<|jYYKoL+qM(h$`bvRJ8tdS@c^KJ4opE1yJZ{b>GiZOp zO6G6JD43gBmWjEVW#0DW)tl5BA(t}Kkwi0k-JtpX- zuhdYGBNR8X)cj(A+#lm?oyl=xC-})p7RJ$McGy9iLCb4?M1uHSt1vm!PyxaAlG`*R zrv-KHKJxH)=5tT)dnz)%47GTr9aj+@rYuY>gZX2EpwwHZY}z5DU}f$H=Nh<(U&|it z?N(ZZ+Zn5}N>84Z`H=b_j`$VIR%N9k&pnH!4NX1qMFo6hQ&_>_52-hO!}k*5 zYF%7Qx>YC-o3*saUahDa$UNSbwsw=J2fui$4S^B)yb0m23C2kf=|;#Ki{HJ==^xPk zy&NE~@W0L3HP`kwGz2mhg)%+#W!AxA)<3gs^9syfvjC!I;DU1EkicQ_{eXyy+`kK$ zB}f%c=LRrpHdl196Y80bDqE5#C!=o%D-CFNvFd%A_Bo9XD6J1(CS_%BMkn=J*#|Wv zUP}#V9?HZIZnn3rjg?QW)^fsM9AfxAT*2b^la|ZSlZ)N|%>CMw{mNfpkPFpxRXRHLJ84fM9w5u1u6fo^ddCI6q4&3^_g{@>iFxnu`^YxKKQzI@^8+wQ;s zPp+nC*%4SGl;OVKS-^q^>s7oxW$ zsVX+yABQI7r_4bfMVLdL_}uTBMoz22O2@~<9!^K;(6zEb$PM_mP9^J^evIq)13}r_ zN>NxoAmWpGMKPdf;m{tF_zVs}o{&*O;-R)tiNs(YqKrnw@(KcW5qr;kauXG53i63_ zqAiNjH^ZL;r*F0B#}t#}szb&|ZZ%1$ikw#9#T#jk)Rk z2euqKrx#Q2&Hi66=7#j00&cqUGByy8A{)tUu8>zniOa_eq%vN}mUsIk$W^DdsH3?! zi|kM*jRBtd=*N0}?%g%WS2O^cC~!;c9Z#dEHll7!yjA^y}?~8P(h>W_wXQIkwNS*1uh?=Edo(JdGd^QAlPn)~J z$R=_8Aj+a;nFR-r)et9DmRSupe|5UKA#~Q|B9Ogkx3HH1gl?A5LqxvQ%XoE^ii+Rl z31!oQg168r@7Ar__w0nl9tQiregd?mx!obRpZPF~GPi4ChF==X;Ai0-o;Ns)7}v2a zWV~n`DViZNM2)6Y^r-E3eg#>S)~7LztXNLnyd2%++nTF56W@7X*^Ni7x8F7aSHt4+ zGAF;z!Z~p!F!?l{{m@uE(X%snRi?Jm;B~y@2d%zSVX3P*HHXT=q!fp9sd-x<3|V+s z4%TmHKd?>?szDz`YGm+EyMG!;A#GEjJ%1^80}%&&~dx`H9&{?EYN(e^8cz8z(P21u$o~mf}0G zm`C%fkeH9JTJ1b3Xe^NC`n&Q)Sk>2=%zMl!Z*pY(lReuxs|&#JAT01<5wuR5NO`F@ zKl`=a|0S*590pj}$L~`+z`a|caMFT2V6vgf&lE*Ys)+^TraH}Hb~ZlHRU1NUZI$d- z9vbF>L3Ml!@KB>WL=m*wb)z1uj+G&bIKU0|w>dGIz5SNSx(GIPf$40X-h4>`iq!A# z8dwzQvQi~lCP~tiQ7&PXY<<1lcKX#hmaLM4AZ#qCt8GJjCuGf?+PlNV!R0z4z~9u> zK)ce^J-W+f44UovZo+dri{k!0<@)oLEVf(+w<&MFRv|4lrdt2@-DFHN90#bxEBkxy zXXjNBWP2LI58BQNu>WIqE~9~~DMR$KV3b3{fX#X$EDm`>TuYMNK_xlqGnzxJS*Bk3 z(5HyYij@*j0xuZi0zVjf#KmbbheYJ$38Vv%K~*c+;N zEsIE@`Yta*;u}JF@o^Spq0YkN+r=;kKlv9GC2xR|fLv7VCVATUWiDS#E zaE9Sj?>>&4Q(iwXbJ%jFR_5^|LnxErs#Wxl&-?4hvd*aJQNonur&Kv#u28E~Jf<`G zh6$7dlXA013kEo@4~AVvCbP#iBFa=eM+<_9oM~=f0t~cjZ#x~;3CSu1c*~-?`3}r} zBFX)A!CpR1%UU@Q6$+)BR~1`bLp4k?*m!;(pIIB`q*T^r(*qbIIJx3r{kH?&jFAp6 zu&PHjOx1t^%zXn_ed;l7NiAn^$HAA;t|WBIEyN6D`=@`J&1f?7bF95DV~1tO5C;nu zJfY2$ESpBpO+z&q!0Jjstyj>ESpVace+!j?rKPX8?x4^zU?JP+M6F8MPv&&zhK-6TGxJ zqupV0JooP_ZgYHd(+-V0DkyG%Qa9z^T)__7nwBrRsx4C*S&W%8j%J^8^l#{W;u{!P zPrPXMiV|5VWf@kCLnC6)3dmW3cHj`PlIm(Et?T27lT&(;Zn3K>o(vo`t#48LR{gv8 z{9JM3ktv6tp43v;3=WbRu0{bC61VDP+&UglU&*)R6>jxtyC0xi@G&$I#ezPiYmBOH zsK;74H0VSQYFqa&_W2VAT|<(#;A3LtJ=}c3b@NmrKoroA(GEaeFI|xgX~u5<6DN6P z9~HFhWJP>pDKe`H$^1xT?!S7A)IXT6X3|MM$j6>8l66T-iMdmZI2YiJs^gA18W*3v zQYU3;%DwK8DJg*N4@(J-8;1>|EFBaC$w;V6v9vIxD#;olg}D=)149`c4@2rNqZfX) z->#&wQP-yZ%DO3EhE}B~E^C8DXB-wzCTs_hRef;aqT78VB&R)ct2#fF5P+Bt5^TYt z7gTuY?cJFdM<Q3v9n_h^-%Dp8-N8vg&6h3C~8gAx2Y{fI~o>q z*ieE)BMigwmQGAg+aJ$p)Uf`^w_LuZ`6O0njLTi zYbLIa=|Y^_pida=EmoTUXm86HyrAMXOGXR!MNN?_48t|7nJ;dqf`Kr-{AaB_9Df#c-Ub{J1yPDd@y z2v`bFE#kBYUnOZAs!pYu+v!k^E9FgSYhl8-Ve!atthU*F%%f-zN(lZCN*Vj~1naG5 z?kzO)55E`RZy@nDU5QJ#Jb}-!6&e~}^O>Z1aJeX-GZ8!=Q=mExDhVOEQ}Q3=bX^}V zOs;Q za8js@CDLHXpx)KWs#R zoHhIRKlNF7yF&SbECE6!N;Mr|Z839`l1fpiTYQJ0TyGaCMWGSEZ0GX)8~5w5My zs}9lAdXYIPDhiT{Goy2eDf`t$9Vfo%R%GVl1W5RCY0A{IsL<<+oiDk^i0kKAQM$-V zV_tUzzAMGAWgnn?S`#CvYOD=17t!%}!xDD%rUscTy*xa_s36x482DTBWp& z7N@C|fg?nq=O_D=IGU8N=4ZvRDB9CX-G>r}ql8gSHob4kbriCzv6Jbyvt7>Fp@v0^ z-WLjXr?cBC`oZ<+z`)SMyIfu9GlO>4++Nh$1=PvK*eVK;#7bF(!*qf7y^^hNCw$(G zBp{M;d!td2gULHGn8&gN{-2mA#$URSSIf`>jk)ptm_Vg%Yi)fZ1+F9q^g#+*Vg~XW z;Prnv0ICLL;-O`7hE(4t7^28A2L@a6=f zKhrCQ(mG6s)JF+x3E?Egv2x`jGMr4xtti}+q}6$Q*(rhe9`}VPR>?G!dK?ONIQ=|_ zNs)!vkc4g4?B;a+2-CBb81fQ@AhlqmNhzCHh(mwf%)B=sjbZvZ@s;;-xyorV# zmoWw_FRv(CjrY7$g3hJvVX5!S98#`JxH5&AsCO`USIgw_MkE^!nO(W>pA=TH?_T6G@Q;Pg1_pIrX9<~he``(p-b`<3qH(NS*=>Vg_s+=z0BrtUz`w< z@>zVI&lOTY4(tQ`b3)S9uHFd0ApP7cCNF^*g(=Nd3XX|EAd9~3DDvsd9tVWB*bK^h zO)J~^FKZat9F+LqDpy#zxHT=Bl%c0RJ|se`Z63J1AngjF=oUxv4#(4#UFur12EZlb zZ;?v(RCm;rHUcd3T(kgH$QIc1E5ZODuD1|-$2&rG*uw=@b2w_#(#?&(=BHPu>e~Rl;!ahYmCsyYeL=CABw#~ZIY8y{Iytlc}l@2%ex*dH6Qy5A-bOYrLd-xjqiM%f!U zKLmnvdsqJx2;}tv0kK3slcckg!R^}9(~SrU*raP3hwWmpjc1>}kOg!G&@7^l)v;#P zjdoV<;vIsicbEd(cu{Vz&T0m~;j8%nSza&oKh^~MFJzQ6F_9zGZhN19)@ z!#kJy(7_L+$LA>&8OMh+t5$TNpN?O?77X}HB7(^eMqijo7Zhio^lnBiQ$j;8ri(V> z{M-A~;$<-0Y)~iXZ0y)geXZ;ulyM?2OAX&Rjljbi1y2VXgixA(&I;TMj|d)F6vl~QOUP|iwL{0E+dT$qrprEBYuxXnX3!6( zZOG@Gc=j4Pv=c{fZ!8OoxplwrBD8FMCScwty}^k4q((L?K@S&RZ0f^R1%+TZL(P|~ zB?rtaBk!T-W@%=2XTiKEkkLOd{wygjn`|q+KSE&%YAG z1Bj~ScM~B@@bd)ozK87oQ`Z-&%?PV-xi9Ok|5cIf8=>!-4IpjTsesF+KBBZZo8$h) z=@3p)^Ota|Iy!9670+?Sd!1Sf`U69;x0mb}ea#0nk?%*uzY6$*OlbThG#AiuoVczh zryzn4l-TYU@FVzQFx(x2a3>@yO#hIIYD4hh*<)n#Wd;*0>OUJlX>UZ5X|Lkt1uLZm|Q6yko(J)CFI7k2} z&LzhP7#YhAqf6L%I2(;Joh@|G;&+mieSvZO%cS)5F9$%~TpSsjVeK1`uDVt1>`pbJ zlc5{0f9;A<)F~F)ts(hDT9a#*1`q$XFdo-Qv=${I1Dx>RnQ>$Blz^K(4`bn8k-S5Z z4lth_6Zeeh&`@fQgWH69Sag>M3oOQ0_R_JpL@A)TDo@f&Pb|pz#5}!aq+(=NW}#z$ zVyPd`S;Nob<8iWIt>wk|q)`(sWu@2|bm)8fk#B~E^zYT<0lM0=dTr6Dv%p?0ajEO& z{}*!jf70f*S-`YYXsfqXzaAg<_LorH=raQieLnXr?S$bRfDJ{D8Lzfiuow%h?(oo| zQGmS^$FRaUW!dd{{=R5)=BAWzKFE4A1p3X+Xu@&)d4RQsz9X$VoFxyHNFT{(!wUWw z<_Q~nfMs}_i@kZP{3V4|?Tx%}oY%v9TCmaz_Tn+Aqq4AM=%@rN7fjH18VMC9Khs}4 zwZT}K!Y$N+w2B-C_2@5cCH>oANFp%A(^V)+rHPd`NXDbJeJfl6%UQH3D&u}Sg`RkE zAfFanE2UWiLm1@q9K981b@iYcsp}Qgj*0RNshJwbp=k>AuQOwN4Yet}vk<)-PCH>y zZc!UL&U4mKhgTBgux)+PhDubZ9-M>-h%V_@j?6_xH)4LB(~&Lwi;D#Vu2wM0Pw8`z z5jd)mfMR%6#?SjhMIacUqRr44!K-ycOClMEtB4ZwQ8M!|LQ?&ePBdB#PLh9i_+EM&lIpK(uXgk1io zT`}SiFn3XoebU^bC)mTj_m!qy0Vnd8c(aXX0r6fQ8H9i4HY4C&5P$Ugg!l z=dz=sVX1+zLDe>#a#7guqU`4xvJ82_2#!FL<%h2)S6j!D(0kG;{5|}z;;L7GerfX` z+Vw;Lh!-cS?U^$w{To#Z+VO89VkikjED0AMHl+vUYP}6}fDeIiZjvS3g46H^dG=7C zazN>*QxU7oFl*E+TG4C)^$#HaQw|Q%;-g{S?<>NZl|osTFu8f!F68b}N_WZ-)N>FD zs6lMqxE3}NJg~kwvHGurhFluS4oqp3J%Yk! zkUQGUQk0;t?mdP$owMX8#M!A!y$mnnUz6$Wr^9(=SFoH2yucCUFI_Gspe|6YQ1wLBCFU=4PL66uUxQeIGZL zlvIu^a)iBIF$f&x;#jurxNP8?mu7NtoW5UQNze`Nay_pHbW z=aV#0h4Di|J;n(irkQ2pE;-d)>tu1C?T|Dc^%^BmP{REp;-8|&DH zCL$pW8au|&JUIFiB2%9(B?$mz@B^g|(WMl0!$ph8xY3m2pgZTr)fhQfGK1~})Wgv| zrwjUC%IkJ3vi{r-xNTR%P5EA%+H+a0j5q%E`J3fAG3*N+(sTU05;vD>TxU&H^9EV$P=sPLaUjnRci*Itq_#hk` z>jU#Dp9V|ITiS0%mjA+j*$q!pOZT@K_m^f3Qw~>ZOB;}|fv7G5tLy9;l+~fMoH>PD zfhSmSc!Xevh)y$fwuRj`;|gUuSm$wStfPhMqf%}P|BVVE3`UZ;dGJE+$An3 z5CQSwO|X-62qdK2jp%%H_ZZjKH3+lM*Zpq6O*vtbXcf8mC?n#)OHmd~iNFob1%TW` zUzaB^^2NXbW^XM1_vzRk)z$t~d*!`@LiY=A5Kh34A<2%mlB>nM9J)K zPZ1jnAxexM&cp{qehOUzIgiMpt-pGEBEt)y8>VbX_q(6-0+Q(yCiDSm$LN!(~^ zq(+!!rri1pu6tw!&Y3PK?fRwx6DMnP`gb~w#E`T#hC6sao@CFVtF+P1)tqcXoW zQYkqjt{Cp>8}DBLpLQ=MOX$WcVM$UKJ)0b1Uv80Mq3=Sf zipoIGpf=h*CLb~?I+ZDXj;~I{jcf7cW!XpfbIgQK8b>PiLkBh|>w^yt(ARIMZ zYVrk{fk#_BTa_FxL{|Kz_r9}Q*pc$zXk_}T@>ymp{(o@&|2@qD+8dUQswq>9 zs4Dp*VoozNJkAeapjN&1H5|`){sVd4iX?}_q`;g&%VBhhgEd=xzro@|kmD;z-7Un8 zq>@A8MgYZFrU$<*r!sJcF%_U2i{TZcdx8qmHj%gsv&vn4pyc@6KfW6`kN>sz1@+ zjY<)0{(-h!bs4xoET+g-TJPZz{~D&c&}zaBMGpFE(z2u+x%zi~5E#opyiwswQq{Q7 zyI8;dRD=4Ic1vPjVsL@@C{DO8B`Yahh@TssR6sL7hy9x^h{e1AYC&rOK96Nt4sdLOyi&oR3#PQ_mi z@9Argj9s;DPhqO=QK|gqW+hT=nYF=G1HQ)762ySkAIW&f4&#(FY-Mm-V=7AeUl=}G z*+oC&pE~~IoX`mdzMF@&*1M_6jBKPBCCcBa1 z&F^mptkhuw+g(zFmSQ?s4m^*uUfk!{GibI}FAVrm=EEwaW=lkFd~SAZ6s=CN4y`u3 z;B>zllB#`l;+QzY$W?)BFnnd~-kw=5)OtJ;B}U{b5GyjCtP=18-fva@U2i<8UnGE{ za%NxfOfk-jR4)8+i%x!1)$_#iiV>4YwgH&I2PA=i21Ru0ToJrFzv`4U$GBeB{<3yf zp(8!7;G)*~m4<{<_ZC*`)&Vy*3Rf`V?(j-$TDfAwu>(YEDRfq5qk&cox>sGon&yw? z>FO3rJ{R9UIPhXHbSszqDI2!gh3!RWT+c-xb+WguM_UqOz&{A9bZDH{iBd72eJj>| zT5vrXPSeZjndrpAlYX1PJv|o|&A*Rw)nMoQMFRg@_f&I7BBOZAbG5e*#Esi&o6+=NGfEkUtSCs+ntysekN2lKZn90QU+lMx03(5awZ{8z zFvg>c2br^TDu)|m@c50C#;2=za7PPA>RAotmD?@@pW-*MTJ%y`UDjlqjv&V+Mi4kv zjvF%;HM4CFB)AKmIzNPx@GH!xY{`+hL&s9*--UmclF$u@wl(g)F>c{s_k?0<(uf$k zx3L1**@q4?dg37`n-K&nhobmu4t;||5zxj}bmN1Y`2R#7f4y?Xkh$&mEk#=O>@a}^ z-Q(HJ|CiL}zj@|arQL5>Fk^5!OpZ-lvzUJdKiw&L&3A$_@TW23511cy9?bpprjnud zvQd?BZEzp$k0Gyp&SO)>{@Dm(&KT|CNYERvkj}Yic`;xIkxKk1l=bv4NHXMT=7{)xuk>HGV&2x|FD%(L2X&xS2 zk7}*ajgG|m`jc31oz z@xsL6PW{@NYZ*Yrbi*b4bcBsq)lr%C< ze=W$g9gAscuE5^^I&2b3*jun-QLJ*^75^?mNd}=_&_oZ34R>aUsJ#^};#1cwoM>~) zecp^`0uSOqAu$V%e!67s88(0&4wI%6mqCFa&&xc|gN+MD9s{*cQ1G@EKF387q&!&V z8a$yo4@vC#sqeOx0++gU7>8QUz{`%DhocvtOV#AXMjt;0V_b2>)pLG-R&UZqYXnSV z(U2F6v<>3G!bd40IIo()u6$_u#@RZ$f@pL2d36+;51ncZ3@j&y=aW8@rb@{E#J%(G z4aI)8kzq^V8w7l+w{sTo68LKs;kqYiYvg(;f%$!)KJSXqB z-Xi14*5`f~uE6trDXFvB0sdT4xYy)68Iqxi*KD`#cL`CTutT9nq}o%Y^fBb<6n5$B zlm~w7S~K)1IfrR2G=U(dC=){~kd+xvVP&e;P&>uudlUhpPgaHTlv#bA&7cKHHT>oxVBRpP_AvKd&8%w|Hh{W`tq@0!NUcwHov-E z@Wl19MlCZcqZ7{R+*mWYKNG+TF9@5LmG$OT=@U~sa6e3*)bsjZ%~X^k=1cyid%QXN zO66)3PLYC}f9<0i1UU_^|4JKLXdry@nd_0!~!x;|fP znj7Zn^xP?E9c;3@>wmwA6?zd_;W@8AhiN@J${o`{|D>ry0HM6iz!KSjR~qj}?^nN8L8QyYgjEj^YVm6~C`NTVQwCUF6~6G()s*E4hw4GH+xN zkA?w3+AYs4U|(Cbp(hjI#b~Ey6$6^jS7K}_N2ZT{_CphV>JqQ70PxkhgY9xs;NUq=yvPz#s0z}xJgl-4W)Mcj8TZHT2mlcz9h0=RDf=rHj8kF$0N;JkW*b3*# zq%<$QBb@c|L$e;eAJd^Vl-FabDEFsxFBrXUH#E&fD~E=zHS)sGAKh7wbn9T|uCNQ) zeh_PjZb3e&zF(u4-n%v)VkJ$@HhtKWX?X_HYY&sQ5wkXCLN7D{Waj+jNGveqQTV3L zqE~W*t10o_DSmD2v_(yRFm>J@I1IwM4FnN-sT`sik-WYFoNp;#JXUsX5e)@hrY4aT zmZD@;3}i~wBQew$_x%;G?Agml>HromZ0%}F2CQxkAtWQh(K5gFI7I*0LqNCM=G5Hv zW}`W698~KUM6PeJi}`EwxNd3y);Ir0{52Ao2^gvXgD0jo)$Xr`Y8b1&cNlBA5OH#1 za1yDd^hA4RY$1*3Fb`FN%i>p73F*+s-BWBMhs;osTH#k@;weCu( zi_ z{`1~a$#auw33>@oC653fX7`a>xU%*liE2io_Am?cqR%Q}u z(GAgb3LEAeC)}$@?Cxp3`dHga5X&?6p|7=6>SCs5Xy{OhXGq?PiM1F969tP(lp$Mg zEWtK21dqNi?}N+ua77|VYJ@4slhR^`(rYBAYRav1{Y(0?J0^~@_~B7hMvJB&$}&l9 z=|F-f*nP2{6_A<0+2ve!y15jyWtkL?u4Qb#%N#fTnPuDLCcwk`PyBN|sheH$UU~CU zBQGkT`D>uFj-dy;M+A)|7G3XqVB6=HnC|vBi0{tiHaAgZ5HGMHNf;yrBr%8mwPjV) z^tzVS-&G;r9yh_>OD$ z>=mbjw&VLKcJ<<2*%B1W;f0algS#=qda|55KWrxqxRG^sW&6tED7WRzzuKQbT4_hK zBf^o`SaB=uA}W|}f;3{89O0;(wg-Ji@1rrJSW96^;Lu4jk&hWTB(=|<)`{cJmKJTS z5>^u8wLR2gl8CgTz@ns64YRf4Ui^n`co_a(KP>gybD9Jtqv1ph&HI!?kRJAk7##dy zJ(eP5BY%r(s@szL#VA)0LAk6(3kihPzoXT}GF%XE-rM<_l5z9-H!qv|D@9!sQmWrO zY*sux*j;$yE;kKXAMT*A{f{l5u)!uZfRS*=!pc_!{J$B1h4Fhj8XnMR74XOMjQgPzn;-fs*~Bu_pI>7z=yHZ57+r5C4+d+c=Ck9imr` zc9flo14|RGz12O;J?-d&Q2BA^%;{@NOHjEEAbS4Hh?4E?@Q8kZ`}?(T|3=LHc-<8~ zUs%?+(L%ZXpin9X*}EqOZeK+mdn|WToY_=*A_tA zW^Y5@trQ4o^=|Chy%$bDyS@q>C)~I}KfnPc@gcI)X-5H0=Q*+x{SP+ydVL(7tf{aR zktu5ytZKTj13%A6psM+ZT92N*;`Hz6t-1AL7O!`Pg@2Z5VYqGOPqO{-Y$u+j5_7b8IWlq>Xn>b`5BWiRq6tk))uds@u8y!xdI> z9P1xhqHG3on#v;2<<{l=P0kb{8XU(wLc*p|Om0)RX6y=HaMxV{pMojQp_92|s(n%6 z%!B1SMkB-N2uaF+A^eqR_Y+0gVY?t@#(jAsnw=pe8A-a)(B80c+}PinHB7MoBw^3L znL-;)nQ2AP^G1$Qa=X3h-|w*P4QJmLZN^=(wAo~%t8tC(<&FJ-4TaVhS(nt*cbT#L zkh6`EJ*FynGu83isMZfqpg%UX!~$x6P!hS67gVxjQ%YzVyM*)x>@(6(O!P1UXC6d)V4-OhO-9=peI;j73kI8%*uWqo-h zhfZKX;*r6|j>PZ%)5s=XSSFYl(RFOFQ~FjZzCuWhVG*sO0#F5sz7ad*v&TP;-ph%F z`P6oyFGOAa)DI)}XJ+@?m>fmb)3`)Z+hN7n>B|i%mdUH4s%@O3k2?)*%%B}?Sxd~Y zuZlM`yHu$?OAcw!x)^(5%Ym%cs#hqpbPW9I4l%MO0sM(0xgD&x7r0nAo;8v2EKTqX8#tl%$pl6| z)&y@J!kT^071&AZUofb3xWkaAZEa2DeP4eq>JYo$BrlAfqkSWo($eliT2$zaH9y^X z0M`I?yw0*u^k!?J+IS6}9C5hdKv3|5z{>i7qU3ylXqDp+Jg`OIk@220cucDprYMdz zQcb7=pO#Q6^wnt9#c}GmhKc)Pg@LnbH;%+2C>lK^EP+l;k_VrME+ah3vrVTNz$yb<_PK{6&u4&yE77|O=2Aa#Fz-Sd5x-Kseq-~*H3F(<%ET&)ZTuvw@Rok$R zor>}%3`=zCWGY0*@|T+5YB8Y|I|(?xgZK~Wt!wf{U}4enq+}{oiu9*YrtnM>DeNQo zx`#s&MXuu#7mg2dUyB7iu!oy6{;GAq*uH?Pxw_tC*Hop>BRmFLi1|HM2lW!Rj|0g0 zKMFc~CMYQ@yc2_p78iBL;(1fu9+{SjU@Gpd+ePO+!Y_tSE}3p46H3~M*|*XBiq5lr z`Wws^Q%vfkKgb*3Opm&KLYI!<&sg@Ux=-llsdVj@KE{nI@-7kXy(h29Q!EruGyq*Y zQpig(d)nUaWz;blJm3%Z$Lb*jNy(%*HgtJzOtqqa=O2@q?eUI@pfI=eR3IdwgOyn; z?=#`s-HZWuPLLu#j@IH<^NS-yEv`rH?wdsZvHp<{At)lM`$ZgK+kbTr^R(XVCWV^@ z+?9?={u>sL##74(Ckr}>I6v_;KHihDC}(E>qY7;hskruQIR@tO0y~fN4jv~xO?(3! zdBk}4Gncp=&CH7U&r!0-;<#Y7ojwNV69Sm}FFcdEkV8AxO~)4_m4f-n=_jMaOC$xK zYv#K6pRHj zGBcQun;HCA*)tGdW>*N)vZ#N4_G%a-vV{`~(G=01{@!U# zy>708Pt*GAqgpicr{nwl9KkGWz)-OIap;IWeP8>Q@ylx@3p74=xmlQjSxEL5XI4LM zVn&MkLr*P^?Aj77Fox@1OeQzQtd$?N4=8M~@B|=LJ@J<(+5VSn0MtdVcG)8;Kw8_? za^k)4eNUdv%w96Ut!#pLo79KkoVx4T{)^Xx8)_rj{<7!_$9f-;h}6KeaGlX^#SU{b z-pl|q4xjsxQiN&)WI7^VFjIV^vO%In3$a%QPMr&xVO)UmN(yQ8cH{xf!$X}Eg<_#= zl3SYckun>78wv|KYu_jj{10Lw>dz}QKC?FJ-|}@#QRrgs{iml~ zp4D9=(G!+1(RgmZfEMH}sH`v>X{kQu!^UmOW1m0wTp}BOog*)vhtds;aJzK&vbJFV zaXI@9CuZZehZh*wRZrY{s;cTDx|hwPc6YjPBAKZ@#6bY91_-omXb8QT6MUO zNbGZB`)GUDUN?c^Hf!xY4-~Xuk9v&qq-Jkwvrj~~_sszj0~Yk}2LLqZqhJ?1%C3Y?QX_Xj zxb-@pB3O05bpf{;AN>3nWf->vm5>%HT}ap^&p9BBVz{k@$;in%U9=Q|Psi)ke0+5Y zeLvcNr@$f`M!>l$vCSaA5X*rAHKIO$-8Ux|5@*4p`}8r+fjo_YXb|d5`nKl@Mf_Fm z=rwtn+mKd6r3+__yhOmft`bdcN~PS$jKwER<4 z`w^qOcG)$tBnHlg;)VjA(ow`uIXzXHk6xF&6)&^XpG`YwqrnVktMfWHZ01?Li#1mn zyu{e4jQULtC66aP_S~2Y$+t6OEIm4lpfPLLvt~(lHRXq5G0BZ1V0w%catUAIu$!7O z<1Y*E8Qa_Ha0&qBbbxCGziMCK^=2!1f;+sio=zf|cGh?GU>`k$i%Xs}7Zqc`FblvL zZLdS=k*Lsen(>S69mNZ2Kd$u&O%ls%!n0DG3?fbZlc}y3ETAJIB>K`{MQDj)!(N19 zgSnI?d&jEo7d0UEWiLF}j>#noN z7OdI*L6&O7Kebho&$*cp?3+C%)tHE}`U7^4P8HopV4{Ke;IF*7+RCEZ=gI6aK zeZ;XoCAitvNo9H0Qii9n(HPPlQz!yfn+IyG_{V^cC^u)4C2X8nzWD!PT>Q2(GWdM! z#{{42Lw{Zc$`BK$&0c*kEpaZna-S@$P~wr8N28*v5-~~yA>QsiIXqrMxjidWGs^5Z z!t(bhQRt*H-TZY1TeMf}*wQR=>S)cV=wRqG|aUUhMykD!|m1ij{EUyAJT(jleX6{PHjbk0d zBJO@V0iN^1%Pg)T$I()2)Dsp8BIpw=#p`fZ3u;7WLX)V<=0NL^wBqSs0U<3(=~A7} zX+~1Q1t4Pdkq~PwnDEmNM)X7lgo@fj-!;1~-2nCzrZFEn;*>9AnxQ}H~Y zR(kHTq>z~2v?Xu!klL>C8L4E-6@ExeNxDKSwryvtmlQMTJ@#hAw2&i4kPzP-8<5f- zx^f%QzDA^Z-C@HZp4sKOpM3tb=%+%79sUK2vdXjIN18-@k=~1{K%`9n5W1qP6uwOWsXJ5D&!H9CS|n^ zMGg_QzS7LhJ*#Jv@aY5EbIK=aM-Q`i%}m#RvTOsglK5hyb?fig+4oDw+P%}{odNGt zlrAUE(c8J7$3>GV7bsTm(CFNE!M;YOh77&md?5>9L3D6Rh~jibWEnMdj|2$wdOLpS#$OQ+8&D+g!7(}&so)NtZ2Y>? zD){ONc&--KroU?qth{y%znfg_ixyieR-Qj;m<=x9<(U-b%xcgXY7%>GZfvaNttNM5 zl}UZgWloo8lkcos%=apf-*+xUiglP>I-a~*lYh+m`JlFvTIXCHJ^PLj$7d9gS=-sJ zy}rOz+DF;o70>iEh1Z_2pINpm*IC!tKzSTXC4tLUk-0jr-CjlPTVGD}T>pUEV9-#p z@l)*)<3C*kH!=Q0*Mm9s<&t2!58*Td!p^=%zJ!;-GGBMvNP*tOh@Axi6pGT>Bw} z@u%>PaggF(bIz%<{cX z4!aM8b({WX?E?ihkb)RHpGNYC#j}z;_ND#=T||Z~5OoBL6$Gu5Q(P zBBlM;e|wGpav=8=`R9``v7-nfjG8?KUN}1BoikR6ghbj6GX;$d3ma}PV$YhvCk24> zbopT^h?|(@KN1%y)9$E!frf_Z9p>O0VnGjR*der-k&YfbKsKgHCf1)V*1DJb95lsrx zU#{`LzsTPN=YoLcOT9Z7`Nz;!6@FSc(5Ot?(AC!ac5du{K}+n*+wNy1y0mTZ-{Acx z;!qBBK;chrtF8>K{{5AIZ1LYyo=Sj#z1Hid(*8HV{xQY(jkLR4`<+VWSI{4D{(mf; zlMTyIy&waZw3PdgMgINWzh?hm1*+~QwBkOA-fNSF?UxSAO~W*fg1_U3;R8fH7{lZk z(?1xJ72?R;Lbn!Fy#voJ6Rni>{>aEV3Ac~-{^et8;9dv@4`%fHWpoP8*77LQ$nL*_ zFMpvphu;|*NDkaEZm7R4JB0{dzWDg_cu{>jEWoUe!Tm|B`7V${+TjeNK%Q8kH^w*| zOC-JpKLMidzal#STJ$>%aK$9p^Sr;o51HcwQKF%|9d3|S{nW2J_yy+V76M}715L-V zQ-AUT0SOV_4iWd0U}>tzKe_F|56FjF{5yny<8JCl>x^8e5hoTO#zF0JlVKVyw^IbvOnS#si2jOsrd2@gDilEZHopHML0lwYo zYJeDj)0eY>#Ht-@C@w?EAH zaY4%+tg_3FwU;}csb#BSV|(vZDFlxf^M1pXMc+SDq(*J@a!E(K8&Iwf-Yl|Rxv}Bz z$MQZP%7-g3ueXPj`0z5$7F%zNTCQZg`ZPV7f?Qw$ya`|ZeZa(J$3IaFKv}^4Ix~xTv*Yz{d|q~+;hO7<#aD@~;{?s{ z8c-;1Rjy}wUsr3HGU@cr$x^;yGaBiDJ)RI7o@n@*J&r})V}oJ@*8 zl(#27UD$oi``QaWHq|wA^_Bno*6CcjoSH?z{5WnreJ8y0st2uzD8v^?opAZao%fCw zP_rQmoBJ-E?lEZc4~x~?0~_0Yp28Ts7L<6MULY^4X`QgiCmNyd+FFR-6(Gs$kMK#- z(3V;7Z$RPvpGP1E9me$SVprkax%Fnzl67{36QLo9^TDlcl<3N28u!-0WPj)1RNq{M zNdU|`7>A>FYFi-?t0^+24gAEYc=0-`TuWu9zAabJgK=~;27h$tD5zi-wa15r$MJ0~ z_3(H-I=>7z;r%M_#G$$7xp}?=X>W#icfJ~O%V#Na5d;1;@IjEc%A#XF1XZV8Wcmk5 zijauz(J*w24qkO?>`k9t`^9AwdunX-BI9VagEmED`MOVL@C>PoVKrj!OdAMf9mpIS zTp_Jy1iKFDCqvi6B0bWGf!!FZO!*rHW#*U!GX-8y)$?T%=*vN@4x#hZ62fhhyRlH z5$#iHyviVXxlZNVeth?eK0#uB0R`J6go8@jFVKHpW_j2aLqH;r6bZ?1@?zFK4)-3o z$`@Dsrfb^=c-Nb$VFHJG6og_1z+P7ytWg=J2r&@bhD5M)uh~~)mAyaF%DDc5>qECn4Rwl$?l(+?!Wc|1dYx!WSIar34c<75MWdgeDM z5e97et3Ez16vrOQBL|5t&B&_Q;OLaUpo!Kp%ONwuU5h-keWaM_Vv6HmocQF!Zx&c@kV=c&Xuw##00kTO4QM<5A%${|f!f+} zElN~ArwMvda&EEqiumPtr=pluo$+!zP_^BKF^MK6io)9(12&qNrwRV>5i&wdjK8`G zj{Bu|ODi>^BbeEip$-MvWbA8m(?=!OZ!Xsr(beZ)KZA;Bg%oAAqY_$i!c09H>^?9u zoUi!RtD{S{+u&KX+QJxXGT_e)1+9o~VTQ$D0j1H?S13fHQa$a|l|2Y5njy`fWKi~Z^U9ZfZu)@n zN)~iR-dm8t+S&7qqK!8`oC;fkli|zen^J1qow!E~HX888WhxZ(J%h})dkONVJ zCrju(eT+oo#{It53wqa~pi3KYZ0qlzDl@rovYwuP*v1!03FLpB(S9?>?tIoqs8nMB z5%M^mDbAniL{RhD;BjIb6nn#9(TjnCk-dQnfugg`oVE~PYo-L}SKerwW zW^A@CJd52_{%GZ(^_kX(s-T>e;?F{xR0jJWu{>iwEpNXdB-_4Fa8`rjSH@F*S?eW% zc7;3Zup)aQ4WaBq1NIOG?+@$@^KS3@EWFay``$R&2WGE-a%hKhj`}&;i0_%SGAY&< z;E>rEz=(l{8e#JOm;^2%QN5>1Sc$v!<-KI5+sP}4L^I?WuN{4cl)cDO0U7zzc~PIi zyEb7SF`SL6pLbm@i40mQ)$QZ^w$F#J@VKFEXWk#sBO{A-qHU(xCGpsh=->7>RGlfn zbYAvR$Fg5c;O}62Gi?1Y#(NAguDrjR_cTC5+sBrv+PK`Z-5t-pD+J)*nd|@ppr5mi z)Jle_XcP9q7*4N7E(~QSN5Z=PaH0Oq869%nmn;ozg^^}|H@hKQea8LD>m^C>sUdrR zZHMY;&SA{f&6sL3Isg)z4+{@{YQQNuieyccd%sYD_eb?Mgp=#V&}<~tKGU{S6W4WW zydi{<>ee5ur`s-FBpokP4pF8fP^G_EmAy+mE~qQ`;VVeziWP1|Xx%|uHGeJav4lRN zj6U`hv|XB$u=!~N?8SmFY}}&O(zfI@!_%beAtXEb<};6Lu&))k1lX+E=0-_QTmh2= zpmZ4L{p=jASrwcGY(z<&qxu;+Zv>N{b%9;eg~i1fx~F44Cs%z9%F!?~&a>XfmdlzdlxAo9^T3pMXEfGv8M7FbSCS`F^@`baXj6{ER(-el5st}Wj z!^DIQUz3Cw5_#JX8&UR{hf#0B65ckMQt6mxSemk0#^0QdGiowUoRr`&TF$<5V2lw# zfM-uE5z@CNl*!K~OHyhFR7eh~u^?dtj58vY2U9&0Epe`vRZm{~)iHuYwX+ktveP%k ztB2%k*lku7;*x(MQ~qZ)Q3bg)6}soa?b>htN6}kd|F$~*%$B;0R!Ty5mNqRT0Xm6xEBwp? zZr^@e)Yhwa2f=gkCfgR)DihyNyo^%5zQgwlN{+y|UK;n!3PDg&cdw^!YIap-@v+*Z zha|ik|PmiMipu$8jW$9Cu>+00Aod%gzrM^3yebQ~Rb^&&JVFz$_4g zTpA5D6vQSPH~P^ok{(bNTk}XzN*4yn^l@8sL}f9DfTraLsho<#w>+PAQOAwEuuVx< z#h8`JynHIE4Tsos@q8{dwM*eo%KVES9SS-28$E6&%c)d<>GeM~_y!z>UR#i@o)O>n zeOC$MW<&D#$e9DyXWmM^ho!aa8u|Gq=GhU~S^sLwGl&pq^9bDr=h9#I)Okx-BK4?M zZ|HdHYcE?#-VKr>qeClCZI`2|5rP?C?0LPCvci>)?}?l8QL@{gGFEpf?mvvB@t|Mt zPYl+76WaRm{qv2vt3HfZi*R61$M{-(NIC<`1058LqY*_}iBN)pUTf06+5Y+9m!|87 z)YXcBxeQT))`JG5m~tJssy)~G&Mgc|VrcuZ28wYCk<62v1<0+q9?q4Z{lt2H{L3-e6iLc`UKb(Ik*9oLVUqgqlIm51KlE7bIC#(%F*9T!e#gim`C7K+*U~*Mr=mz2M`U{~mSKVp z8ckA=!R(ya$fRgu4~>F?)7x>3RN=3Bsu_C#>sA~o1}X6-4T3kDjKHf-o*-w}ouBZM zYVeMCQ^odEa4OBZH67yGNEo5^`Dn>`tRf6S7 z-Kf4Pw={o=(#GL+=k*}1PEJ(`MH#Sgj>4%D+?_pyj&pn<6M5F_fNxxF6bCH?@fv1# zg0;c)bVnfKM-uNXA5C=^(n>}Fcrdk3^q)$2Z48PNB2 zNbDFckKKVqn2#@c&&kjCq;p$;CeyJLadZrngisF{+XZuZ-oW41mBX8_S_*)G6D3qs zyQthNy!~pK0;@3-QA|1fwZV)^Q~lKBD*%M_q9CY^7u@SiJT0BJe&MQ|=c*~#n?e!E zw461?hPw;}T_m`%wX%dAYRMS+3nxM^B-EFnw^b;qUK};5^Y9Grla*?;U|13x%E@Fk zg*;N1LqA6OGrThA+?Qb))h23aGbVfUi-?kt+bx$4<>B`K%J=;C>kbk@rM1-Djh<2{ z{*EmIIMW+w#3;dtbefvv8&X!od4aMy);6+cSCfh?d*0>Cc3<3F;M3r} zRf^!7cnzZ0c97YS{O7PRB;ag!gXbs`Sgz8nSr#99)D;tz1?YV{VETm8!o-h*)!@hg zpgt`RPIFX-8B~cwzla&9x)*D4!%0+;b} z`Q%6oxu)g_C~jafG{zUXE74C#R_V#_Ke%~I;b4K)^TJ9gW8049^9+iA++)CyVwl5} z({_N>@qR+fa(mlwIY{>IUGGg}wVz4lq7Ikdd=$+dOt0z+L7cY|{bodaBxyX0i9j(f z*=v<&XT`#;6--dR7Hdouq2s~qvC2}+S%XW>_YO|EL|QwciyHw6CSDu8J?HHin$c_C z?DC|yj%u*EoZ5fe%}Z6~vg)46SI>&v_NZ70X7?Z`#ME(;!2FCek9YIY;vm31u$2dr?4cYDv^} zIVQUm9r67HO)||5Y<)Ft4TI#&*YXth%?;B(%FLx3zbgvYG zn*O#cGctPO)sRBQ+L5Gfc9+{><6_Tmj@${gFm=XKflY;I!OrDFNqvvCI2nkAT21hI zuWfi_*Ef*^z0`xmr!#90zZ$v-$seH$BSSz-?Q5J~mGh0!1uE`_Qp}h)H-5s$3CvuK zb)fI0E_4n>v1D)i&N_@`Ga{)Qu-u(JeTIs&C!GXI@!YGb(M_q+%5i?l8%S7r^K)&> z>IF7Cb$LIZ-0b(o#K6T2DQ#D@T(3sufFz)>`r`GxI>&@o_xKC<3oLW)>c!lMx@$Ms z!Z_}By+2nW4Hn`{f>9T}p&(AKzTeTP4ru5h@D|dp&8tCGS0^glSo6mTJ)t7xOO?L~ z9UstrVISX)>)wg*zpJ=R^eFgw3~|kI+e!*oji7p984TbizFO@CF()m}tD;4PesN!o}{N zrq*MYnzje?Im=huvDJp6iQ$GmJD011y7w@(Qhyjdd&G?J5?HJ;qQ1*w_d}eC{*o^U zvEkz~)@n(Zq{WC(iLhYdGWC|XL$3qKyEU6?b=94pUXhdA58cCd+U^dj+M9PMy!e64 z=sW?s>Z0WrK$OX5L^b|P1=D+J&1LE&QbxtPrw6(1!t1Is2&_H?ix;HMgr+~Ti8R;I zqx!Zbqss8T<5r-;$*fiAvWnin@HxZF!j)qIWA(n@|KibxgVk!q|I|Xoz`gH`Q_bB5 zunK20P-~QewYBXK_oTDd{`!gUJBp26)7SSxT@lG^0*i--rI226L-AoN$ywci=TYve zJ1Hw@Lv93QV>jwfVS>a1Cx@rLo?qOtjGvEIe`MZa^ixQ^*X_dBQ-R* zu+cI(%45IC5V*RcDiekNGPxW?m_?dm3V@Kg8>={=)G(1_Ns-g#`I(4`42|$jyLz&i z!WrEJXYd=$gef=4FfuZT40@4Ia>YlrA$d818Q0ckcLFjxysXwBHZa%N`2=*LS3_0+ zRb+K(Jv2Tqq^+-14`EpeT8mohFknI`v4?6ST~fBiZB*SyFD{C~W#pc8NF#heHL!e4 z5=CX6&~DKb+?~BZXxgkUE!O^p;mShm6DD>b0bg{G!S~_{8sYg7n%!OWyw(Zu$nxj? zt-Z+23+Vo@!MzQ7Zr?cI#HZt8w2gz3Wf=6Kf_r0e1BW$L!e25h4fl3)bHx$RBOzGP z=>vpETp=O)gjZaMNQEiB@kHF%VSBs?env)y$NaEVKZ26G;ke!YS4?>YiUa-TP4vN6QJSmITPfVK>}q1KXL^i&m?REw@Pru)l!GZh7%d?#IC zLh-5Dr-E19YEoFCx-*@*fM$-o$g&B{C;vxD@@|X8u+v0Y9ERx3Bj$c%@U;8j;ZZ-x zEu{Fwcj-)KPFn)!A0HqlLV@Xz{eVRB|4Ik}qdZlv&T_eOe{(@l4!}V7X;StxoI{Bn zF}`x86Qo}|N?*G-?KJ?GPCIcp+#>V;D3LF*|4ZN>I^-#izYg=;lYDQg@;{EsKL-BZ zA3VwLgowNubJhRw=YK}meLy{xfoR~>aFAsG3u*svQ@Z()bakx>5?Qas{~n_M`xbxa zW2a0YFcy#NgogtD?t=XfOaCvZ|BKcC<<_wXERq3*m}yG zIPr}1*)2ifn@f)D1)ky95Kp;4)#F(+oAQel;!51&awN&&#ijg%ny^xFIZHze{y8vD zHKEOK)*klpsy{GK$aZb4=Z=44DLc{RkHQ&WU$Tp(Mu+mX?C_fx@dQOIJ)^2}-#e6p znMcPti_cV%f_b?fHI^bYNIRfU4qhoPe<+^=HD5m_Fy8ezW9uP6!Y1BIjoW;-3U)K7 zm3%PVy|bubIbUTW=c-uAsBIp<&hHwbszi*Ll|mo+$H&oqsA}4h525~GHhoG41q*@X zUjS1?z-$SB6D&m@Wt35}!CsVP`=ck*^cB_Rf(E(MF^db+pPzjPo%C}0pmPoo^ht)Z z%EFBfz~Fm^E`H0l|5eQgP2n#A2QaUB6yxn&&GJ<&LFB15{8IQ4?g(+o0PhdAzlj5< zI7*{1Db6)NpZADC-xIi@$pPsv)Ug=l7`5i=220`CtT4dxuysM{(Lmg{UU<7WXK;$! zP%PxzNdk1Pb*N|y8>B#tdJ9c>d3;WXU~Qi>2%Ep3eR65OC|M&zJJ085gNcZ|93tHz zzL+k}Q@vDxNn+diwCW&-H8FdAh*^7iDT?a1q+kLw-Rb!t3pzVe}lV5Rjrj2qefeY zMuD4=pazFAd#tRT6#<%w*6d=5VHLaiiu&KI+CxYJAw;pPwI<&GaHDM58evQ$D@ zOoHd7hm03nxwj@pBmR^&g`mFcbj@Z?866T>P}X#WU}lLQ$!yy{nIh7FrCq=X zY5X=AA-!<^Zqld>tKqE&%thF58|Nv?=ZZ2`OEc0u53{Zyn1&7Z2HjT|pY1E8tGH(| zDB67RlSB;@{_ml!TKta_9$%Mh9Mlm8ho5?Z(M8{DY6gmKFv3nN_B5plHS`H^k-Ur+;Az0y*icv}Qm$&C&& zR2Chnx*wR0{@VziDgr;bZ44662)k|KRdSNF90HAcim6SO0$unke0mBq+hoviPjO!K z0m4n5e@`)V$D)pW&$XfsRaf`i62kSx6?N13*EL`=Tp=gXM;=CBL75tD{dd8j4=4iY zG}QbXz5vBkHiRnv(PY7B`)^D8pw;I}EA>=W>wYH|g?#}Or^2~1(Dm&M1t@{ISX?W! zP^(1|F$53P9JC`rap5kEmTD03f5XcV1F;C^M3DT(Kg!jW2Vak5;`LhwJBa(}q;L)N zXHwX7f+r+XSlI4MWH0b20Ill@#fay7A`qkX9qj}hvOrKU&78NrVe_dE-dGIJzX{8U zrn^^RfX};34}RiE^cXC2?>`sQ)nu@iUg{%@vrwbzdQd$$Zy5+H+$WL<~Lu(Uw-(<@Bf3Te$x&FkH>oA zAL)SqVWjM41P}r)117xx_Pzf$+BXXwNS*z0SK{BvF4*#xp` zJ4cn+KX?4kk^H{QXh4pO7;*WZ@biz+Zrc1S*U7u6e{e^(PfH~hdDLDmU=BM^-+!~v zz&r44W-s;M(D6^grIdwTD#(LqwtF%!737j z55cPQ=+}NXvy;cCCx1s5{i?aQXDzmgZm=1@R+cVq%)gk$Q?gq=VE?@n?)iu&Q`|*X z7aFhA6Es(=t;l}rv4q-@s;p>ZL_X?%eI>Z6j`|ZG8^+{11J+p)U#V^MQ@e!Jr6pBZ z5E1w5pteVN16#Gm$UG@4%Q+(2u$m>#t`4;`0o`;e-0=si`CFe>RwlabW3QJ@0?Avn~>2eQAp)Em1adz)`-cl76>i) zAneGmNUmntfVvo`nX4GMg^uC#Q%?IpT;BR9a)@EcSVwI?XV5Y9z7|TJzJRzFRr|tjRLrhM zPlrp1ztSps+?T!TrVWn9Y;cxzKGv`yTeaY>~bC zE?YcZTwd~r)b=e|DIcmj?qYb|8kKZZ-mJYt5amK~31oi1!X4t`%%^;vocubVnG{aW z%N>Q}4{Ki(-F>YEe`40|!DynGGut-$Bk^f%>?h#`dwc*p({#^d<8dM`PIvIXq=ctP zjvh*avcC&v1qlaK+^Ag(Z@C zV$R|(1gOhZ5awQ_?V2MezUC~m0e)k4U>&OeqItFcuK?4RM`Iuh9&${OA>Um(i*u}O zat-4r54wTQ2c=ckgDpoih)Giq{3MEZ2%;Y}I0yEex|bX@?UKHm=Pavl@U*!}QPs4g zg4#1i5Rt(FUtqS>`Tktqo(u}6kt4d{1j))`FXMYnpnLYqf|J+9=m&CvpvxN@%b-?@ z4_`!ikZ_r3^_O+q|0*&5=zCzJ^Ku2KfljdIIfb}OFe7G}esG)z-d>3tM0qB2EC9I| z9*uRjl|4>8`>wU>pxtaWL7~aMPO+COEGxSp^5%GX#2xV`y{72FZ>-$LT6Ce$@QxQS z78OJaG0n|m;1odgWn7PVpB7VemXN`oMWC(h+Q(=zIl*wtyhD|!7>h6;IPiUz31;O|1yb%^R znts9{$b)JF-)PD*k)(x~Xz2EN?sTmbaP@+9pdpxfSKXVD;5Efn z9|z;b%RCh@@NcPvL`{U89eLzl?bzCLGme!1)yk=;s3?$H3>cr&n%DxB(#WnVc%}K^Ha~gh1`DkXstgP~*UKz{y3#W^VTes9ghrPRnLnSm^tZk^%%$)@! zzyU5%NvSwd^56^I#a(R6?utyKf|X@)8FNl>08UzUgf#w-jKP7k!zx2O8Jz8jI*^2+l1D- zmG9-B3iV(T6bpv6<=}QI``DUCT~DzksC_Mmvm!V_2pa<%l3f6ZH_h2TYg8CcEb;LYN+RJhF zSjfzRuY#(gXAu*|#5@I-#v<1`unK>j0h=bi@)y*SzY8S=m(*^^>1@<29-G(&OI1#5 zvq+j1#ojexl`xb56AkX}y$$w}@(3#C&%sUg4GlB~2lXNN+d(>-a{dU``8oWUnXa{; zjOGO{tm!?G;AxUMl`3{8m5%h*r+F7kD8)&2_LLHfE9%5?#~|LAkb{VAjxzKc)Tu#u z@WHbRPo+UB%PJCED|)o=w9m5SMR^ubUH21yN7HdZI+>oN)ffw2`8S7EHg=X9AW0&k z`r{>nw`b459{SRVh6AJ`)4gNZvQ(3NFfYAqk_l8t$ITh@au}epJ-oo*IgBmuF3Kqx zT~4MMM8~R^45IkTjggs{>MnmS3{@WmfG8o{V(i%>%Al_@SW{Sa5I}ZDwLsGdyWFH| zsVeB&WC;GUqUh-z>TG;J#Xbohs*>q95^BfBXG=edvE@dnsuJ&Ew|>NH2;`k0Wt)uSyhfECI?EVxzSJmC*x@m2>D zjaUBFL-JErixGGn8*cALV`45KR1a{#@N$=C8*=UZo#pDMJ}VK&A<6p76FBSc{2Y`l zb|nul?dYsyb8ZK))HN1r6m31BogknAD;?D3L2NHTaIlOfT05ED{PW3uA;>n zxIZsY0WHoCyC=&*qwg9YNGaIt#c{Hl?NEr)#=*=6MsT@m4qg>%gT?Pc6FIdo-Y%op z9vY`gw1J`$qD#$){1)!A@4TS$3ty+J)?vQ9ih8OaON(?OIETL9Y@!#bV?m9)$~4y; z@bL=b#TESTy`#ZWs;w?aUd&37DtwF4_ff1`D%1+z+HPCAFXm@90TMf!1}~FYNROgN zY(@nT4~Bb$?H!_SHe6@PxS~Ehwy81Z7<6IFG;U30rcp>lW$=u?Pkyd=WcEQJ^xz6my8Mt$%BE$>1yJA@NBZ)>RANMkaij!zm$QdvqK?baWideC zYqDYq`Dqq>&wZO|tS!dycGqK89`59|{k-@xwaZ#`Q&~pSBawaH+vc9{D^f5pKnSJT{sOIuKL*kKPeomz1t=IB3q1@p9AU8Gz3H^ife#r!Ffy>jt+QXL)SpQ&= zZTe)0&5FrreJ!WVS#HC43r%Kc61)uko@w;xA33EZ+cx?-RAj?aOCr$`-S|edt$Mtt zEQzC76O?K$PZd0i%c{Phjb}We;k$*FR9!YFz>rj0tKW(&b!NZwdEC>)GB-}Ksd<``BpPTK};a^BA#*Y+{#44z$2ZQjVHu15=(;Jd0(VGn}ZB`kGKN2Fv z*yE@qWc#gT^F}x{Z=WMd_7w2NLsK@|D`n<3%5nrr7lf`Dcbwa`s~FYlYBUTpML?OO#ZX&~@L zz;r&npBZ{Co7+9m4p@OE@i~)GxS|)w^mVdy_O8_FMcilPGd!ruDK%y$WMR^Yf@L>o z<`#9cx--jQF2;^{S6F!WcT>GOehNl&WC==`m)Z1)dYLqa&%dO0_JmY<848Bve1a9B z^X1A!+#V@*h7!hQewoVhN_XrnyA>(taSE}_S8(`ve+L93%_#8pd+uSm!Eh(es#o8u z5}rNoL#{SB;8dpLfMgr{GVYCOt(1-)>((#G;|yuYWn}uRZ1DI*UmsCJ(AiadP-)oh zqv@EYd2kVey)#ML*(xw#pj~b&k=J9{o;oApe$wW&!;;=7wK%7kiQP}sh|!P@<$Ezf zchdQSgGh@P#-C~hsNtZpAtfz%81i=B5evE@N)|T>sKT1NI|k+YnBAKgJbJN8%xtSx zZU73!;a~BQPyCU)jT1`k4!QKx8F@!l{&UK{WRfz|6MC%4@Y{Avcqlx0{|?kW!V z`=fwIy!=LUQ5--zB|#&x-bqP}zTv%`Gi>q>b^8%}NKbtCQR~Z+-az)hPTD%zr&xB- zrV9E#>}ia8Bb|@)(Xa!NJO0>Q+p{QpTiF|AvWL=2Ep<&S5-g7f^%7q z#O2>@zPM2i&CMz6Rm>)8FKbSzs$9jl#sRy()r%g9cez|btu~PKXsej7hl+84*<1|6 zaJRIeSd5b3(qTP_Ly+iTm5v!xD_h49Xk>wSH(SvJJ#Z5<>QO>zFHTE=ZU}m zqPrC;3t<^q?R`3M!F_HH~$5d=RYdrVHh2CaYy2TrgmKK3tic^`b5QDz==) z?I?9ivNu|Em8>SR%=5gF9TRIE7wb9QB3ULFIcEOjX7^(fr?FF?7g{Rq4|OwBOH-`+ zZ+K4MxqbFKM|v-V3|qh9B^UtA^M{&Gp68DY9-d4wwObUh_B}GM=uNhOA+bNgEIvZr zutGvFUk`a_Q0QW%`wB=OPQt-lc62$Y3J$+vBAnAZ;-*Ics2J62^kf?A9TT=XsgbL7 z6=OWXv(tmIgCqi^uMJoL3fDDdvk@g;wCS1ODQJrM#mzmdj&nj7`nmkv2C*zx?LORB znVMF_H8Sa#1z*Z)#1ivNGtWp|P@4s}Y{kV+E2EJ*1{b1m%_~nxO!k~_jEk73A}D{W zD-tnowR04hC9rNTqy9v5s-m1)k|TCRb0;;DyjoKGdaw#qZf}64T_jEJak9lG_~uM|t#C03qeR>uv>5TadV9BD1#Aq@A~h z7l!#F%x zt+zor%f=-&T2gE(=ks}&*VTos_Xx&KfgNLka8vz%*n7*UIGbi&I1t=5xC9UG5Zqlt z2=4Cg4#6D)!JS~i-CYKEcXt~G8T=#f^JMSyXPRqJ^Fl&PTlm1$Kfqj zvbNV3MQZGbNDOio{&AdmV%lH(Y6hSRcIyNom6delx1+JQ6)|R&1!(!(v{|gIaSKsy z#S@|YYe2)7W8;LU!yKf~`cy9RGN^;YH& z9jtQ+3K!qeh2^8Eh8|c?(`+q=GyXbUhB_6F)~GV=GuW{v`=c@dP1*>?gjV#o&p zM3De?)Gzg!x8rLov&jU^i9fe@<^a_q_)&>6!cFuvZPs5?*e}fsidxXUzSdS9>tn}c z(!>#)e{w%j3%{&aLn#~N^~LaeWSt?N1>Mf>K33A--%DH-oR0Tsk4 zm#U)Xh{5EeMKX(s*?JFekU|kj>LoLIXKzU?5che?iH-q$G2AN^l^O!|8p51Mzxp&4 z(fda2=P(^~z=VA?D(?Ky&q{eP?z03|pw%|{Ebi_h&ni+$61X^!?^k7*TPc2#B~yj49+jQ% z#x1!#+U@$d1JXK2n;p(=eqtqb9-AHFEVuE`JS7*=aMr)+h&KEPD{V?pkD-SSp7VfT z)qb=j=2u)-&;E=kC^&n*_>jDCIL8b6naAwvkd5&JzktzIa@vR&|cf<*a zkbdl{5lW)$s)-XMSt?+t8aeL?DM)4tdiN0xLy8~W77akkbGW12da5Ea^ni(M@`_yDi^WilSoNaZT zxPElMitM$=0-{Epy^r_NzNvlN5!5RaDP0H%zAtGqa$zN;ioVYXLg&A-87WE5#E*7y z&f1@0Tbeu*?>59ghBtE8nF zxzVl~;#H9i+u3d_rspXNB_{Nzm~sT1t0sa2jeDY$M=t1ru5Jb=^S$6P-)v460R3FX zsKVB)-Zxm& z^>^$u@?DR9+&$>qnum4}ec)tTK4j)u+U*;LE>YKs6VgJo5tZ}uxc?5dT+stua(fr8 zjd#0C7wldz8W~h(yxPthA@X8*UwZSfd;0-dT)a8+iK^|*5P-~65 zNp)%JD#Vc_0aPh!yUM1#vhi#jmzDz2YAM*AAOcD>G7=K})mJLSLlJ>H{>n`3SQk3U zfs}aYgW>i$#f9Mt^UChcNv^}>Kiy635CmEndmWKHHKq^h&m@5%8zqfLd4$?ONJ9Gu zUcRlwl&>h-_v_#&Bu;iT=M!c(TQeRa5>1D2ljCl7cu9VI0YU3!mEoFVv^ttl&5uV) zu&@1kRGc#yN#EmMoDPP~_Cw0mGR5R6M6rKxVUyTvtJ^@#~0m4(=yclnZ)$TsxYJC}Vyw;d_FlRjZe;G^m; zs9(FFk&z2u(9;<8<*TsOP}o42nFK2}8~*PBf(R?lae$ysQedC`#xz*v?S_6d zM@VY?_Qj#%GYOtDjMb0qHM=?fkn}bAs|Slaiwl+G;;y8I)bF4-Iv0Hu9XnQ0I{{X&f$Ht{d8d+}p6F9bTTZ9Vy;U!H9)pCs zn0@92V+GvObf|43L8VMXqHh;% zbc}z+C3IORrz`Qn6Ste5n1P$Ko3kNKOPlpQ)}5z!P4zFfrM5L{nc`*29!X616`$FN zym8W5yn7b;=6ttJk>5(Uvd`M{{JOwoF)S|6F@J8t8*V*DSZ@(oqSkV(C+y`@%jixKh04-1)$<=KU9|G?wGkdZGWd+ z0{1W=R*}ZL+(Z$DxZDtyHIH(5G`ZVgd1|y)R$Qh`R3~EroKrmByC!e7Bl{73zzgS~ z+jp4K;Lv?iW+C!Ipj=>(5GNy%fli9AU9sQ&+9}H< zCERg9>f08$=W$G+F}%;+@f_bzxCdU&&7{86rm2-q!Gg0URuoi7s^1oeu_7upOk&mr zP>!&wJDQEh2~~I8qHxQ*1{0&I=`sRf8Pj^L5e3Ga(IYAK(&l2ePy7>J#ZQz4pr$*PR!WW3`gGW^!01N(9G*@-TtHCyw#@I2I^4`}-5!^2zgt6C^S)Uu{qn646DHb%S8%nUPJD5nH9M3#6_t zHuaVERw?Enzw~xCpO2;!W@79}P=rqzz z>}gQ?s3^DEoo#Pj2!+M%;G9Apzp2}3u%TpH1_RSg7)E*pwbM8#e3E94gkQGlSAENf z0d&JkOT*XD>>1efMEDH*2KW)kQM;Y?3bS1@5WU=Nf&?U^Gyt{VC#gKMku^L?+6uQs zE7SXxMp9F$Xd~J;4JRiCz-_AyAC`ID12Jx7%|eNs%KFW4!0}`cPBU}~xN8rj&Ng{5 z%f@@@g`WK*Dt1i(we$G_y)e=o@Ueo>`bb*1jUP3zlI@x9KgNs{k}X0=Nt(Q)r*3sX zjwQ#)n$3B#spcr;?KPpA?zuY!HMse5+Aa=B0^s15E#Kp{?ds^9S%FvVcD<(F_kzl` z`k2x_>m*vgYzb(MXibaVBL68Fru+J70Asoxv=H)~Oq6G*J9bA$E2Lhos^?G5lCqM> zn0BEbC0+1&_IJ_HDMm90*X|!H^#v-lL_}wVO=XI6YL%QcJe?B8c~ILbmvYy^`t{fpiaCZeO>B`hBtRdn0@ z>NVf8Os5tsZrAqSJ4q($KTO>?s&%>muI|$KQ*yNYR z3u#|%MaiiR2)TSKlp`FzMS zRxJ*Ru+5+;eS-m@)eTDN2)O}AS+O&>E{f7~aXJs&OuD|MV_L26P}}@I_V9~pV3%nG zZqT1q$?Q{5i;-uPa>C-D=~_TX0;_iu%DNY0U6(6bo{3uocw|<1y+LSyp-JWFR_(UvpOe4LknUY^- z0a7VxjAav1Vt;O3AMxRuh+S`UE>@n{2oDTNgT@<@^U;VHJ{HbJ=MGJfy(Y&65}~7i z7hpmr9z9y?uanea_jCqT=qc+4yi7Dzy?e*6C@m)Zjo+Cf%~XQ3Lk~^&bWs-oCY+y@ zjTND?YN_LFgq@Q@WRihAth=6P`)C3x?ALOUzpZ9M^n4PEl|tYnxPi5X3=J@=@{@fWJE_#Dn)OK<+$>d2-?L^+R<0jO~__!FEN?KsLl&nr;*G4;C(JW*hLnbBb(+eB~J zSl{_AQO{2hxb!iLhTikBenP)E_}#$Y53o-}?|Z$BK0q7rDoP$N zs#jwi7}E$CH1@JHTP{>xR<$~caJMW#Dx4)&p)0?&Drix_SCp_UKr76_-<}0!FN>c@ zf29RE5?hOtSgkmxZ!>)pbD1M5uDJF%NwU$5D&Q^7B`)2&-29S%ouh})5klVty<(j=Yc9C7+)7$SGr#LtpkRGe7VZ5sK@2RQ`O zEPB^i&!Y1-;C+T%HRMONk>22n?*DT0_)<~7@#h{IA0y_N5uKX1sMP^tM zzs2BG6#l`qPpOMbPRPw|)V&G&2T>HFok(H{^TvCZO&3ISTmq|nu!Tv73!P;&;0fGV zX-G1o8w00#pzpQp`DK_rt&oVx#_)b)G(h`!za5Te=ZB#+-N9(OsuB7ocj3*itJ+9% zkdmANgzbn;mnij$lK7U(grc6fw>QoF!9YGK9mLY7DfQ8|Lp+DT=i3ICfDWDpUu$0V z?_RXO(KyJoN4>|t_jV9{V#Z6AJSWGts1(|gyM|`1d+I^6sQnLwn z*>FBo`L3vlb8rvwu(mrNCQgXe+`_HiM0k`=9?FA7E0En>CquC zAT{F!a%i6#vCyIAr|4pu*RP{Ak+id)e%V&Hfyj)=ueYBUj44nW6*h5FAaMCY5iMvo z)6xvN*);@%rP`R2#-TqchD+$MSd0Xh6xC_SlzsQVw{O&~m6Fyc19t{Ngns5c=W^YpL}lk$lUUI5m}Io%)Gz zP%|;AErpfmwq;UXH&A3fI~kx9cEf1^Qq2|^8kQFknDN5r{UZ22YvpM6YLDr-Tg|rv z`?K5*ZW#xM3rM>|@iv(Q0u;(~D|b9L8PE)Mvh~!qu(DUsIc=sNIT2;_+&+c>zUM&6 z?Tmt$M^cMtz%Eg%vEtpUH;a?aQj}eLrVjTv_0WU6Z~C$x)M@G$=LE5B!LKbwpIBkO zrX+YdvE6>)+J>?TRJ z6O(lK;2G+E4lijPu>U)vwQ2M-Ox<2SK2yqsws zP2$%7;98VV4{4rS8Jsv>Hut@%+Izi?;j{f^pPDbPyC|4Bd8TwKUc@h{=fy$n4rb1x zw-!g^Zy8UL z5~XmaW7cqhbV1I?2=n=fJaG1Ef2=#LA09kadAAiiD

kXLtgnT5X}9!q;%J`RseY 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 I&MBb@0PwD*2LJ#7 diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index 6cd6657a0aaeb8..99cfd12eeade9c 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -1,28 +1,27 @@ [[managing-licenses]] == License Management -When you install the default distribution of {kib}, you receive a basic license -with no expiration date. For the full list of free features that are included in -the basic license, refer to https://www.elastic.co/subscriptions[the subscription page]. +When you install the default distribution of {kib}, you receive free features +with no expiration date. For the full list of features, refer to +{subscriptions}. -If you want to try out the full set of platinum features, you can activate a -30-day trial license. To view the -status of your license, start a trial, or install a new license, open the menu, then go to *Stack Management > {es} > License Management*. +If you want to try out the full set of features, you can activate a free 30-day +trial. To view the status of your license, start a trial, or install a new +license, open the menu, then go to *Stack Management > {es} > License Management*. NOTE: You can start a trial only if your cluster has not already activated a trial license for the current major product version. For example, if you have already activated a trial for 6.0, you cannot start a new trial until -7.0. You can, however, contact `info@elastic.co` to request an extended trial -license. +7.0. You can, however, request an extended trial at {extendtrial}. When you activate a new license level, new features appear in *Stack Management*. [role="screenshot"] image::images/management-license.png[] -At the end of the trial period, the platinum features operate in a -<>. You can revert to a basic license, -extend the trial, or purchase a subscription. +At the end of the trial period, some features operate in a +<>. You can revert to Basic, extend the trial, +or purchase a subscription. TIP: If {security-features} are enabled, unless you have a trial license, you must configure Transport Layer Security (TLS) in {es}. From c7f3d9219f433078d52d398b1ad2ef37811e43ee Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Thu, 9 Jul 2020 16:37:58 -0400 Subject: [PATCH 14/15] [Security_Solution][Resolver]Add beta badge to Resolver panel (#71183) * Add beta badge to Resolver panel --- .../view/panels/panel_content_utilities.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 517b8478556478..b5c4e6481216c1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { EuiBreadcrumbs, Breadcrumb, EuiCode } from '@elastic/eui'; +import { EuiBreadcrumbs, Breadcrumb, EuiCode, EuiBetaBadge } from '@elastic/eui'; import styled from 'styled-components'; import React, { memo } from 'react'; import { useResolverTheme } from '../assets'; @@ -19,6 +19,10 @@ export const BoldCode = styled(EuiCode)` } `; +const BetaHeader = styled(`header`)` + margin-bottom: 1em; +`; + /** * The two query parameters we read/write on to control which view the table presents: */ @@ -40,6 +44,13 @@ const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: str } `; +const betaBadgeLabel = i18n.translate( + 'xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel', + { + defaultMessage: 'BETA', + } +); + /** * Breadcrumb menu with adjustments per direction from UX team */ @@ -54,12 +65,17 @@ export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({ colorMap: { resolverBreadcrumbBackground, resolverEdgeText }, } = useResolverTheme(); return ( - + <> + + + + + ); }); From a32b9e89b65314d5277d15fd7c522371290a9802 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 9 Jul 2020 16:50:00 -0400 Subject: [PATCH 15/15] Support multiple features declaring same properties (#71106) Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../capabilities_switcher.test.ts | 51 ++++++++++++++++++- .../capabilities/capabilities_switcher.ts | 44 ++++++++++++---- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index babd25dd3ec4b2..797d7fd1bdcc4e 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -59,6 +59,27 @@ const features = ([ }, }, }, + { + // feature 4 intentionally delcares the same items as feature 3 + id: 'feature_4', + name: 'Feature 4', + navLinkId: 'feature3', + app: ['feature3', 'feature3_app'], + catalogue: ['feature3Entry'], + management: { + kibana: ['indices'], + }, + privileges: { + all: { + app: [], + ui: [], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, ] as unknown) as Feature[]; const buildCapabilities = () => @@ -73,6 +94,7 @@ const buildCapabilities = () => catalogue: { discover: true, visualize: false, + feature3Entry: true, }, management: { kibana: { @@ -217,11 +239,38 @@ describe('capabilitiesSwitcher', () => { expect(result).toEqual(expectedCapabilities); }); + it('does not disable catalogue, management, or app entries when they are shared with an enabled feature', async () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_3'], + }; + + const capabilities = buildCapabilities(); + + const { switcher } = setup(space); + const request = httpServerMock.createKibanaRequest(); + const result = await switcher(request, capabilities); + + const expectedCapabilities = buildCapabilities(); + + // These capabilities are shared by feature_4, which is enabled + expectedCapabilities.navLinks.feature3 = true; + expectedCapabilities.navLinks.feature3_app = true; + expectedCapabilities.catalogue.feature3Entry = true; + expectedCapabilities.management.kibana.indices = true; + // These capabilities are only exposed by feature_3, which is disabled + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + }); + it('can disable everything', async () => { const space: Space = { id: 'space', name: '', - disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + disabledFeatures: ['feature_1', 'feature_2', 'feature_3', 'feature_4'], }; const capabilities = buildCapabilities(); diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 05d04295964892..00e2419136f488 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -54,22 +54,38 @@ function toggleDisabledFeatures( ) { const disabledFeatureKeys = activeSpace.disabledFeatures; - const disabledFeatures = disabledFeatureKeys - .map((key) => features.find((feature) => feature.id === key)) - .filter((feature) => typeof feature !== 'undefined') as Feature[]; + const [enabledFeatures, disabledFeatures] = features.reduce( + (acc, feature) => { + if (disabledFeatureKeys.includes(feature.id)) { + return [acc[0], [...acc[1], feature]]; + } + return [[...acc[0], feature], acc[1]]; + }, + [[], []] as [Feature[], Feature[]] + ); const navLinks = capabilities.navLinks; const catalogueEntries = capabilities.catalogue; const managementItems = capabilities.management; + const enabledAppEntries = new Set(enabledFeatures.flatMap((ef) => ef.app ?? [])); + const enabledCatalogueEntries = new Set(enabledFeatures.flatMap((ef) => ef.catalogue ?? [])); + const enabledManagementEntries = enabledFeatures.reduce((acc, feature) => { + const sections = Object.entries(feature.management ?? {}); + sections.forEach((section) => { + if (!acc.has(section[0])) { + acc.set(section[0], []); + } + acc.get(section[0])!.push(...section[1]); + }); + return acc; + }, new Map()); + for (const feature of disabledFeatures) { // Disable associated navLink, if one exists - if (feature.navLinkId && navLinks.hasOwnProperty(feature.navLinkId)) { - navLinks[feature.navLinkId] = false; - } - - feature.app.forEach((app) => { - if (navLinks.hasOwnProperty(app)) { + const featureNavLinks = feature.navLinkId ? [feature.navLinkId, ...feature.app] : feature.app; + featureNavLinks.forEach((app) => { + if (navLinks.hasOwnProperty(app) && !enabledAppEntries.has(app)) { navLinks[app] = false; } }); @@ -77,18 +93,24 @@ function toggleDisabledFeatures( // Disable associated catalogue entries const privilegeCatalogueEntries = feature.catalogue || []; privilegeCatalogueEntries.forEach((catalogueEntryId) => { - catalogueEntries[catalogueEntryId] = false; + if (!enabledCatalogueEntries.has(catalogueEntryId)) { + catalogueEntries[catalogueEntryId] = false; + } }); // Disable associated management items const privilegeManagementSections = feature.management || {}; Object.entries(privilegeManagementSections).forEach(([sectionId, sectionItems]) => { sectionItems.forEach((item) => { + const enabledManagementEntriesSection = enabledManagementEntries.get(sectionId); if ( managementItems.hasOwnProperty(sectionId) && managementItems[sectionId].hasOwnProperty(item) ) { - managementItems[sectionId][item] = false; + const isEnabledElsewhere = (enabledManagementEntriesSection ?? []).includes(item); + if (!isEnabledElsewhere) { + managementItems[sectionId][item] = false; + } } }); });