From fe86a86d14718ab510d60ecb1c091fefb83d3695 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 3 Feb 2020 11:02:29 +0000 Subject: [PATCH 01/17] adds the AlertDetails page (#55671) This PR adds an Alerts Details page, linked from the Alerts List. It includes: Header containing details about the Alert. Quick Enable / Mute buttons for the Alert Disabled buttons to the _Edit Alert+ flyout (waiting on #51545), View in App (waiting on #56298) and Activity Log (waiting on #51548) --- .../np_ready/public/application/app.tsx | 10 +- .../public/application/constants/index.ts | 1 + .../lib/action_connector_api.test.ts | 1 + .../public/application/lib/alert_api.test.ts | 101 ++++ .../public/application/lib/alert_api.ts | 40 +- .../application/lib/value_validators.test.ts | 77 +++ .../application/lib/value_validators.ts | 33 ++ .../action_connector_form.test.tsx | 6 +- .../action_type_menu.test.tsx | 4 +- .../connector_add_flyout.test.tsx | 4 +- .../connector_edit_flyout.test.tsx | 2 +- .../components/alert_details.test.tsx | 519 ++++++++++++++++++ .../components/alert_details.tsx | 176 ++++++ .../components/alert_details_route.test.tsx | 409 ++++++++++++++ .../components/alert_details_route.tsx | 118 ++++ .../components/alerts_list.test.tsx | 9 +- .../alerts_list/components/alerts_list.tsx | 45 +- .../components/collapsed_item_actions.tsx | 32 +- .../components/alert_quick_edit_buttons.tsx} | 189 +++---- .../components/bulk_operation_popover.tsx | 42 ++ .../with_actions_api_operations.test.tsx | 51 ++ .../with_actions_api_operations.tsx | 28 + .../with_bulk_alert_api_operations.test.tsx | 269 +++++++++ .../with_bulk_alert_api_operations.tsx | 103 ++++ .../np_ready/public/types.ts | 12 +- .../apps/triggers_actions_ui/details.ts | 146 +++++ .../apps/triggers_actions_ui/home_page.ts | 38 ++ .../apps/triggers_actions_ui/index.ts | 1 + .../fixtures/plugins/alerts/index.ts | 43 +- .../page_objects/alert_details.ts | 26 + .../page_objects/index.ts | 2 + .../page_objects/triggers_actions_ui_page.ts | 4 + .../services/alerting/actions.ts | 48 ++ .../services/alerting/alerts.ts | 79 +++ .../services/alerting/index.ts | 22 + .../functional_with_es_ssl/services/index.ts | 2 + 36 files changed, 2527 insertions(+), 165 deletions(-) create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx rename x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/{alerts_list/components/bulk_action_popover.tsx => common/components/alert_quick_edit_buttons.tsx} (52%) create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx create mode 100644 x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts create mode 100644 x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts create mode 100644 x-pack/test/functional_with_es_ssl/services/alerting/actions.ts create mode 100644 x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts create mode 100644 x-pack/test/functional_with_es_ssl/services/alerting/index.ts diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx index 57e6fc4a9e18b..7d9a963c9c6b3 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx @@ -13,12 +13,13 @@ import { IUiSettingsClient, ApplicationStart, } from 'kibana/public'; -import { BASE_PATH, Section } from './constants'; +import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { TriggersActionsUIHome } from './home'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; import { LegacyDependencies, ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; +import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; export interface AppDeps { chrome: ChromeStart; @@ -53,11 +54,8 @@ export const AppWithoutRouter = ({ sectionsRegex }: any) => { const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( - + + {canShowAlerts && } ); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts index a8364ffe21019..11b094dea0e62 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts @@ -13,6 +13,7 @@ export type Section = 'connectors' | 'alerts'; export const routeToHome = `${BASE_PATH}`; export const routeToConnectors = `${BASE_PATH}/connectors`; export const routeToAlerts = `${BASE_PATH}/alerts`; +export const routeToAlertDetails = `${BASE_PATH}/alert/:alertId`; export { TIME_UNITS } from './time_units'; export enum SORT_ORDERS { diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts index bc2949917edea..00a55bb2588bb 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts @@ -24,6 +24,7 @@ describe('loadActionTypes', () => { { id: 'test', name: 'Test', + enabled: true, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts index 0106970cf9c38..35d1a095188de 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts @@ -8,15 +8,22 @@ import { Alert, AlertType } from '../../types'; import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createAlert, + deleteAlert, deleteAlerts, disableAlerts, enableAlerts, + disableAlert, + enableAlert, + loadAlert, loadAlerts, loadAlertTypes, muteAlerts, unmuteAlerts, + muteAlert, + unmuteAlert, updateAlert, } from './alert_api'; +import uuid from 'uuid'; const http = httpServiceMock.createStartContract(); @@ -42,6 +49,31 @@ describe('loadAlertTypes', () => { }); }); +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: alertId, + name: 'name', + tags: [], + enabled: true, + alertTypeId: '.noop', + schedule: { interval: '1s' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + }); +}); + describe('loadAlerts', () => { test('should call find API with base parameters', async () => { const resolvedValue = { @@ -230,6 +262,19 @@ describe('loadAlerts', () => { }); }); +describe('deleteAlert', () => { + test('should call delete API for alert', async () => { + const id = '1'; + const result = await deleteAlert({ http, id }); + expect(result).toEqual(undefined); + expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alert/1", + ] + `); + }); +}); + describe('deleteAlerts', () => { test('should call delete API for each alert', async () => { const ids = ['1', '2', '3']; @@ -335,6 +380,62 @@ describe('updateAlert', () => { }); }); +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_enable", + ], + ] + `); + }); +}); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_disable", + ], + ] + `); + }); +}); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_mute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_unmute_all", + ], + ] + `); + }); +}); + describe('enableAlerts', () => { test('should call enable alert API per alert', async () => { const ids = ['1', '2', '3']; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts index 0b4f5731c1315..acc318bd5fbea 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts @@ -12,6 +12,16 @@ export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise { + return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); +} + export async function loadAlerts({ http, page, @@ -55,6 +65,10 @@ export async function loadAlerts({ }); } +export async function deleteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.delete(`${BASE_ALERT_API_PATH}/${id}`); +} + export async function deleteAlerts({ ids, http, @@ -62,7 +76,7 @@ export async function deleteAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.delete(`${BASE_ALERT_API_PATH}/${id}`))); + await Promise.all(ids.map(id => deleteAlert({ http, id }))); } export async function createAlert({ @@ -91,6 +105,10 @@ export async function updateAlert({ }); } +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`); +} + export async function enableAlerts({ ids, http, @@ -98,7 +116,11 @@ export async function enableAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`))); + await Promise.all(ids.map(id => enableAlert({ id, http }))); +} + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`); } export async function disableAlerts({ @@ -108,11 +130,19 @@ export async function disableAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`))); + await Promise.all(ids.map(id => disableAlert({ id, http }))); +} + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`); } export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`))); + await Promise.all(ids.map(id => muteAlert({ http, id }))); +} + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`); } export async function unmuteAlerts({ @@ -122,5 +152,5 @@ export async function unmuteAlerts({ ids: string[]; http: HttpSetup; }): Promise { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`))); + await Promise.all(ids.map(id => unmuteAlert({ id, http }))); } diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts new file mode 100644 index 0000000000000..90f575d9391b3 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.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 { throwIfAbsent, throwIfIsntContained } from './value_validators'; +import uuid from 'uuid'; + +describe('throwIfAbsent', () => { + test('throws if value is absent', () => { + [undefined, null].forEach(val => { + expect(() => { + throwIfAbsent('OMG no value')(val); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + }); + + test('doesnt throws if value is present but falsey', () => { + [false, ''].forEach(val => { + expect(throwIfAbsent('OMG no value')(val)).toEqual(val); + }); + }); + + test('doesnt throw if value is present', () => { + expect(throwIfAbsent('OMG no value')({})).toEqual({}); + }); +}); + +describe('throwIfIsntContained', () => { + test('throws if value is absent', () => { + expect(() => { + throwIfIsntContained(new Set([uuid.v4()]), 'OMG no value', val => val)([uuid.v4()]); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + + test('throws if value is absent using custom message', () => { + const id = uuid.v4(); + expect(() => { + throwIfIsntContained( + new Set([id]), + (value: string) => `OMG no ${value}`, + val => val + )([uuid.v4()]); + }).toThrow(`OMG no ${id}`); + }); + + test('returns values if value is present', () => { + const id = uuid.v4(); + const values = [uuid.v4(), uuid.v4(), id, uuid.v4()]; + expect(throwIfIsntContained(new Set([id]), 'OMG no value', val => val)(values)).toEqual( + values + ); + }); + + test('returns values if multiple values is present', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [uuid.v4(), uuid.v4(), secondId, uuid.v4(), firstId]; + expect( + throwIfIsntContained(new Set([firstId, secondId]), 'OMG no value', val => val)(values) + ).toEqual(values); + }); + + test('allows a custom value extractor', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [ + { id: firstId, some: 'prop' }, + { id: secondId, someOther: 'prop' }, + ]; + expect( + throwIfIsntContained<{ id: string }>( + new Set([firstId, secondId]), + 'OMG no value', + (val: { id: string }) => val.id + )(values) + ).toEqual(values); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts new file mode 100644 index 0000000000000..7ee7359086406 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.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 { constant } from 'lodash'; + +export function throwIfAbsent(message: string) { + return (value: T | undefined): T => { + if (value === undefined || value === null) { + throw new Error(message); + } + return value; + }; +} + +export function throwIfIsntContained( + requiredValues: Set, + message: string | ((requiredValue: string) => string), + valueExtractor: (value: T) => string +) { + const toError = typeof message === 'function' ? message : constant(message); + return (values: T[]) => { + const availableValues = new Set(values.map(valueExtractor)); + for (const value of requiredValues.values()) { + if (!availableValues.has(value)) { + throw new Error(toError(value)); + } + } + return values; + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx index 6896ac954bb06..f27f7d8c3054d 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -82,7 +82,11 @@ describe('action_connector_form', () => { editFlyoutVisible: false, setEditFlyoutVisibility: () => {}, actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'my-action-type-name' }, + 'my-action-type': { + id: 'my-action-type', + name: 'my-action-type-name', + enabled: true, + }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx index 6ef2f62315d9a..6d98a5e3d120f 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -75,8 +75,8 @@ describe('connector_add_flyout', () => { editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first' }, - 'second-action-type': { id: 'second-action-type', name: 'second' }, + 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, + 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 71ba52f047d61..a03296c7c3679 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -58,7 +58,9 @@ describe('connector_add_flyout', () => { setAddFlyoutVisibility: state => {}, editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, - actionTypesIndex: { 'my-action-type': { id: 'my-action-type', name: 'test' } }, + actionTypesIndex: { + 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + }, reloadConnectors: () => { return new Promise(() => {}); }, diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 57e950a98eb2a..0dc38523bfab8 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -84,7 +84,7 @@ describe('connector_edit_flyout', () => { editFlyoutVisible: true, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'test-action-type-id': { id: 'test-action-type-id', name: 'test' }, + 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true }, }, reloadConnectors: () => { return new Promise(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx new file mode 100644 index 0000000000000..228bceb87cad7 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx @@ -0,0 +1,519 @@ +/* + * 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 * as React from 'react'; +import uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { AlertDetails } from './alert_details'; +import { Alert, ActionType } from '../../../../types'; +import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui'; +import { times, random } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; + +jest.mock('../../../app_context', () => ({ + useAppDependencies: jest.fn(() => ({ + http: jest.fn(), + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), +})); + +jest.mock('../../../lib/capabilities', () => ({ + hasSaveAlertsCapability: jest.fn(() => true), +})); + +const mockAlertApis = { + muteAlert: jest.fn(), + unmuteAlert: jest.fn(), + enableAlert: jest.fn(), + disableAlert: jest.fn(), +}; + +// const AlertDetails = withBulkAlertOperations(RawAlertDetails); +describe('alert_details', () => { + // mock Api handlers + + it('renders the alert name as a title', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + +

{alert.name}

+
+ ) + ).toBeTruthy(); + }); + + it('renders the alert type badge', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement({alertType.name}) + ).toBeTruthy(); + }); + + describe('actions', () => { + it('renders an alert action', () => { + const alert = mockAlert({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + }, + ]; + + expect( + shallow( + + ).containsMatchingElement( + + {actionTypes[0].name} + + ) + ).toBeTruthy(); + }); + + it('renders a counter for multiple alert action', () => { + const actionCount = random(1, 10); + const alert = mockAlert({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ...times(actionCount, () => ({ + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.email', + })), + ], + }); + const alertType = { + id: '.noop', + name: 'No Op', + }; + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + }, + { + id: '.email', + name: 'Send email', + enabled: true, + }, + ]; + + const details = shallow( + + ); + + expect( + details.containsMatchingElement( + + {actionTypes[0].name} + + ) + ).toBeTruthy(); + + expect( + details.containsMatchingElement( + + {`+${actionCount}`} + + ) + ).toBeTruthy(); + }); + }); + + describe('links', () => { + it('links to the Edit flyout', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + + + + ) + ).toBeTruthy(); + }); + + it('links to the app that created the alert', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + + + + ) + ).toBeTruthy(); + }); + + it('links to the activity log', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + + ).containsMatchingElement( + + + + ) + ).toBeTruthy(); + }); + }); +}); + +describe('enable button', () => { + it('should render an enable button when alert is enabled', () => { + const alert = mockAlert({ + enabled: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: true, + disabled: false, + }); + }); + + it('should render an enable button when alert is disabled', () => { + const alert = mockAlert({ + enabled: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: false, + }); + }); + + it('should enable the alert when alert is disabled and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const disableAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(disableAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(disableAlert).toHaveBeenCalledTimes(1); + }); + + it('should disable the alert when alert is enabled and button is clicked', () => { + const alert = mockAlert({ + enabled: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(enableAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(enableAlert).toHaveBeenCalledTimes(1); + }); +}); + +describe('mute button', () => { + it('should render an mute button when alert is enabled', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: false, + }); + }); + + it('should render an muted button when alert is muted', () => { + const alert = mockAlert({ + enabled: true, + muteAll: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: true, + disabled: false, + }); + }); + + it('should mute the alert when alert is unmuted and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const muteAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(muteAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(muteAlert).toHaveBeenCalledTimes(1); + }); + + it('should unmute the alert when alert is muted and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + muteAll: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const unmuteAlert = jest.fn(); + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(unmuteAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(unmuteAlert).toHaveBeenCalledTimes(1); + }); + + it('should disabled mute button when alert is disabled', () => { + const alert = mockAlert({ + enabled: false, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: true, + }); + }); +}); + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx new file mode 100644 index 0000000000000..ffdf846efd49d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx @@ -0,0 +1,176 @@ +/* + * 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, { useState } from 'react'; +import { indexBy } from 'lodash'; +import { + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiPage, + EuiPageContentBody, + EuiButtonEmpty, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useAppDependencies } from '../../../app_context'; +import { hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; + +type AlertDetailsProps = { + alert: Alert; + alertType: AlertType; + actionTypes: ActionType[]; +} & Pick; + +export const AlertDetails: React.FunctionComponent = ({ + alert, + alertType, + actionTypes, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, +}) => { + const { capabilities } = useAppDependencies(); + + const canSave = hasSaveAlertsCapability(capabilities); + + const actionTypesByTypeId = indexBy(actionTypes, 'id'); + const [firstAction, ...otherActions] = alert.actions; + + const [isEnabled, setIsEnabled] = useState(alert.enabled); + const [isMuted, setIsMuted] = useState(alert.muteAll); + + return ( + + + + + + +

{alert.name}

+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + {alertType.name} + + {firstAction && ( + + + {actionTypesByTypeId[firstAction.actionTypeId].name ?? + firstAction.actionTypeId} + + + )} + {otherActions.length ? ( + + +{otherActions.length} + + ) : null} + + + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(alert); + } else { + setIsEnabled(true); + await enableAlert(alert); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + }} + label={ + + } + /> + + + + + +
+
+
+ ); +}; + +export const AlertDetailsWithApi = withBulkAlertOperations(AlertDetails); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx new file mode 100644 index 0000000000000..7a40104e97d9f --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx @@ -0,0 +1,409 @@ +/* + * 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 * as React from 'react'; +import uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { createMemoryHistory, createLocation } from 'history'; +import { ToastsApi } from 'kibana/public'; +import { AlertDetailsRoute, getAlertData } from './alert_details_route'; +import { Alert } from '../../../../types'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +jest.mock('../../../app_context', () => { + const toastNotifications = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ toastNotifications })), + }; +}); +describe('alert_details_route', () => { + it('render a loader while fetching data', () => { + const alert = mockAlert(); + + expect( + shallow( + + ).containsMatchingElement() + ).toBeTruthy(); + }); +}); + +describe('getAlertData useEffect handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches alert', async () => { + const alert = mockAlert(); + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementationOnce(async () => alert); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlert).toHaveBeenCalledWith(alert.id); + expect(setAlert).toHaveBeenCalledWith(alert); + }); + + it('fetches alert and action types', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlertTypes).toHaveBeenCalledTimes(1); + expect(loadActionTypes).toHaveBeenCalledTimes(1); + + expect(setAlertType).toHaveBeenCalledWith(alertType); + expect(setActionTypes).toHaveBeenCalledWith([actionType]); + }); + + it('displays an error if the alert isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => { + throw new Error('OMG'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG', + }); + }); + + it('displays an error if the alert type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => { + throw new Error('OMG no alert type'); + }); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no alert type', + }); + }); + + it('displays an error if the action type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => { + throw new Error('OMG no action type'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no action type', + }); + }); + + it('displays an error if the alert type isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Alert Type: ${alert.alertTypeId}`, + }); + }); + + it('displays an error if an action type isnt found', async () => { + const availableActionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const missingActionType = { + id: '.noop', + name: 'No Op', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: availableActionType.id, + params: {}, + }, + { + group: '', + id: uuid.v4(), + actionTypeId: missingActionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [availableActionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Action Type: ${missingActionType.id}`, + }); + }); +}); + +function mockApis() { + return { + loadAlert: jest.fn(), + loadAlertTypes: jest.fn(), + loadActionTypes: jest.fn(), + }; +} + +function mockStateSetter() { + return { + setAlert: jest.fn(), + setAlertType: jest.fn(), + setActionTypes: jest.fn(), + }; +} + +function mockRouterProps(alert: Alert) { + return { + match: { + isExact: false, + path: `/alert/${alert.id}`, + url: '', + params: { alertId: alert.id }, + }, + history: createMemoryHistory(), + location: createLocation(`/alert/${alert.id}`), + }; +} +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx new file mode 100644 index 0000000000000..4e00ea304d987 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx @@ -0,0 +1,118 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { ToastsApi } from 'kibana/public'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { AlertDetailsWithApi as AlertDetails } from './alert_details'; +import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; +import { + ComponentOpts as AlertApis, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; +import { + ComponentOpts as ActionApis, + withActionOperations, +} from '../../common/components/with_actions_api_operations'; + +type AlertDetailsRouteProps = RouteComponentProps<{ + alertId: string; +}> & + Pick & + Pick; + +export const AlertDetailsRoute: React.FunctionComponent = ({ + match: { + params: { alertId }, + }, + loadAlert, + loadAlertTypes, + loadActionTypes, +}) => { + const { http, toastNotifications } = useAppDependencies(); + + const [alert, setAlert] = useState(null); + const [alertType, setAlertType] = useState(null); + const [actionTypes, setActionTypes] = useState(null); + + useEffect(() => { + getAlertData( + alertId, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + }, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]); + + return alert && alertType && actionTypes ? ( + + ) : ( +
+ +
+ ); +}; + +export async function getAlertData( + alertId: string, + loadAlert: AlertApis['loadAlert'], + loadAlertTypes: AlertApis['loadAlertTypes'], + loadActionTypes: ActionApis['loadActionTypes'], + setAlert: React.Dispatch>, + setAlertType: React.Dispatch>, + setActionTypes: React.Dispatch>, + toastNotifications: Pick +) { + try { + const loadedAlert = await loadAlert(alertId); + setAlert(loadedAlert); + + const [loadedAlertType, loadedActionTypes] = await Promise.all([ + loadAlertTypes() + .then(types => types.find(type => type.id === loadedAlert.alertTypeId)) + .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)), + loadActionTypes().then( + throwIfIsntContained( + new Set(loadedAlert.actions.map(action => action.actionTypeId)), + (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`, + (action: ActionType) => action.id + ) + ), + ]); + + setAlertType(loadedAlertType); + setActionTypes(loadedActionTypes); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage', + { + defaultMessage: 'Unable to load alert: {message}', + values: { + message: e.message, + }, + } + ), + }); + } +} + +export const AlertDetailsRouteWithApi = withActionOperations( + withBulkAlertOperations(AlertDetailsRoute) +); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx index ff1510ea873d3..f410fff44172f 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -21,7 +21,14 @@ jest.mock('../../../lib/alert_api', () => ({ loadAlerts: jest.fn(), loadAlertTypes: jest.fn(), })); - +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/alerts/', + }), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx index 12122983161bd..32de924f63e80 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx @@ -15,19 +15,23 @@ import { EuiFlexItem, EuiIcon, EuiSpacer, + EuiLink, } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; import { AlertAdd } from '../../alert_add'; -import { BulkActionPopover } from './bulk_action_popover'; -import { CollapsedItemActions } from './collapsed_item_actions'; +import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; +import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; +import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { routeToAlertDetails } from '../../../constants'; const ENTER_KEY = 13; @@ -43,6 +47,7 @@ interface AlertState { } export const AlertsList: React.FunctionComponent = () => { + const history = useHistory(); const { http, injectedMetadata, toastNotifications, capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -151,6 +156,18 @@ export const AlertsList: React.FunctionComponent = () => { sortable: false, truncateText: true, 'data-test-subj': 'alertsTableCell-name', + render: (name: string, alert: AlertTableItem) => { + return ( + { + history.push(routeToAlertDetails.replace(`:alertId`, alert.id)); + }} + > + {name} + + ); + }, }, { field: 'tagsText', @@ -236,17 +253,19 @@ export const AlertsList: React.FunctionComponent = () => { {selectedIds.length > 0 && canDelete && ( - setIsPerformingAction(true)} - onActionPerformed={() => { - loadAlertsData(); - setIsPerformingAction(false); - }} - /> + + setIsPerformingAction(true)} + onActionPerformed={() => { + loadAlertsData(); + setIsPerformingAction(false); + }} + /> + )} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index aa1c6dd7c5b9a..2bac159ed79ed 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -20,23 +20,25 @@ import { AlertTableItem } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; -export interface ComponentOpts { +export type ComponentOpts = { item: AlertTableItem; onAlertChanged: () => void; -} +} & BulkOperationsComponentOpts; export const CollapsedItemActions: React.FunctionComponent = ({ item, onAlertChanged, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + deleteAlert, }: ComponentOpts) => { - const { http, capabilities } = useAppDependencies(); + const { capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -71,9 +73,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({ data-test-subj="enableSwitch" onChange={async () => { if (item.enabled) { - await disableAlerts({ http, ids: [item.id] }); + await disableAlert(item); } else { - await enableAlerts({ http, ids: [item.id] }); + await enableAlert(item); } onAlertChanged(); }} @@ -93,9 +95,9 @@ export const CollapsedItemActions: React.FunctionComponent = ({ data-test-subj="muteSwitch" onChange={async () => { if (item.muteAll) { - await unmuteAlerts({ http, ids: [item.id] }); + await unmuteAlert(item); } else { - await muteAlerts({ http, ids: [item.id] }); + await muteAlert(item); } onAlertChanged(); }} @@ -115,7 +117,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ color="text" data-test-subj="deleteAlert" onClick={async () => { - await deleteAlerts({ http, ids: [item.id] }); + await deleteAlert(item); onAlertChanged(); }} > @@ -129,3 +131,5 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ); }; + +export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx similarity index 52% rename from x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx rename to x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx index 59ec52ac83a6c..9635e6cd11983 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx @@ -5,34 +5,35 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiButtonEmpty, EuiFormRow, EuiPopover } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; -import { AlertTableItem } from '../../../../types'; +import { Alert } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + withBulkAlertOperations, + ComponentOpts as BulkOperationsComponentOpts, +} from './with_bulk_alert_api_operations'; -export interface ComponentOpts { - selectedItems: AlertTableItem[]; - onPerformingAction: () => void; - onActionPerformed: () => void; -} +export type ComponentOpts = { + selectedItems: Alert[]; + onPerformingAction?: () => void; + onActionPerformed?: () => void; +} & BulkOperationsComponentOpts; -export const BulkActionPopover: React.FunctionComponent = ({ +export const AlertQuickEditButtons: React.FunctionComponent = ({ selectedItems, - onPerformingAction, - onActionPerformed, + onPerformingAction = noop, + onActionPerformed = noop, + muteAlerts, + unmuteAlerts, + enableAlerts, + disableAlerts, + deleteAlerts, }: ComponentOpts) => { - const { http, toastNotifications } = useAppDependencies(); + const { toastNotifications } = useAppDependencies(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isMutingAlerts, setIsMutingAlerts] = useState(false); const [isUnmutingAlerts, setIsUnmutingAlerts] = useState(false); const [isEnablingAlerts, setIsEnablingAlerts] = useState(false); @@ -47,9 +48,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onmMuteAllClick() { onPerformingAction(); setIsMutingAlerts(true); - const ids = selectedItems.filter(item => !isAlertMuted(item)).map(item => item.id); try { - await muteAlerts({ http, ids }); + await muteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -68,9 +68,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onUnmuteAllClick() { onPerformingAction(); setIsUnmutingAlerts(true); - const ids = selectedItems.filter(isAlertMuted).map(item => item.id); try { - await unmuteAlerts({ http, ids }); + await unmuteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -89,9 +88,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onEnableAllClick() { onPerformingAction(); setIsEnablingAlerts(true); - const ids = selectedItems.filter(isAlertDisabled).map(item => item.id); try { - await enableAlerts({ http, ids }); + await enableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -110,9 +108,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function onDisableAllClick() { onPerformingAction(); setIsDisablingAlerts(true); - const ids = selectedItems.filter(item => !isAlertDisabled(item)).map(item => item.id); try { - await disableAlerts({ http, ids }); + await disableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -131,9 +128,8 @@ export const BulkActionPopover: React.FunctionComponent = ({ async function deleteSelectedItems() { onPerformingAction(); setIsDeletingAlerts(true); - const ids = selectedItems.map(item => item.id); try { - await deleteAlerts({ http, ids }); + await deleteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -150,104 +146,83 @@ export const BulkActionPopover: React.FunctionComponent = ({ } return ( - setIsPopoverOpen(false)} - data-test-subj="bulkAction" - button={ - setIsPopoverOpen(!isPopoverOpen)} + + {!allAlertsMuted && ( + - - } - > - {!allAlertsMuted && ( - - - - - + )} {allAlertsMuted && ( - - - - - + + + )} {allAlertsDisabled && ( - - - - - + + + )} {!allAlertsDisabled && ( - - - - - - )} - - - + )} + + + + + ); }; -function isAlertDisabled(alert: AlertTableItem) { +export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons); + +function isAlertDisabled(alert: Alert) { return alert.enabled === false; } -function isAlertMuted(alert: AlertTableItem) { +function isAlertMuted(alert: Alert) { return alert.muteAll === true; } + +function noop() {} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx new file mode 100644 index 0000000000000..d0fd0e1792818 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx @@ -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 React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFormRow, EuiPopover } from '@elastic/eui'; + +export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + data-test-subj="bulkAction" + button={ + setIsPopoverOpen(!isPopoverOpen)} + > + + + } + > + {children && + React.Children.map(children, child => + React.isValidElement(child) ? ( + {React.cloneElement(child, {})} + ) : ( + child + ) + )} + + ); +}; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx new file mode 100644 index 0000000000000..dd6b8775ba3d0 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx @@ -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. + */ +import * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import { withActionOperations, ComponentOpts } from './with_actions_api_operations'; +import * as actionApis from '../../../lib/action_connector_api'; +import { useAppDependencies } from '../../../app_context'; + +jest.mock('../../../lib/action_connector_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + })), + }; +}); + +describe('with_action_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with Action Api methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.loadActionTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + it('loadActionTypes calls the loadActionTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadActionTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(actionApis.loadActionTypes).toHaveBeenCalledTimes(1); + expect(actionApis.loadActionTypes).toHaveBeenCalledWith({ http }); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx new file mode 100644 index 0000000000000..45e6c6b10532c --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.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 from 'react'; + +import { ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { loadActionTypes } from '../../../lib/action_connector_api'; + +export interface ComponentOpts { + loadActionTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withActionOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + loadActionTypes({ http })} /> + ); + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx new file mode 100644 index 0000000000000..30a065479ce33 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx @@ -0,0 +1,269 @@ +/* + * 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 * as React from 'react'; +import { shallow, mount } from 'enzyme'; +import uuid from 'uuid'; +import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations'; +import * as alertApi from '../../../lib/alert_api'; +import { useAppDependencies } from '../../../app_context'; +import { Alert } from '../../../../types'; + +jest.mock('../../../lib/alert_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), + }; +}); + +describe('with_bulk_alert_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with AlertApi methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.muteAlerts).toEqual('function'); + expect(typeof props.unmuteAlerts).toEqual('function'); + expect(typeof props.enableAlerts).toEqual('function'); + expect(typeof props.disableAlerts).toEqual('function'); + expect(typeof props.deleteAlerts).toEqual('function'); + expect(typeof props.muteAlert).toEqual('function'); + expect(typeof props.unmuteAlert).toEqual('function'); + expect(typeof props.enableAlert).toEqual('function'); + expect(typeof props.disableAlert).toEqual('function'); + expect(typeof props.deleteAlert).toEqual('function'); + expect(typeof props.loadAlert).toEqual('function'); + expect(typeof props.loadAlertTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + // single alert + it('muteAlert calls the muteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('unmuteAlert calls the unmuteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ muteAll: true }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('enableAlert calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ enabled: false }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('disableAlert calls the disableAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('deleteAlert calls the deleteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + // bulk alerts + it('muteAlerts calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('unmuteAlerts calls the unmuteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('enableAlerts calls the muteAlertss api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [ + mockAlert({ enabled: false }), + mockAlert({ enabled: true }), + mockAlert({ enabled: false }), + ]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http }); + }); + + it('disableAlerts calls the disableAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlerts).toHaveBeenCalledWith({ + ids: [alerts[0].id, alerts[1].id], + http, + }); + }); + + it('deleteAlerts calls the deleteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('loadAlert calls the loadAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ + loadAlert, + alertId, + }: ComponentOpts & { alertId: Alert['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alertId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlert).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); + }); + + it('loadAlertTypes calls the loadAlertTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http }); + }); +}); + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx new file mode 100644 index 0000000000000..c61ba631ab868 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -0,0 +1,103 @@ +/* + * 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 { Alert, AlertType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { + deleteAlerts, + disableAlerts, + enableAlerts, + muteAlerts, + unmuteAlerts, + deleteAlert, + disableAlert, + enableAlert, + muteAlert, + unmuteAlert, + loadAlert, + loadAlertTypes, +} from '../../../lib/alert_api'; + +export interface ComponentOpts { + muteAlerts: (alerts: Alert[]) => Promise; + unmuteAlerts: (alerts: Alert[]) => Promise; + enableAlerts: (alerts: Alert[]) => Promise; + disableAlerts: (alerts: Alert[]) => Promise; + deleteAlerts: (alerts: Alert[]) => Promise; + muteAlert: (alert: Alert) => Promise; + unmuteAlert: (alert: Alert) => Promise; + enableAlert: (alert: Alert) => Promise; + disableAlert: (alert: Alert) => Promise; + deleteAlert: (alert: Alert) => Promise; + loadAlert: (id: Alert['id']) => Promise; + loadAlertTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withBulkAlertOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + + muteAlerts({ http, ids: items.filter(item => !isAlertMuted(item)).map(item => item.id) }) + } + unmuteAlerts={async (items: Alert[]) => + unmuteAlerts({ http, ids: items.filter(isAlertMuted).map(item => item.id) }) + } + enableAlerts={async (items: Alert[]) => + enableAlerts({ http, ids: items.filter(isAlertDisabled).map(item => item.id) }) + } + disableAlerts={async (items: Alert[]) => + disableAlerts({ + http, + ids: items.filter(item => !isAlertDisabled(item)).map(item => item.id), + }) + } + deleteAlerts={async (items: Alert[]) => + deleteAlerts({ http, ids: items.map(item => item.id) }) + } + muteAlert={async (alert: Alert) => { + if (!isAlertMuted(alert)) { + return muteAlert({ http, id: alert.id }); + } + }} + unmuteAlert={async (alert: Alert) => { + if (isAlertMuted(alert)) { + return unmuteAlert({ http, id: alert.id }); + } + }} + enableAlert={async (alert: Alert) => { + if (isAlertDisabled(alert)) { + return enableAlert({ http, id: alert.id }); + } + }} + disableAlert={async (alert: Alert) => { + if (!isAlertDisabled(alert)) { + return disableAlert({ http, id: alert.id }); + } + }} + deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })} + loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} + loadAlertTypes={async () => loadAlertTypes({ http })} + /> + ); + }; +} + +function isAlertDisabled(alert: Alert) { + return alert.enabled === false; +} + +function isAlertMuted(alert: Alert) { + return alert.muteAll === true; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts index ed63ade903104..7fb7d0bf48e4d 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeRegistry } from './application/type_registry'; -import { SanitizedAlert as Alert } from '../../../alerting/common'; -export { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { ActionType } from '../../../../../plugins/actions/common'; + +export { Alert, AlertAction }; +export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Record; @@ -47,11 +50,6 @@ export interface ValidationResult { errors: Record; } -export interface ActionType { - id: string; - name: string; -} - export interface ActionConnector { secrets: Record; id: string; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts new file mode 100644 index 0000000000000..e8ed54571c77c --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -0,0 +1,146 @@ +/* + * 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 uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']); + const browser = getService('browser'); + const alerting = getService('alerting'); + + describe('Alert Details', function() { + const testRunUuid = uuid.v4(); + + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + + const actions = await Promise.all([ + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${0}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${1}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + ]); + + const alert = await alerting.alerts.createAlwaysFiringWithActions( + `test-alert-${testRunUuid}`, + actions.map(action => ({ + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + })) + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + }); + + it('renders the alert details', async () => { + const headingText = await pageObjects.alertDetailsUI.getHeadingText(); + expect(headingText).to.be(`test-alert-${testRunUuid}`); + + const alertType = await pageObjects.alertDetailsUI.getAlertType(); + expect(alertType).to.be(`Always Firing`); + + const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); + expect(actionType).to.be(`Server log`); + expect(actionCount).to.be(`+1`); + }); + + it('should disable the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await enableSwitch.click(); + + const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + + it('shouldnt allow you to mute a disabled alert', async () => { + const disabledEnableSwitch = await testSubjects.find('enableSwitch'); + expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); + + const muteSwitch = await testSubjects.find('muteSwitch'); + expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch'); + const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute( + 'aria-checked' + ); + expect(isDisabledMuteAfterDisabling).to.eql('false'); + }); + + it('should reenable a disabled the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await enableSwitch.click(); + + const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should mute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should unmute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await muteSwitch.click(); + + const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 13f50a505b0b6..307f39382a236 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -12,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const log = getService('log'); const browser = getService('browser'); + const alerting = getService('alerting'); describe('Home page', function() { before(async () => { @@ -55,6 +56,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); }); + + it('navigates to an alert details page', async () => { + const action = await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + + const alert = await alerting.alerts.createAlwaysFiringWithAction( + `test-alert-${Date.now()}`, + { + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + } + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + // Verify url + expect(await browser.getCurrentUrl()).to.contain(`/alert/${alert.id}`); + + await alerting.alerts.deleteAlert(alert.id); + }); }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index c76f477c8cfbe..a771fbf85e0b6 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -12,5 +12,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./details')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index df651c67c2c28..43162e9256370 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -12,13 +12,42 @@ export default function(kibana: any) { require: ['alerting'], name: 'alerts', init(server: any) { - const noopAlertType: AlertType = { - id: 'test.noop', - name: 'Test: Noop', - actionGroups: ['default'], - async executor() {}, - }; - server.plugins.alerting.setup.registerType(noopAlertType); + createNoopAlertType(server.plugins.alerting.setup); + createAlwaysFiringAlertType(server.plugins.alerting.setup); }, }); } + +function createNoopAlertType(setupContract: any) { + const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: ['default'], + async executor() {}, + }; + setupContract.registerType(noopAlertType); +} + +function createAlwaysFiringAlertType(setupContract: any) { + // Alert types + const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: any) { + const { services, state } = alertExecutorOptions; + + services + .alertInstanceFactory('1') + .replaceState({ instanceStateValue: true }) + .scheduleActions('default', { + instanceContextValue: true, + }); + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, + }; + setupContract.registerType(alwaysFiringAlertType); +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts new file mode 100644 index 0000000000000..6d2038a6ba04c --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.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 { FtrProviderContext } from '../ftr_provider_context'; + +export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async getHeadingText() { + return await testSubjects.getVisibleText('alertDetailsTitle'); + }, + async getAlertType() { + return await testSubjects.getVisibleText('alertTypeLabel'); + }, + async getActionsLabels() { + return { + actionType: await testSubjects.getVisibleText('actionTypeLabel'), + actionCount: await testSubjects.getVisibleText('actionCountLabel'), + }; + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts index a068ba7dfe81d..cfc44221a9c17 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts @@ -6,8 +6,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { TriggersActionsPageProvider } from './triggers_actions_ui_page'; +import { AlertDetailsPageProvider } from './alert_details'; export const pageObjects = { ...xpackFunctionalPageObjects, triggersActionsUI: TriggersActionsPageProvider, + alertDetailsUI: AlertDetailsPageProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index a04ecc969a7e1..ae66ac0ddddfb 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -92,6 +92,10 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }; }); }, + async clickOnAlertInAlertsList(name: string) { + await this.searchAlerts(name); + await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); + }, async changeTabs(tab: 'alertsTab' | 'connectorsTab') { return await testSubjects.click(tab); }, diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts new file mode 100644 index 0000000000000..9454a32757068 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts @@ -0,0 +1,48 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Actions { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/actions' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAction(actionParams: { + name: string; + actionTypeId: string; + config: Record; + secrets: Record; + }) { + this.log.debug(`creating action ${actionParams.name}`); + + const { data: action, status: actionStatus, actionStatusText } = await this.axios.post( + `/api/action`, + actionParams + ); + if (actionStatus !== 200) { + throw new Error( + `Expected status code of 200, received ${actionStatus} ${actionStatusText}: ${util.inspect( + action + )}` + ); + } + + this.log.debug(`created action ${action.id}`); + return action; + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts new file mode 100644 index 0000000000000..1a31d4796d5bc --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -0,0 +1,79 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Alerts { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/alerts' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAlwaysFiringWithActions( + name: string, + actions: Array<{ + id: string; + group: string; + params: Record; + }> + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags: ['foo'], + alertTypeId: 'test.always-firing', + consumer: 'bar', + schedule: { interval: '1m' }, + throttle: '1m', + actions, + params: {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + + public async createAlwaysFiringWithAction( + name: string, + action: { + id: string; + group: string; + params: Record; + } + ) { + return this.createAlwaysFiringWithActions(name, [action]); + } + + public async deleteAlert(id: string) { + this.log.debug(`deleting alert ${id}`); + + const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + this.log.debug(`deleted alert ${alert.id}`); + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/index.ts b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts new file mode 100644 index 0000000000000..e0aa827316c01 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import { Alerts } from './alerts'; +import { Actions } from './actions'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function AlertsServiceProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new (class AlertingService { + actions = new Actions(url, log); + alerts = new Alerts(url, log); + })(); +} diff --git a/x-pack/test/functional_with_es_ssl/services/index.ts b/x-pack/test/functional_with_es_ssl/services/index.ts index 6e96921c25a31..f04c2c980055d 100644 --- a/x-pack/test/functional_with_es_ssl/services/index.ts +++ b/x-pack/test/functional_with_es_ssl/services/index.ts @@ -5,7 +5,9 @@ */ import { services as xpackFunctionalServices } from '../../functional/services'; +import { AlertsServiceProvider } from './alerting'; export const services = { ...xpackFunctionalServices, + alerting: AlertsServiceProvider, }; From 38dc1cbd3cb74baeb498978688c92683d42b974e Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 3 Feb 2020 12:03:50 +0100 Subject: [PATCH 02/17] Migrate es-archiver to typescript (#56008) * migrate lib/archives and lib/docs * migrate lib/indices * migrate end of /lib * migrate /actions * migrate es_archiver * migrate cli * migrate tests * use proper log stub * add Record typing Co-authored-by: Elastic Machine --- .../lib/config/read_config_file.ts | 2 +- src/es_archiver/actions/{edit.js => edit.ts} | 13 +- ..._kibana_index.js => empty_kibana_index.ts} | 18 +- .../actions/{index.js => index.ts} | 0 src/es_archiver/actions/{load.js => load.ts} | 29 +++- .../{rebuild_all.js => rebuild_all.ts} | 21 ++- src/es_archiver/actions/{save.js => save.ts} | 24 ++- .../actions/{unload.js => unload.ts} | 22 ++- src/es_archiver/{cli.js => cli.ts} | 22 +-- src/es_archiver/es_archiver.d.ts | 39 ----- .../{es_archiver.js => es_archiver.ts} | 33 ++-- src/es_archiver/index.js | 20 --- src/es_archiver/{index.d.ts => index.ts} | 0 .../lib/__tests__/{stats.js => stats.ts} | 18 +- .../__tests__/{format.js => format.ts} | 16 +- .../archives/__tests__/{parse.js => parse.ts} | 20 +-- .../archives/{constants.js => constants.ts} | 0 .../archives/{filenames.js => filenames.ts} | 6 +- .../lib/archives/{format.js => format.ts} | 4 +- .../lib/archives/{index.js => index.ts} | 2 - .../lib/archives/{parse.js => parse.ts} | 4 +- .../lib/{directory.js => directory.ts} | 5 +- ...ream.js => generate_doc_records_stream.ts} | 2 +- ..._stream.js => index_doc_records_stream.ts} | 4 +- .../lib/docs/__tests__/{stubs.js => stubs.ts} | 29 +++- ...ream.js => generate_doc_records_stream.ts} | 19 ++- .../lib/docs/{index.js => index.ts} | 0 ..._stream.js => index_doc_records_stream.ts} | 9 +- src/es_archiver/lib/{index.js => index.ts} | 2 +- ...index_stream.js => create_index_stream.ts} | 51 +++--- ...index_stream.js => delete_index_stream.ts} | 29 ++-- ...am.js => generate_index_records_stream.ts} | 14 +- .../lib/indices/__tests__/stubs.js | 128 --------------- .../lib/indices/__tests__/stubs.ts | 154 ++++++++++++++++++ ...index_stream.js => create_index_stream.ts} | 35 +++- .../{delete_index.js => delete_index.ts} | 34 ++-- ...index_stream.js => delete_index_stream.ts} | 10 +- ...am.js => generate_index_records_stream.ts} | 8 +- .../lib/indices/{index.js => index.ts} | 0 .../{kibana_index.js => kibana_index.ts} | 96 +++++++---- ...rds_stream.js => filter_records_stream.ts} | 2 +- ...rds_stream.js => filter_records_stream.ts} | 4 +- .../lib/records/{index.js => index.ts} | 0 src/es_archiver/lib/stats.ts | 2 + src/legacy/utils/index.d.ts | 13 ++ src/legacy/utils/streams/index.d.ts | 6 +- 46 files changed, 576 insertions(+), 393 deletions(-) rename src/es_archiver/actions/{edit.js => edit.ts} (91%) rename src/es_archiver/actions/{empty_kibana_index.js => empty_kibana_index.ts} (73%) rename src/es_archiver/actions/{index.js => index.ts} (100%) rename src/es_archiver/actions/{load.js => load.ts} (84%) rename src/es_archiver/actions/{rebuild_all.js => rebuild_all.ts} (84%) rename src/es_archiver/actions/{save.js => save.ts} (83%) rename src/es_archiver/actions/{unload.js => unload.ts} (79%) rename src/es_archiver/{cli.js => cli.ts} (90%) delete mode 100644 src/es_archiver/es_archiver.d.ts rename src/es_archiver/{es_archiver.js => es_archiver.ts} (83%) delete mode 100644 src/es_archiver/index.js rename src/es_archiver/{index.d.ts => index.ts} (100%) rename src/es_archiver/lib/__tests__/{stats.js => stats.ts} (95%) rename src/es_archiver/lib/archives/__tests__/{format.js => format.ts} (89%) rename src/es_archiver/lib/archives/__tests__/{parse.js => parse.ts} (93%) rename src/es_archiver/lib/archives/{constants.js => constants.ts} (100%) rename src/es_archiver/lib/archives/{filenames.js => filenames.ts} (91%) rename src/es_archiver/lib/archives/{format.js => format.ts} (93%) rename src/es_archiver/lib/archives/{index.js => index.ts} (99%) rename src/es_archiver/lib/archives/{parse.js => parse.ts} (91%) rename src/es_archiver/lib/{directory.js => directory.ts} (88%) rename src/es_archiver/lib/docs/__tests__/{generate_doc_records_stream.js => generate_doc_records_stream.ts} (98%) rename src/es_archiver/lib/docs/__tests__/{index_doc_records_stream.js => index_doc_records_stream.ts} (98%) rename src/es_archiver/lib/docs/__tests__/{stubs.js => stubs.ts} (74%) rename src/es_archiver/lib/docs/{generate_doc_records_stream.js => generate_doc_records_stream.ts} (80%) rename src/es_archiver/lib/docs/{index.js => index.ts} (100%) rename src/es_archiver/lib/docs/{index_doc_records_stream.js => index_doc_records_stream.ts} (86%) rename src/es_archiver/lib/{index.js => index.ts} (96%) rename src/es_archiver/lib/indices/__tests__/{create_index_stream.js => create_index_stream.ts} (76%) rename src/es_archiver/lib/indices/__tests__/{delete_index_stream.js => delete_index_stream.ts} (66%) rename src/es_archiver/lib/indices/__tests__/{generate_index_records_stream.js => generate_index_records_stream.ts} (89%) delete mode 100644 src/es_archiver/lib/indices/__tests__/stubs.js create mode 100644 src/es_archiver/lib/indices/__tests__/stubs.ts rename src/es_archiver/lib/indices/{create_index_stream.js => create_index_stream.ts} (81%) rename src/es_archiver/lib/indices/{delete_index.js => delete_index.ts} (76%) rename src/es_archiver/lib/indices/{delete_index_stream.js => delete_index_stream.ts} (86%) rename src/es_archiver/lib/indices/{generate_index_records_stream.js => generate_index_records_stream.ts} (89%) rename src/es_archiver/lib/indices/{index.js => index.ts} (100%) rename src/es_archiver/lib/indices/{kibana_index.js => kibana_index.ts} (70%) rename src/es_archiver/lib/records/__tests__/{filter_records_stream.js => filter_records_stream.ts} (97%) rename src/es_archiver/lib/records/{filter_records_stream.js => filter_records_stream.ts} (91%) rename src/es_archiver/lib/records/{index.js => index.ts} (100%) diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 72926cae7dbc4..96fd525efa3ec 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -56,7 +56,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid return transformDeprecations(settingsWithDefaults, logDeprecation); } -export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any) { +export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { return new Config({ settings: await getSettingsFromFile(log, path, settingOverrides), primary: true, diff --git a/src/es_archiver/actions/edit.js b/src/es_archiver/actions/edit.ts similarity index 91% rename from src/es_archiver/actions/edit.js rename to src/es_archiver/actions/edit.ts index 5e3a3490133c7..de63081a1ea1b 100644 --- a/src/es_archiver/actions/edit.js +++ b/src/es_archiver/actions/edit.ts @@ -22,12 +22,23 @@ import Fs from 'fs'; import { createGunzip, createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { promisify } from 'util'; import globby from 'globby'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; const unlinkAsync = promisify(Fs.unlink); -export async function editAction({ prefix, dataDir, log, handler }) { +export async function editAction({ + prefix, + dataDir, + log, + handler, +}: { + prefix: string; + dataDir: string; + log: ToolingLog; + handler: () => Promise; +}) { const archives = ( await globby('**/*.gz', { cwd: prefix ? resolve(dataDir, prefix) : dataDir, diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.ts similarity index 73% rename from src/es_archiver/actions/empty_kibana_index.js rename to src/es_archiver/actions/empty_kibana_index.ts index 386863ec18a43..5f96fbc5f996c 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.ts @@ -16,13 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; + import { migrateKibanaIndex, deleteKibanaIndices, createStats } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kbnClient }) { +export async function emptyKibanaIndexAction({ + client, + log, + kbnClient, +}: { + client: Client; + log: ToolingLog; + kbnClient: KbnClient; +}) { const stats = createStats('emptyKibanaIndex', log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); - await deleteKibanaIndices({ client, stats }); - await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); + await deleteKibanaIndices({ client, stats, log }); + await migrateKibanaIndex({ client, log, kibanaPluginIds }); return stats; } diff --git a/src/es_archiver/actions/index.js b/src/es_archiver/actions/index.ts similarity index 100% rename from src/es_archiver/actions/index.js rename to src/es_archiver/actions/index.ts diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.ts similarity index 84% rename from src/es_archiver/actions/load.js rename to src/es_archiver/actions/load.ts index ea02ce9dd3ad3..404fd0daea91d 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.ts @@ -19,6 +19,9 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable } from 'stream'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '../../legacy/utils'; @@ -38,12 +41,26 @@ import { // pipe a series of streams into each other so that data and errors // flow from the first stream to the last. Errors from the last stream // are not listened for -const pipeline = (...streams) => +const pipeline = (...streams: Readable[]) => streams.reduce((source, dest) => - source.once('error', error => dest.emit('error', error)).pipe(dest) + source.once('error', error => dest.emit('error', error)).pipe(dest as any) ); -export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { +export async function loadAction({ + name, + skipExisting, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + skipExisting: boolean; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); @@ -64,12 +81,12 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn { objectMode: true } ); - const progress = new Progress('load progress'); + const progress = new Progress(); progress.activate(log); await createPromiseFromStreams([ recordStream, - createCreateIndexStream({ client, stats, skipExisting, log, kibanaPluginIds }), + createCreateIndexStream({ client, stats, skipExisting, log }), createIndexDocRecordsStream(client, stats, progress), ]); @@ -77,7 +94,7 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn const result = stats.toJSON(); for (const [index, { docs }] of Object.entries(result)) { - if (!docs && docs.indexed > 0) { + if (docs && docs.indexed > 0) { log.info('[%s] Indexed %d docs into %j', name, docs.indexed, index); } } diff --git a/src/es_archiver/actions/rebuild_all.js b/src/es_archiver/actions/rebuild_all.ts similarity index 84% rename from src/es_archiver/actions/rebuild_all.js rename to src/es_archiver/actions/rebuild_all.ts index 9379a29c38130..1467a1d0430b7 100644 --- a/src/es_archiver/actions/rebuild_all.js +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,13 +18,12 @@ */ import { resolve, dirname, relative } from 'path'; - import { stat, rename, createReadStream, createWriteStream } from 'fs'; - +import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { prioritizeMappings, readDirectory, @@ -33,12 +32,20 @@ import { createFormatArchiveStreams, } from '../lib'; -async function isDirectory(path) { +async function isDirectory(path: string): Promise { const stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } -export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { +export async function rebuildAllAction({ + dataDir, + log, + rootDir = dataDir, +}: { + dataDir: string; + log: ToolingLog; + rootDir?: string; +}) { const childNames = prioritizeMappings(await readDirectory(dataDir)); for (const childName of childNames) { const childPath = resolve(dataDir, childName); @@ -58,11 +65,11 @@ export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { const tempFile = childPath + (gzip ? '.rebuilding.gz' : '.rebuilding'); await createPromiseFromStreams([ - createReadStream(childPath), + createReadStream(childPath) as Readable, ...createParseArchiveStreams({ gzip }), ...createFormatArchiveStreams({ gzip }), createWriteStream(tempFile), - ]); + ] as [Readable, ...Writable[]]); await fromNode(cb => rename(tempFile, childPath, cb)); log.info(`${archiveName} Rebuilt ${childName}`); diff --git a/src/es_archiver/actions/save.js b/src/es_archiver/actions/save.ts similarity index 83% rename from src/es_archiver/actions/save.js rename to src/es_archiver/actions/save.ts index 2c264ed2ee3a9..7a3a9dd97c0ab 100644 --- a/src/es_archiver/actions/save.js +++ b/src/es_archiver/actions/save.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; import { createListStream, createPromiseFromStreams } from '../../legacy/utils'; - import { createStats, createGenerateIndexRecordsStream, @@ -30,7 +32,21 @@ import { Progress, } from '../lib'; -export async function saveAction({ name, indices, client, dataDir, log, raw }) { +export async function saveAction({ + name, + indices, + client, + dataDir, + log, + raw, +}: { + name: string; + indices: string | string[]; + client: Client; + dataDir: string; + log: ToolingLog; + raw: boolean; +}) { const outputDir = resolve(dataDir, name); const stats = createStats(name, log); @@ -48,7 +64,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateIndexRecordsStream(client, stats), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), - ]), + ] as [Readable, ...Writable[]]), // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ @@ -56,7 +72,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateDocRecordsStream(client, stats, progress), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), - ]), + ] as [Readable, ...Writable[]]), ]); progress.deactivate(); diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.ts similarity index 79% rename from src/es_archiver/actions/unload.js rename to src/es_archiver/actions/unload.ts index 2acf8d2d71986..130a6b542b218 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { isGzip, createStats, @@ -32,7 +34,19 @@ import { createDeleteIndexStream, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kbnClient }) { +export async function unloadAction({ + name, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); @@ -42,11 +56,11 @@ export async function unloadAction({ name, client, dataDir, log, kbnClient }) { log.info('[%s] Unloading indices from %j', name, filename); await createPromiseFromStreams([ - createReadStream(resolve(inputDir, filename)), + createReadStream(resolve(inputDir, filename)) as Readable, ...createParseArchiveStreams({ gzip: isGzip(filename) }), createFilterRecordsStream('index'), createDeleteIndexStream(client, stats, log, kibanaPluginIds), - ]); + ] as [Readable, ...Writable[]]); } return stats.toJSON(); diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.ts similarity index 90% rename from src/es_archiver/cli.js rename to src/es_archiver/cli.ts index 56d1fdca89780..252f99f8f47af 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.ts @@ -17,7 +17,7 @@ * under the License. */ -/************************************************************* +/** *********************************************************** * * Run `node scripts/es_archiver --help` for usage information * @@ -27,17 +27,17 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import readline from 'readline'; - import { Command } from 'commander'; import * as legacyElasticsearch from 'elasticsearch'; -import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '@kbn/test'; +import { EsArchiver } from './es_archiver'; + const cmd = new Command('node scripts/es_archiver'); -const resolveConfigPath = v => resolve(process.cwd(), v); +const resolveConfigPath = (v: string) => resolve(process.cwd(), v); const defaultConfigPath = resolveConfigPath('test/functional/config.js'); cmd @@ -56,6 +56,7 @@ cmd defaultConfigPath ) .on('--help', () => { + // eslint-disable-next-line no-console console.log(readFileSync(resolve(__dirname, './cli_help.txt'), 'utf8')); }); @@ -95,10 +96,10 @@ cmd output: process.stdout, }); - await new Promise(resolve => { + await new Promise(resolveInput => { rl.question(`Press enter when you're done`, () => { rl.close(); - resolve(); + resolveInput(); }); }); }) @@ -112,12 +113,12 @@ cmd cmd.parse(process.argv); -const missingCommand = cmd.args.every(a => !(a instanceof Command)); +const missingCommand = cmd.args.every(a => !((a as any) instanceof Command)); if (missingCommand) { execute(); } -async function execute(fn) { +async function execute(fn?: (esArchiver: EsArchiver, command: Command) => void): Promise { try { const log = new ToolingLog({ level: cmd.verbose ? 'debug' : 'info', @@ -134,7 +135,7 @@ async function execute(fn) { // log and count all validation errors let errorCount = 0; - const error = msg => { + const error = (msg: string) => { errorCount++; log.error(msg); }; @@ -170,11 +171,12 @@ async function execute(fn) { dataDir: resolve(cmd.dir), kibanaUrl: cmd.kibanaUrl, }); - await fn(esArchiver, cmd); + await fn!(esArchiver, cmd); } finally { await client.close(); } } catch (err) { + // eslint-disable-next-line no-console console.log('FATAL ERROR', err.stack); } } diff --git a/src/es_archiver/es_archiver.d.ts b/src/es_archiver/es_archiver.d.ts deleted file mode 100644 index c50ae19d99cbf..0000000000000 --- a/src/es_archiver/es_archiver.d.ts +++ /dev/null @@ -1,39 +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 { ToolingLog } from '@kbn/dev-utils'; -import { Client } from 'elasticsearch'; -import { createStats } from './lib/stats'; - -export type JsonStats = ReturnType['toJSON']>; - -export class EsArchiver { - constructor(options: { client: Client; dataDir: string; log: ToolingLog; kibanaUrl: string }); - public save( - name: string, - indices: string | string[], - options?: { raw?: boolean } - ): Promise; - public load(name: string, options?: { skipExisting?: boolean }): Promise; - public unload(name: string): Promise; - public rebuildAll(): Promise; - public edit(prefix: string, handler: () => Promise): Promise; - public loadIfNeeded(name: string): Promise; - public emptyKibanaIndex(): Promise; -} diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.ts similarity index 83% rename from src/es_archiver/es_archiver.js rename to src/es_archiver/es_archiver.ts index 705706d0e5877..5614dfd842087 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.ts @@ -17,7 +17,8 @@ * under the License. */ -import { KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { saveAction, @@ -29,7 +30,22 @@ import { } from './actions'; export class EsArchiver { - constructor({ client, dataDir, log, kibanaUrl }) { + private readonly client: Client; + private readonly dataDir: string; + private readonly log: ToolingLog; + private readonly kbnClient: KbnClient; + + constructor({ + client, + dataDir, + log, + kibanaUrl, + }: { + client: Client; + dataDir: string; + log: ToolingLog; + kibanaUrl: string; + }) { this.client = client; this.dataDir = dataDir; this.log = log; @@ -46,7 +62,7 @@ export class EsArchiver { * @property {Boolean} options.raw - should the archive be raw (unzipped) or not * @return Promise */ - async save(name, indices, { raw = false } = {}) { + async save(name: string, indices: string | string[], { raw = false }: { raw?: boolean } = {}) { return await saveAction({ name, indices, @@ -66,9 +82,7 @@ export class EsArchiver { * be ignored or overwritten * @return Promise */ - async load(name, options = {}) { - const { skipExisting } = options; - + async load(name: string, { skipExisting = false }: { skipExisting?: boolean } = {}) { return await loadAction({ name, skipExisting: !!skipExisting, @@ -85,7 +99,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async unload(name) { + async unload(name: string) { return await unloadAction({ name, client: this.client, @@ -103,7 +117,6 @@ export class EsArchiver { */ async rebuildAll() { return await rebuildAllAction({ - client: this.client, dataDir: this.dataDir, log: this.log, }); @@ -117,7 +130,7 @@ export class EsArchiver { * @param {() => Promise} handler * @return Promise */ - async edit(prefix, handler) { + async edit(prefix: string, handler: () => Promise) { return await editAction({ prefix, log: this.log, @@ -132,7 +145,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async loadIfNeeded(name) { + async loadIfNeeded(name: string) { return await this.load(name, { skipExisting: true }); } diff --git a/src/es_archiver/index.js b/src/es_archiver/index.js deleted file mode 100644 index f7a579a98a42d..0000000000000 --- a/src/es_archiver/index.js +++ /dev/null @@ -1,20 +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 { EsArchiver } from './es_archiver'; diff --git a/src/es_archiver/index.d.ts b/src/es_archiver/index.ts similarity index 100% rename from src/es_archiver/index.d.ts rename to src/es_archiver/index.ts diff --git a/src/es_archiver/lib/__tests__/stats.js b/src/es_archiver/lib/__tests__/stats.ts similarity index 95% rename from src/es_archiver/lib/__tests__/stats.js rename to src/es_archiver/lib/__tests__/stats.ts index ccc24c25fb860..28e337b3da529 100644 --- a/src/es_archiver/lib/__tests__/stats.js +++ b/src/es_archiver/lib/__tests__/stats.ts @@ -17,26 +17,26 @@ * under the License. */ -import expect from '@kbn/expect'; import { uniq } from 'lodash'; import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { ToolingLog } from '@kbn/dev-utils'; import { createStats } from '../'; -import { ToolingLog } from '@kbn/dev-utils'; -function createBufferedLog() { - const log = new ToolingLog({ +function createBufferedLog(): ToolingLog & { buffer: string } { + const log: ToolingLog = new ToolingLog({ level: 'debug', writeTo: { - write: chunk => (log.buffer += chunk), + write: chunk => ((log as any).buffer += chunk), }, }); - log.buffer = ''; - return log; + (log as any).buffer = ''; + return log as ToolingLog & { buffer: string }; } -function assertDeepClones(a, b) { - const path = []; +function assertDeepClones(a: any, b: any) { + const path: string[] = []; try { (function recurse(one, two) { if (typeof one !== 'object' || typeof two !== 'object') { diff --git a/src/es_archiver/lib/archives/__tests__/format.js b/src/es_archiver/lib/archives/__tests__/format.ts similarity index 89% rename from src/es_archiver/lib/archives/__tests__/format.js rename to src/es_archiver/lib/archives/__tests__/format.ts index 20ead30824d06..f472f094134d7 100644 --- a/src/es_archiver/lib/archives/__tests__/format.js +++ b/src/es_archiver/lib/archives/__tests__/format.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream from 'stream'; +import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; import expect from '@kbn/expect'; @@ -43,11 +43,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -58,7 +58,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); @@ -73,11 +73,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([1, 2, { foo: 'bar' }, [1, 2]]), ...createFormatArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -89,7 +89,7 @@ describe('esArchiver createFormatArchiveStreams', () => { ...createFormatArchiveStreams({ gzip: true }), createGunzip(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.be(INPUT_JSON); }); }); @@ -100,7 +100,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); diff --git a/src/es_archiver/lib/archives/__tests__/parse.js b/src/es_archiver/lib/archives/__tests__/parse.ts similarity index 93% rename from src/es_archiver/lib/archives/__tests__/parse.js rename to src/es_archiver/lib/archives/__tests__/parse.ts index 2e1506e543a35..ba30156b5af39 100644 --- a/src/es_archiver/lib/archives/__tests__/parse.js +++ b/src/es_archiver/lib/archives/__tests__/parse.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream, { PassThrough, Transform } from 'stream'; +import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; import expect from '@kbn/expect'; @@ -66,13 +66,13 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { - let onReceived; + let onReceived: (resolved: any) => void; const receivedPromise = new Promise(resolve => (onReceived = resolve)); const input = new PassThrough(); const check = new Transform({ @@ -80,16 +80,16 @@ describe('esArchiver createParseArchiveStreams', () => { readableObjectMode: true, transform(chunk, env, callback) { onReceived(chunk); - callback(null, chunk); + callback(undefined, chunk); }, }); const finalPromise = createPromiseFromStreams([ - input, + input as Readable, ...createParseArchiveStreams(), check, createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); expect(await receivedPromise).to.eql({ a: 1 }); @@ -110,7 +110,7 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('Unexpected number'); @@ -149,7 +149,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, { a: 2 }]); }); @@ -161,7 +161,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([]); }); @@ -173,7 +173,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams({ gzip: true }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('incorrect header check'); diff --git a/src/es_archiver/lib/archives/constants.js b/src/es_archiver/lib/archives/constants.ts similarity index 100% rename from src/es_archiver/lib/archives/constants.js rename to src/es_archiver/lib/archives/constants.ts diff --git a/src/es_archiver/lib/archives/filenames.js b/src/es_archiver/lib/archives/filenames.ts similarity index 91% rename from src/es_archiver/lib/archives/filenames.js rename to src/es_archiver/lib/archives/filenames.ts index 4ced04401d28d..24c355edda278 100644 --- a/src/es_archiver/lib/archives/filenames.js +++ b/src/es_archiver/lib/archives/filenames.ts @@ -19,7 +19,7 @@ import { basename, extname } from 'path'; -export function isGzip(path) { +export function isGzip(path: string) { return extname(path) === '.gz'; } @@ -28,7 +28,7 @@ export function isGzip(path) { * @param {String} path * @return {Boolean} */ -export function isMappingFile(path) { +export function isMappingFile(path: string) { return basename(path, '.gz') === 'mappings.json'; } @@ -41,7 +41,7 @@ export function isMappingFile(path) { * @param {Array} filenames * @return {Array} */ -export function prioritizeMappings(filenames) { +export function prioritizeMappings(filenames: string[]) { return filenames.slice().sort((fa, fb) => { if (isMappingFile(fa) === isMappingFile(fb)) return 0; return isMappingFile(fb) ? 1 : -1; diff --git a/src/es_archiver/lib/archives/format.js b/src/es_archiver/lib/archives/format.ts similarity index 93% rename from src/es_archiver/lib/archives/format.js rename to src/es_archiver/lib/archives/format.ts index 01fca87e7ba98..9bef4c9adbf05 100644 --- a/src/es_archiver/lib/archives/format.js +++ b/src/es_archiver/lib/archives/format.ts @@ -19,14 +19,12 @@ import { createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { PassThrough } from 'stream'; - import stringify from 'json-stable-stringify'; import { createMapStream, createIntersperseStream } from '../../../legacy/utils'; - import { RECORD_SEPARATOR } from './constants'; -export function createFormatArchiveStreams({ gzip = false } = {}) { +export function createFormatArchiveStreams({ gzip = false }: { gzip?: boolean } = {}) { return [ createMapStream(record => stringify(record, { space: ' ' })), createIntersperseStream(RECORD_SEPARATOR), diff --git a/src/es_archiver/lib/archives/index.js b/src/es_archiver/lib/archives/index.ts similarity index 99% rename from src/es_archiver/lib/archives/index.js rename to src/es_archiver/lib/archives/index.ts index 4020f52e45a35..6aa489ea5a46d 100644 --- a/src/es_archiver/lib/archives/index.js +++ b/src/es_archiver/lib/archives/index.ts @@ -18,7 +18,5 @@ */ export { isGzip, prioritizeMappings } from './filenames'; - export { createParseArchiveStreams } from './parse'; - export { createFormatArchiveStreams } from './format'; diff --git a/src/es_archiver/lib/archives/parse.js b/src/es_archiver/lib/archives/parse.ts similarity index 91% rename from src/es_archiver/lib/archives/parse.js rename to src/es_archiver/lib/archives/parse.ts index 4fe1df7259229..0f4460c925019 100644 --- a/src/es_archiver/lib/archives/parse.js +++ b/src/es_archiver/lib/archives/parse.ts @@ -29,7 +29,7 @@ export function createParseArchiveStreams({ gzip = false } = {}) { gzip ? createGunzip() : new PassThrough(), createReplaceStream('\r\n', '\n'), createSplitStream(RECORD_SEPARATOR), - createFilterStream(l => l.match(/[^\s]/)), - createMapStream(json => JSON.parse(json.trim())), + createFilterStream(l => !!l.match(/[^\s]/)), + createMapStream(json => JSON.parse(json.trim())), ]; } diff --git a/src/es_archiver/lib/directory.js b/src/es_archiver/lib/directory.ts similarity index 88% rename from src/es_archiver/lib/directory.js rename to src/es_archiver/lib/directory.ts index 5aee10cfea65d..8581207fa795d 100644 --- a/src/es_archiver/lib/directory.js +++ b/src/es_archiver/lib/directory.ts @@ -18,10 +18,9 @@ */ import { readdir } from 'fs'; - import { fromNode } from 'bluebird'; -export async function readDirectory(path) { - const allNames = await fromNode(cb => readdir(path, cb)); +export async function readDirectory(path: string) { + const allNames = await fromNode(cb => readdir(path, cb)); return allNames.filter(name => !name.startsWith('.')); } diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts index bf4aab208127f..03599cdc9fbcf 100644 --- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts @@ -143,7 +143,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, }, ]); - sinon.assert.calledTwice(stats.archivedDoc); + sinon.assert.calledTwice(stats.archivedDoc as any); expect(progress.getTotal()).to.be(2); expect(progress.getComplete()).to.be(2); }); diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts index 2535642c27cc9..35b068a691090 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts @@ -26,12 +26,12 @@ import { Progress } from '../../progress'; import { createIndexDocRecordsStream } from '../index_doc_records_stream'; import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; -const recordsToBulkBody = records => { +const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { const { index, id, source } = record.value; return [...acc, { index: { _index: index, _id: id } }, source]; - }, []); + }, [] as any[]); }; describe('esArchiver: createIndexDocRecordsStream()', () => { diff --git a/src/es_archiver/lib/docs/__tests__/stubs.js b/src/es_archiver/lib/docs/__tests__/stubs.ts similarity index 74% rename from src/es_archiver/lib/docs/__tests__/stubs.js rename to src/es_archiver/lib/docs/__tests__/stubs.ts index 9ed48efa7d03a..698d62e450cb4 100644 --- a/src/es_archiver/lib/docs/__tests__/stubs.js +++ b/src/es_archiver/lib/docs/__tests__/stubs.ts @@ -17,17 +17,22 @@ * under the License. */ +import { Client } from 'elasticsearch'; import sinon from 'sinon'; import Chance from 'chance'; import { times } from 'lodash'; + +import { Stats } from '../../stats'; + const chance = new Chance(); -export const createStubStats = () => ({ - indexedDoc: sinon.stub(), - archivedDoc: sinon.stub(), -}); +export const createStubStats = (): Stats => + ({ + indexedDoc: sinon.stub(), + archivedDoc: sinon.stub(), + } as any); -export const createPersonDocRecords = n => +export const createPersonDocRecords = (n: number) => times(n, () => ({ type: 'doc', value: { @@ -42,15 +47,21 @@ export const createPersonDocRecords = n => }, })); -export const createStubClient = (responses = []) => { - const createStubClientMethod = name => +type MockClient = Client & { + assertNoPendingResponses: () => void; +}; + +export const createStubClient = ( + responses: Array<(name: string, params: any) => any | Promise> = [] +): MockClient => { + const createStubClientMethod = (name: string) => sinon.spy(async params => { if (responses.length === 0) { throw new Error(`unexpected client.${name} call`); } const response = responses.shift(); - return await response(name, params); + return await response!(name, params); }); return { @@ -63,5 +74,5 @@ export const createStubClient = (responses = []) => { throw new Error(`There are ${responses.length} unsent responses.`); } }, - }; + } as any; }; diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.js b/src/es_archiver/lib/docs/generate_doc_records_stream.ts similarity index 80% rename from src/es_archiver/lib/docs/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/generate_doc_records_stream.ts index be8b0351d95c8..e255a0abc36c5 100644 --- a/src/es_archiver/lib/docs/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/generate_doc_records_stream.ts @@ -18,33 +18,36 @@ */ import { Transform } from 'stream'; +import { Client, SearchParams, SearchResponse } from 'elasticsearch'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client, stats, progress) { +export function createGenerateDocRecordsStream(client: Client, stats: Stats, progress: Progress) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(index, enc, callback) { try { - let remainingHits = null; - let resp = null; + let remainingHits = 0; + let resp: SearchResponse | null = null; while (!resp || remainingHits > 0) { if (!resp) { resp = await client.search({ - index: index, + index, scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, _source: true, - rest_total_hits_as_int: true, - }); + rest_total_hits_as_int: true, // not declared on SearchParams type + } as SearchParams); remainingHits = resp.hits.total; progress.addToTotal(remainingHits); } else { resp = await client.scroll({ - scrollId: resp._scroll_id, + scrollId: resp._scroll_id!, scroll: SCROLL_TIMEOUT, }); } @@ -68,7 +71,7 @@ export function createGenerateDocRecordsStream(client, stats, progress) { progress.addToComplete(resp.hits.hits.length); } - callback(null); + callback(undefined); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/docs/index.js b/src/es_archiver/lib/docs/index.ts similarity index 100% rename from src/es_archiver/lib/docs/index.js rename to src/es_archiver/lib/docs/index.ts diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.ts similarity index 86% rename from src/es_archiver/lib/docs/index_doc_records_stream.js rename to src/es_archiver/lib/docs/index_doc_records_stream.ts index 73fb75c52ff0a..8236ae8adb6db 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.ts @@ -17,11 +17,14 @@ * under the License. */ +import { Client } from 'elasticsearch'; import { Writable } from 'stream'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; -export function createIndexDocRecordsStream(client, stats, progress) { - async function indexDocs(docs) { - const body = []; +export function createIndexDocRecordsStream(client: Client, stats: Stats, progress: Progress) { + async function indexDocs(docs: any[]) { + const body: any[] = []; docs.forEach(doc => { stats.indexedDoc(doc.index); diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.ts similarity index 96% rename from src/es_archiver/lib/index.js rename to src/es_archiver/lib/index.ts index 246dd8169cd6b..960d51e411859 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.ts @@ -30,7 +30,7 @@ export { export { createFilterRecordsStream } from './records'; -export { createStats } from './stats'; +export { createStats, Stats } from './stats'; export { isGzip, diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.js b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts similarity index 76% rename from src/es_archiver/lib/indices/__tests__/create_index_stream.js rename to src/es_archiver/lib/indices/__tests__/create_index_stream.ts index 9e0f83c9f7eb9..c90497eded88c 100644 --- a/src/es_archiver/lib/indices/__tests__/create_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts @@ -34,10 +34,13 @@ import { createStubIndexRecord, createStubDocRecord, createStubClient, + createStubLogger, } from './stubs'; const chance = new Chance(); +const log = createStubLogger(); + describe('esArchiver: createCreateIndexStream()', () => { describe('defaults', () => { it('deletes existing indices, creates all', async () => { @@ -48,15 +51,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), ]); expect(stats.getTestSummary()).to.eql({ deletedIndex: 1, createdIndex: 2, }); - sinon.assert.callCount(client.indices.delete, 1); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 1); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('deletes existing aliases, creates all', async () => { @@ -67,14 +70,19 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats, log: { debug: () => {} } }), + createCreateIndexStream({ client, stats, log }), ]); - expect(client.indices.getAlias.calledOnce).to.be.ok(); - expect(client.indices.getAlias.args[0][0]).to.eql({ name: 'existing-index', ignore: [404] }); - expect(client.indices.delete.calledOnce).to.be.ok(); - expect(client.indices.delete.args[0][0]).to.eql({ index: ['actual-index'] }); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + name: 'existing-index', + ignore: [404], + }); + expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + index: ['actual-index'], + }); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('passes through "hit" records', async () => { @@ -86,7 +94,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubDocRecord('index', 1), createStubDocRecord('index', 2), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -101,11 +109,11 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('index', { foo: {} }), createStubDocRecord('index', 1), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); - sinon.assert.calledWith(client.indices.create, { + sinon.assert.calledWith(client.indices.create as sinon.SinonSpy, { method: 'PUT', index: 'index', body: { @@ -126,7 +134,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream([createStubIndexRecord('index'), ...randoms]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -140,7 +148,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream(nonRecordValues), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -161,6 +169,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), ]); @@ -169,9 +178,12 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing - expect(client.indices.create.args[0][0]).to.have.property('index', 'new-index'); + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing + expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + 'index', + 'new-index' + ); }); it('filters documents for skipped indices', async () => { @@ -190,6 +202,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), createConcatStream([]), @@ -199,8 +212,8 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing expect(output).to.have.length(2); expect(output).to.eql([ diff --git a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts similarity index 66% rename from src/es_archiver/lib/indices/__tests__/delete_index_stream.js rename to src/es_archiver/lib/indices/__tests__/delete_index_stream.ts index 955d1fff8779e..1c989ba158a29 100644 --- a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts @@ -23,7 +23,14 @@ import { createListStream, createPromiseFromStreams } from '../../../../legacy/u import { createDeleteIndexStream } from '../delete_index_stream'; -import { createStubStats, createStubClient, createStubIndexRecord } from './stubs'; +import { + createStubStats, + createStubClient, + createStubIndexRecord, + createStubLogger, +} from './stubs'; + +const log = createStubLogger(); describe('esArchiver: createDeleteIndexStream()', () => { it('deletes the index without checking if it exists', async () => { @@ -32,13 +39,13 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.notCalled(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.notCalled(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('reports the delete when the index existed', async () => { @@ -47,12 +54,12 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.calledOnce(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.calledOnce(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); }); diff --git a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js rename to src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts index 3523e9e82b153..7a3712ca1a336 100644 --- a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts @@ -45,10 +45,10 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { archivedIndex: 4, }); - sinon.assert.callCount(client.indices.get, 4); - sinon.assert.notCalled(client.indices.create); - sinon.assert.notCalled(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.callCount(client.indices.get as sinon.SinonSpy, 4); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('filters index metadata from settings', async () => { @@ -60,9 +60,9 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - const params = client.indices.get.args[0][0]; + const params = (client.indices.get as sinon.SinonSpy).args[0][0]; expect(params).to.have.property('filterPath'); - const filters = params.filterPath; + const filters: string[] = params.filterPath; expect(filters.some(path => path.includes('index.creation_date'))).to.be(true); expect(filters.some(path => path.includes('index.uuid'))).to.be(true); expect(filters.some(path => path.includes('index.version'))).to.be(true); @@ -73,7 +73,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient(['index1', 'index2', 'index3']); - const indexRecords = await createPromiseFromStreams([ + const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), createGenerateIndexRecordsStream(client, stats), createConcatStream([]), diff --git a/src/es_archiver/lib/indices/__tests__/stubs.js b/src/es_archiver/lib/indices/__tests__/stubs.js deleted file mode 100644 index 00649a06f9efe..0000000000000 --- a/src/es_archiver/lib/indices/__tests__/stubs.js +++ /dev/null @@ -1,128 +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 sinon from 'sinon'; - -export const createStubStats = () => ({ - createdIndex: sinon.stub(), - createdAliases: sinon.stub(), - deletedIndex: sinon.stub(), - skippedIndex: sinon.stub(), - archivedIndex: sinon.stub(), - getTestSummary() { - const summary = {}; - Object.keys(this).forEach(key => { - if (this[key].callCount) { - summary[key] = this[key].callCount; - } - }); - return summary; - }, -}); - -export const createStubIndexRecord = (index, aliases = {}) => ({ - type: 'index', - value: { index, aliases }, -}); - -export const createStubDocRecord = (index, id) => ({ - type: 'doc', - value: { index, id }, -}); - -const createEsClientError = errorType => { - const err = new Error(`ES Client Error Stub "${errorType}"`); - err.body = { - error: { - type: errorType, - }, - }; - return err; -}; - -const indexAlias = (aliases, index) => Object.keys(aliases).find(k => aliases[k] === index); - -export const createStubClient = (existingIndices = [], aliases = {}) => ({ - indices: { - get: sinon.spy(async ({ index }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - - return { - [index]: { - mappings: {}, - settings: {}, - }, - }; - }), - existsAlias: sinon.spy(({ name }) => { - return Promise.resolve(aliases.hasOwnProperty(name)); - }), - getAlias: sinon.spy(async ({ index, name }) => { - if (index && existingIndices.indexOf(index) >= 0) { - const result = indexAlias(aliases, index); - return { [index]: { aliases: result ? { [result]: {} } : {} } }; - } - - if (name && aliases[name]) { - return { [aliases[name]]: { aliases: { [name]: {} } } }; - } - - return { status: 404 }; - }), - updateAliases: sinon.spy(async ({ body }) => { - body.actions.forEach(({ add: { index, alias } }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - existingIndices.push({ index, alias }); - }); - - return { ok: true }; - }), - create: sinon.spy(async ({ index }) => { - if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { - throw createEsClientError('resource_already_exists_exception'); - } else { - existingIndices.push(index); - return { ok: true }; - } - }), - delete: sinon.spy(async ({ index }) => { - const indices = Array.isArray(index) ? index : [index]; - if (indices.every(ix => existingIndices.includes(ix))) { - // Delete aliases associated with our indices - indices.forEach(ix => { - const alias = Object.keys(aliases).find(k => aliases[k] === ix); - if (alias) { - delete aliases[alias]; - } - }); - indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); - return { ok: true }; - } else { - throw createEsClientError('index_not_found_exception'); - } - }), - exists: sinon.spy(async () => { - throw new Error('Do not use indices.exists(). React to errors instead.'); - }), - }, -}); diff --git a/src/es_archiver/lib/indices/__tests__/stubs.ts b/src/es_archiver/lib/indices/__tests__/stubs.ts new file mode 100644 index 0000000000000..3f4682299c38d --- /dev/null +++ b/src/es_archiver/lib/indices/__tests__/stubs.ts @@ -0,0 +1,154 @@ +/* + * 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 { Client } from 'elasticsearch'; +import sinon from 'sinon'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../../stats'; + +type StubStats = Stats & { + getTestSummary: () => Record; +}; + +export const createStubStats = (): StubStats => + ({ + createdIndex: sinon.stub(), + createdAliases: sinon.stub(), + deletedIndex: sinon.stub(), + skippedIndex: sinon.stub(), + archivedIndex: sinon.stub(), + getTestSummary() { + const summary: Record = {}; + Object.keys(this).forEach(key => { + if (this[key].callCount) { + summary[key] = this[key].callCount; + } + }); + return summary; + }, + } as any); + +export const createStubLogger = (): ToolingLog => + ({ + debug: sinon.stub(), + info: sinon.stub(), + success: sinon.stub(), + warning: sinon.stub(), + error: sinon.stub(), + } as any); + +export const createStubIndexRecord = (index: string, aliases = {}) => ({ + type: 'index', + value: { index, aliases }, +}); + +export const createStubDocRecord = (index: string, id: number) => ({ + type: 'doc', + value: { index, id }, +}); + +const createEsClientError = (errorType: string) => { + const err = new Error(`ES Client Error Stub "${errorType}"`); + (err as any).body = { + error: { + type: errorType, + }, + }; + return err; +}; + +const indexAlias = (aliases: Record, index: string) => + Object.keys(aliases).find(k => aliases[k] === index); + +type StubClient = Client; + +export const createStubClient = ( + existingIndices: string[] = [], + aliases: Record = {} +): StubClient => + ({ + indices: { + get: sinon.spy(async ({ index }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + + return { + [index]: { + mappings: {}, + settings: {}, + }, + }; + }), + existsAlias: sinon.spy(({ name }) => { + return Promise.resolve(aliases.hasOwnProperty(name)); + }), + getAlias: sinon.spy(async ({ index, name }) => { + if (index && existingIndices.indexOf(index) >= 0) { + const result = indexAlias(aliases, index); + return { [index]: { aliases: result ? { [result]: {} } : {} } }; + } + + if (name && aliases[name]) { + return { [aliases[name]]: { aliases: { [name]: {} } } }; + } + + return { status: 404 }; + }), + updateAliases: sinon.spy(async ({ body }) => { + body.actions.forEach( + ({ add: { index, alias } }: { add: { index: string; alias: string } }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + existingIndices.push({ index, alias } as any); + } + ); + + return { ok: true }; + }), + create: sinon.spy(async ({ index }) => { + if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { + throw createEsClientError('resource_already_exists_exception'); + } else { + existingIndices.push(index); + return { ok: true }; + } + }), + delete: sinon.spy(async ({ index }) => { + const indices = Array.isArray(index) ? index : [index]; + if (indices.every(ix => existingIndices.includes(ix))) { + // Delete aliases associated with our indices + indices.forEach(ix => { + const alias = Object.keys(aliases).find(k => aliases[k] === ix); + if (alias) { + delete aliases[alias]; + } + }); + indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); + return { ok: true }; + } else { + throw createEsClientError('index_not_found_exception'); + } + }), + exists: sinon.spy(async () => { + throw new Error('Do not use indices.exists(). React to errors instead.'); + }), + }, + } as any); diff --git a/src/es_archiver/lib/indices/create_index_stream.js b/src/es_archiver/lib/indices/create_index_stream.ts similarity index 81% rename from src/es_archiver/lib/indices/create_index_stream.js rename to src/es_archiver/lib/indices/create_index_stream.ts index 8fe4bc568cd23..df9d3bb623ad6 100644 --- a/src/es_archiver/lib/indices/create_index_stream.js +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -17,13 +17,36 @@ * under the License. */ -import { Transform } from 'stream'; - +import { Transform, Readable } from 'stream'; import { get, once } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { Stats } from '../stats'; import { deleteKibanaIndices } from './kibana_index'; import { deleteIndex } from './delete_index'; -export function createCreateIndexStream({ client, stats, skipExisting, log }) { +interface DocRecord { + value: { + index: string; + type: string; + settings: Record; + mappings: Record; + aliases: Record; + }; +} + +export function createCreateIndexStream({ + client, + stats, + skipExisting = false, + log, +}: { + client: Client; + stats: Stats; + skipExisting?: boolean; + log: ToolingLog; +}) { const skipDocsFromIndices = new Set(); // If we're trying to import Kibana index docs, we need to ensure that @@ -31,7 +54,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { // migrations. This only needs to be done once per archive load operation. const deleteKibanaIndicesOnce = once(deleteKibanaIndices); - async function handleDoc(stream, record) { + async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { return; } @@ -39,7 +62,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { stream.push(record); } - async function handleIndex(record) { + async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; const isKibana = index.startsWith('.kibana'); @@ -102,7 +125,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { break; } - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/indices/delete_index.js b/src/es_archiver/lib/indices/delete_index.ts similarity index 76% rename from src/es_archiver/lib/indices/delete_index.js rename to src/es_archiver/lib/indices/delete_index.ts index 6f60d9533a36b..e3fca587fbc3d 100644 --- a/src/es_archiver/lib/indices/delete_index.js +++ b/src/es_archiver/lib/indices/delete_index.ts @@ -18,22 +18,34 @@ */ import { get } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; // see https://github.com/elastic/elasticsearch/blob/99f88f15c5febbca2d13b5b5fda27b844153bf1a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java#L313-L319 const PENDING_SNAPSHOT_STATUSES = ['INIT', 'STARTED', 'WAITING']; -export async function deleteIndex(options) { +export async function deleteIndex(options: { + client: Client; + stats: Stats; + index: string; + log: ToolingLog; + retryIfSnapshottingCount?: number; +}): Promise { const { client, stats, index, log, retryIfSnapshottingCount = 10 } = options; const getIndicesToDelete = async () => { const aliasInfo = await client.indices.getAlias({ name: index, ignore: [404] }); - return aliasInfo.status === 404 ? index : Object.keys(aliasInfo); + return aliasInfo.status === 404 ? [index] : Object.keys(aliasInfo); }; try { const indicesToDelete = await getIndicesToDelete(); await client.indices.delete({ index: indicesToDelete }); - stats.deletedIndex(indicesToDelete); + for (let i = 0; i < indicesToDelete.length; i++) { + const indexToDelete = indicesToDelete[i]; + stats.deletedIndex(indexToDelete); + } } catch (error) { if (retryIfSnapshottingCount > 0 && isDeleteWhileSnapshotInProgressError(error)) { stats.waitingForInProgressSnapshot(index); @@ -56,7 +68,7 @@ export async function deleteIndex(options) { * @param {Error} error * @return {Boolean} */ -export function isDeleteWhileSnapshotInProgressError(error) { +export function isDeleteWhileSnapshotInProgressError(error: object) { return get(error, 'body.error.reason', '').startsWith( 'Cannot delete indices that are being snapshotted' ); @@ -65,13 +77,9 @@ export function isDeleteWhileSnapshotInProgressError(error) { /** * Wait for the any snapshot in any repository that is * snapshotting this index to complete. - * - * @param {EsClient} client - * @param {string} index the name of the index to look for - * @return {Promise} */ -export async function waitForSnapshotCompletion(client, index, log) { - const isSnapshotPending = async (repository, snapshot) => { +export async function waitForSnapshotCompletion(client: Client, index: string, log: ToolingLog) { + const isSnapshotPending = async (repository: string, snapshot: string) => { const { snapshots: [status], } = await client.snapshot.status({ @@ -83,7 +91,7 @@ export async function waitForSnapshotCompletion(client, index, log) { return PENDING_SNAPSHOT_STATUSES.includes(status.state); }; - const getInProgressSnapshots = async repository => { + const getInProgressSnapshots = async (repository: string) => { const { snapshots: inProgressSnapshots } = await client.snapshot.get({ repository, snapshot: '_current', @@ -91,9 +99,9 @@ export async function waitForSnapshotCompletion(client, index, log) { return inProgressSnapshots; }; - for (const repository of Object.keys(await client.snapshot.getRepository())) { + for (const repository of Object.keys(await client.snapshot.getRepository({} as any))) { const allInProgress = await getInProgressSnapshots(repository); - const found = allInProgress.find(s => s.indices.includes(index)); + const found = allInProgress.find((s: any) => s.indices.includes(index)); if (!found) { continue; diff --git a/src/es_archiver/lib/indices/delete_index_stream.js b/src/es_archiver/lib/indices/delete_index_stream.ts similarity index 86% rename from src/es_archiver/lib/indices/delete_index_stream.js rename to src/es_archiver/lib/indices/delete_index_stream.ts index 31a49ed30a124..b4e1e530e1f84 100644 --- a/src/es_archiver/lib/indices/delete_index_stream.js +++ b/src/es_archiver/lib/indices/delete_index_stream.ts @@ -18,11 +18,19 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; -export function createDeleteIndexStream(client, stats, log, kibanaPluginIds) { +export function createDeleteIndexStream( + client: Client, + stats: Stats, + log: ToolingLog, + kibanaPluginIds: string[] +) { return new Transform({ readableObjectMode: true, writableObjectMode: true, diff --git a/src/es_archiver/lib/indices/generate_index_records_stream.js b/src/es_archiver/lib/indices/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/generate_index_records_stream.js rename to src/es_archiver/lib/indices/generate_index_records_stream.ts index 1d1a44aa634c2..b4b98f8ae262c 100644 --- a/src/es_archiver/lib/indices/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/generate_index_records_stream.ts @@ -18,14 +18,16 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { Stats } from '../stats'; -export function createGenerateIndexRecordsStream(client, stats) { +export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(indexOrAlias, enc, callback) { try { - const resp = await client.indices.get({ + const resp = (await client.indices.get({ index: indexOrAlias, filterPath: [ '*.settings', @@ -36,7 +38,7 @@ export function createGenerateIndexRecordsStream(client, stats) { '-*.settings.index.version', '-*.settings.index.provided_name', ], - }); + })) as Record; for (const [index, { settings, mappings }] of Object.entries(resp)) { const { diff --git a/src/es_archiver/lib/indices/index.js b/src/es_archiver/lib/indices/index.ts similarity index 100% rename from src/es_archiver/lib/indices/index.js rename to src/es_archiver/lib/indices/index.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.ts similarity index 70% rename from src/es_archiver/lib/indices/kibana_index.js rename to src/es_archiver/lib/indices/kibana_index.ts index 744132bdcef69..de67ba7c4e31e 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.ts @@ -17,29 +17,34 @@ * under the License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import fs from 'fs'; -import path from 'path'; +import Path from 'path'; import { promisify } from 'util'; import { toArray } from 'rxjs/operators'; +import { Client, CreateDocumentParams } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; -import { collectUiExports } from '../../../legacy/ui/ui_exports'; import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { SavedObjectsSchema } from '../../../core/server/saved_objects'; +// @ts-ignore +import { collectUiExports } from '../../../legacy/ui/ui_exports'; +// @ts-ignore import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** * Load the uiExports for a Kibana instance, only load uiExports from xpack if * it is enabled in the Kibana server. */ -const getUiExports = async kibanaPluginIds => { +const getUiExports = async (kibanaPluginIds: string[]) => { const xpackEnabled = kibanaPluginIds.includes('xpack_main'); const { spec$ } = await findPluginSpecs({ plugins: { - scanDirs: [path.resolve(__dirname, '../../../legacy/core_plugins')], - paths: xpackEnabled ? [path.resolve(__dirname, '../../../../x-pack')] : [], + scanDirs: [Path.resolve(__dirname, '../../../legacy/core_plugins')], + paths: xpackEnabled ? [Path.resolve(__dirname, '../../../../x-pack')] : [], }, }); @@ -50,7 +55,15 @@ const getUiExports = async kibanaPluginIds => { /** * Deletes all indices that start with `.kibana` */ -export async function deleteKibanaIndices({ client, stats, log }) { +export async function deleteKibanaIndices({ + client, + stats, + log, +}: { + client: Client; + stats: Stats; + log: ToolingLog; +}) { const indexNames = await fetchKibanaIndices(client); if (!indexNames.length) { return; @@ -76,37 +89,52 @@ export async function deleteKibanaIndices({ client, stats, log }) { * builds up an object that implements just enough of the kbnMigrations interface * as is required by migrations. */ -export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { +export async function migrateKibanaIndex({ + client, + log, + kibanaPluginIds, +}: { + client: Client; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { const uiExports = await getUiExports(kibanaPluginIds); const kibanaVersion = await loadKibanaVersion(); - const config = { + const config: Record = { 'xpack.task_manager.index': '.kibana_task_manager', }; + const logger = { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + fatal: log.error.bind(log), + log: (entry: any) => log.info(entry.message), + get: () => logger, + }; + const migratorOptions = { - config: { get: path => config[path] }, + config: { get: (path: string) => config[path] } as any, savedObjectsConfig: { scrollDuration: '5m', batchSize: 100, pollInterval: 100, + skip: false, }, kibanaConfig: { index: '.kibana', - }, - logger: { - trace: log.verbose.bind(log), - debug: log.debug.bind(log), - info: log.info.bind(log), - warn: log.warning.bind(log), - error: log.error.bind(log), - }, - version: kibanaVersion, + } as any, + logger, + kibanaVersion, savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), savedObjectMappings: uiExports.savedObjectMappings, savedObjectMigrations: uiExports.savedObjectMigrations, savedObjectValidations: uiExports.savedObjectValidations, - callCluster: (path, ...args) => _.get(client, path).call(client, ...args), + callCluster: (path: string, ...args: any[]) => + (get(client, path) as Function).call(client, ...args), }; return await new KibanaMigrator(migratorOptions).runMigrations(); @@ -114,8 +142,8 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { async function loadKibanaVersion() { const readFile = promisify(fs.readFile); - const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); - return JSON.parse(packageJson).version; + const packageJson = await readFile(Path.join(__dirname, '../../../../package.json')); + return JSON.parse(packageJson.toString('utf-8')).version; } /** @@ -123,16 +151,24 @@ async function loadKibanaVersion() { * .kibana, .kibana_1, .kibana_323, etc. This finds all indices starting * with .kibana, then filters out any that aren't actually Kibana's core * index (e.g. we don't want to remove .kibana_task_manager or the like). - * - * @param {string} index */ -async function fetchKibanaIndices(client) { +async function fetchKibanaIndices(client: Client) { const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = index => /^\.kibana(:?_\d*)?$/.test(index); - return kibanaIndices.map(x => x.index).filter(isKibanaIndex); + const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex); } -export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds }) { +export async function cleanKibanaIndices({ + client, + stats, + log, + kibanaPluginIds, +}: { + client: Client; + stats: Stats; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { if (!kibanaPluginIds.includes('spaces')) { return await deleteKibanaIndices({ client, @@ -178,7 +214,7 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } stats.deletedIndex('.kibana'); } -export async function createDefaultSpace({ index, client }) { +export async function createDefaultSpace({ index, client }: { index: string; client: Client }) { await client.create({ index, id: 'space:default', @@ -193,5 +229,5 @@ export async function createDefaultSpace({ index, client }) { _reserved: true, }, }, - }); + } as CreateDocumentParams); } diff --git a/src/es_archiver/lib/records/__tests__/filter_records_stream.js b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts similarity index 97% rename from src/es_archiver/lib/records/__tests__/filter_records_stream.js rename to src/es_archiver/lib/records/__tests__/filter_records_stream.ts index fd35575ca59ba..d5830478decba 100644 --- a/src/es_archiver/lib/records/__tests__/filter_records_stream.js +++ b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts @@ -51,7 +51,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { it('produces record values that have a matching type', async () => { const type1 = chance.word({ length: 5 }); - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([ { type: type1, value: {} }, { type: type1, value: {} }, diff --git a/src/es_archiver/lib/records/filter_records_stream.js b/src/es_archiver/lib/records/filter_records_stream.ts similarity index 91% rename from src/es_archiver/lib/records/filter_records_stream.js rename to src/es_archiver/lib/records/filter_records_stream.ts index 5a835ffe8e84d..191cbd3b921e3 100644 --- a/src/es_archiver/lib/records/filter_records_stream.js +++ b/src/es_archiver/lib/records/filter_records_stream.ts @@ -19,14 +19,14 @@ import { Transform } from 'stream'; -export function createFilterRecordsStream(type) { +export function createFilterRecordsStream(type: string) { return new Transform({ writableObjectMode: true, readableObjectMode: true, transform(record, enc, callback) { if (record && record.type === type) { - callback(null, record); + callback(undefined, record); } else { callback(); } diff --git a/src/es_archiver/lib/records/index.js b/src/es_archiver/lib/records/index.ts similarity index 100% rename from src/es_archiver/lib/records/index.js rename to src/es_archiver/lib/records/index.ts diff --git a/src/es_archiver/lib/stats.ts b/src/es_archiver/lib/stats.ts index 5f73304abf9a8..c69b764fc7290 100644 --- a/src/es_archiver/lib/stats.ts +++ b/src/es_archiver/lib/stats.ts @@ -37,6 +37,8 @@ export interface IndexStats { }; } +export type Stats = ReturnType; + export function createStats(name: string, log: ToolingLog) { const info = (msg: string, ...args: any[]) => log.info(`[${name}] ${msg}`, ...args); const debug = (msg: string, ...args: any[]) => log.debug(`[${name}] ${msg}`, ...args); diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts index a57caad1d34bf..c294c79542bbe 100644 --- a/src/legacy/utils/index.d.ts +++ b/src/legacy/utils/index.d.ts @@ -18,3 +18,16 @@ */ export function unset(object: object, rawPath: string): void; + +export { + concatStreamProviders, + createConcatStream, + createFilterStream, + createIntersperseStream, + createListStream, + createMapStream, + createPromiseFromStreams, + createReduceStream, + createReplaceStream, + createSplitStream, +} from './streams'; diff --git a/src/legacy/utils/streams/index.d.ts b/src/legacy/utils/streams/index.d.ts index b8d4c67050b2d..5ef39b292c685 100644 --- a/src/legacy/utils/streams/index.d.ts +++ b/src/legacy/utils/streams/index.d.ts @@ -20,17 +20,17 @@ import { Readable, Transform, Writable, TransformOptions } from 'stream'; export function concatStreamProviders( - sourceProviders: Readable[], + sourceProviders: Array<() => Readable>, options: TransformOptions ): Transform; export function createIntersperseStream(intersperseChunk: string | Buffer): Transform; export function createSplitStream(splitChunk: T): Transform; -export function createListStream(items: any[]): Readable; +export function createListStream(items: any | any[]): Readable; export function createReduceStream(reducer: (value: any, chunk: T, enc: string) => T): Transform; export function createPromiseFromStreams([first, ...rest]: [Readable, ...Writable[]]): Promise< T >; -export function createConcatStream(initial: any): Transform; +export function createConcatStream(initial?: any): Transform; export function createMapStream(fn: (value: T, i: number) => void): Transform; export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform; export function createFilterStream(fn: (obj: T) => boolean): Transform; From 3d96e4c95bdc7ce2401b66fc1c816d826496382a Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 3 Feb 2020 13:24:38 +0100 Subject: [PATCH 03/17] [SIEM] Fix Welcome to SIEM typo (#56582) --- x-pack/legacy/plugins/siem/public/pages/common/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts index 3e20338375616..072aee50d5136 100644 --- a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts @@ -12,7 +12,7 @@ export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management, you’ll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', { From 598968f20f1b3b09429dd6eac079b010913f0dee Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 3 Feb 2020 13:33:19 +0100 Subject: [PATCH 04/17] fix duplicate header warning (#56491) logged when attaching 2+ headers due to excessive check Co-authored-by: Elastic Machine --- src/core/server/http/integration_tests/lifecycle.test.ts | 1 + src/core/server/http/lifecycle/on_pre_response.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index b16352838fad1..6dc7ece1359df 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -721,6 +721,7 @@ describe('Auth', () => { res.ok({ headers: { 'www-authenticate': 'from handler', + 'another-header': 'yet another header', }, }) ); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 45d7478df9805..50d3d7b47bf8d 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -120,8 +120,8 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo ...(result.headers as any), // hapi types don't specify string[] as valid value }; } else { + findHeadersIntersection(response.headers, result.headers, log); for (const [headerName, headerValue] of Object.entries(result.headers)) { - findHeadersIntersection(response.headers, result.headers, log); response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value } } From 8def60e1dac63ba7d7900bbffca6e1519ff1dfb7 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Mon, 3 Feb 2020 14:43:10 +0100 Subject: [PATCH 05/17] Unify Security and EncryptedSavedObjects public contract names according to NP migration guide. (#56597) --- .../plugins/alerting/server/alerts_client.ts | 6 +++--- .../alerting/server/alerts_client_factory.ts | 6 +++--- x-pack/legacy/plugins/alerting/server/shim.ts | 18 +++++++++--------- .../server/task_runner/task_runner_factory.ts | 4 ++-- .../plugins/encrypted_saved_objects/index.ts | 4 ++-- x-pack/legacy/plugins/reporting/index.ts | 2 +- .../legacy/plugins/reporting/server/plugin.ts | 2 +- x-pack/legacy/plugins/siem/server/plugin.ts | 4 ++-- .../actions/server/lib/action_executor.ts | 4 ++-- .../actions/server/lib/task_runner_factory.ts | 4 ++-- x-pack/plugins/actions/server/plugin.ts | 8 ++++---- x-pack/plugins/case/server/plugin.ts | 2 +- x-pack/plugins/case/server/services/index.ts | 5 +---- .../encrypted_saved_objects/server/index.ts | 2 +- .../encrypted_saved_objects/server/mocks.ts | 6 +++--- .../encrypted_saved_objects/server/plugin.ts | 6 +++--- x-pack/plugins/security/server/index.ts | 6 +++--- x-pack/plugins/security/server/mocks.ts | 4 ++-- x-pack/plugins/security/server/plugin.ts | 4 ++-- .../lib/spaces_client/spaces_client.test.ts | 8 ++++---- .../server/lib/spaces_client/spaces_client.ts | 6 +++--- x-pack/plugins/spaces/server/plugin.ts | 2 +- .../server/spaces_service/spaces_service.ts | 2 +- .../common/fixtures/plugins/aad/index.ts | 5 +++-- .../plugins/encrypted_saved_objects/index.ts | 9 +++++---- 25 files changed, 64 insertions(+), 65 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index a6ba936b76570..1346d403edda1 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -29,7 +29,7 @@ import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; type NormalizedAlertAction = Omit; @@ -45,7 +45,7 @@ interface ConstructorOptions { taskManager: TaskManagerStartContract; savedObjectsClient: SavedObjectsClientContract; alertTypeRegistry: AlertTypeRegistry; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceId?: string; namespace?: string; getUserName: () => Promise; @@ -120,7 +120,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ alertTypeRegistry, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts index eab1cc3ce627b..de789fba0ac38 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts @@ -11,7 +11,7 @@ import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { SecurityPluginStartContract } from './shim'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { InvalidateAPIKeyParams } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; export interface ConstructorOpts { @@ -21,7 +21,7 @@ export interface ConstructorOpts { securityPluginSetup?: SecurityPluginStartContract; getSpaceId: (request: Hapi.Request) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; } export class AlertsClientFactory { @@ -31,7 +31,7 @@ export class AlertsClientFactory { private readonly securityPluginSetup?: SecurityPluginStartContract; private readonly getSpaceId: (request: Hapi.Request) => string | undefined; private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction; - private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor(options: ConstructorOpts) { this.logger = options.logger; diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index 80d01ea722926..bc8b0eb863634 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -15,10 +15,10 @@ import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/ser import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { CoreSetup, LoggerFactory, @@ -44,8 +44,8 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type SecurityPluginSetupContract = Pick; -export type SecurityPluginStartContract = Pick; +export type SecurityPluginSetupContract = Pick; +export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; /** @@ -71,14 +71,14 @@ export interface AlertingPluginsSetup { taskManager: TaskManagerSetupContract; actions: ActionsPluginSetupContract; xpack_main: XPackMainPluginSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; security?: SecurityPluginStartContract; spaces: () => SpacesPluginStartContract | undefined; - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } @@ -120,7 +120,7 @@ export function shim( actions: newPlatform.setup.plugins.actions as ActionsPluginSetupContract, xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup, licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; @@ -131,7 +131,7 @@ export function shim( // initializes after this function is called spaces: () => server.plugins.spaces, encryptedSavedObjects: newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsStartContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart, taskManager: getTaskManagerStart(server)!, }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts index 67fef33b69c6d..d2ecfb64c8a81 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; import { RunContext } from '../../../../../plugins/task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../plugins/encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../../plugins/actions/server'; import { AlertType, @@ -19,7 +19,7 @@ export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; executeAction: ActionsPluginStartContract['execute']; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; } diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 69058a7a33f59..ce343dba006cf 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -6,7 +6,7 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import { PluginSetupContract } from '../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../../plugins/encrypted_saved_objects/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; @@ -29,7 +29,7 @@ export const encryptedSavedObjects = (kibana: { init(server: Legacy.Server) { const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins - .encryptedSavedObjects as unknown) as PluginSetupContract; + .encryptedSavedObjects as unknown) as EncryptedSavedObjectsPluginSetup; if (!encryptedSavedObjectsPlugin) { throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.'); } diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index d2a68e309a4b3..966e4ff209ad6 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -9,7 +9,7 @@ import { Legacy } from 'kibana'; import { IUiSettingsClient } from 'kibana/server'; import { resolve } from 'path'; import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../plugins/security/server'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { LegacySetup, ReportingPlugin, reportingPluginFactory } from './server/plugin'; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index a2938d442f7df..e618d23e8ed1f 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -14,7 +14,7 @@ import { } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 96eef2f44e5a0..94314367be59c 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -17,7 +17,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; -export type SiemPluginSecurity = Pick; +export type SiemPluginSecurity = Pick; export interface PluginsSetup { features: FeaturesSetupContract; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index be6916a74fe88..03a892a42792e 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,7 +12,7 @@ import { GetServicesFunction, RawAction, } from '../types'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; @@ -21,7 +21,7 @@ export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c3e89e0c16efc..c78b43f4ef3ba 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -8,12 +8,12 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; export interface TaskRunnerContext { logger: Logger; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; getScopedSavedObjectsClient: CoreStart['savedObjects']['getScopedClient']; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index cb0e3347541fd..dab09fc455ecf 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -20,8 +20,8 @@ import { } from '../../../../src/core/server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -67,13 +67,13 @@ export interface PluginStartContract { export interface ActionsPluginsSetup { taskManager: TaskManagerSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; spaces?: SpacesPluginSetup; event_log: IEventLogService; } export interface ActionsPluginsStart { - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index c52461cade058..37d087433a2ed 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { CaseService } from './services'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map(config => config)); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 684d905a5c71f..531d5fa5b87e5 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -21,10 +21,7 @@ import { UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; -import { - AuthenticatedUser, - PluginSetupContract as SecurityPluginSetup, -} from '../../../security/server'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 5e6edb95ec37a..3b4b91de355c7 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -9,7 +9,7 @@ import { ConfigSchema } from './config'; import { Plugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; -export { PluginSetupContract, PluginStartContract } from './plugin'; +export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 7f53f47760f12..13d7127db7835 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract, PluginStartContract } from './plugin'; +import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, usingEphemeralEncryptionKey: true, - } as jest.Mocked; + } as jest.Mocked; } function createEncryptedSavedObjectsStartMock() { return { isEncryptionError: jest.fn(), getDecryptedAsInternalUser: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; } export const encryptedSavedObjectsMock = { diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index d9185251ca466..a0218c51c2723 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -20,13 +20,13 @@ import { import { EncryptedSavedObjectsAuditLogger } from './audit'; import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; -export interface PluginSetupContract { +export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; usingEphemeralEncryptionKey: boolean; } -export interface PluginStartContract extends SavedObjectsSetup { +export interface EncryptedSavedObjectsPluginStart extends SavedObjectsSetup { isEncryptionError: (error: Error) => boolean; } @@ -59,7 +59,7 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup): Promise { + public async setup(core: CoreSetup): Promise { const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 17e49b8cf40d3..c0e86b289fe54 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -12,7 +12,7 @@ import { RecursiveReadonly, } from '../../../../src/core/server'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin'; +import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. @@ -24,7 +24,7 @@ export { InvalidateAPIKeyParams, InvalidateAPIKeyResult, } from './authentication'; -export { PluginSetupContract }; +export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; export const config: PluginConfigDescriptor> = { @@ -35,7 +35,7 @@ export const config: PluginConfigDescriptor> = { ], }; export const plugin: PluginInitializer< - RecursiveReadonly, + RecursiveReadonly, void, PluginSetupDependencies > = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index d5c08d5ab1ab9..ababf12c2be60 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract } from './plugin'; +import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; @@ -19,7 +19,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), - __legacyCompat: {} as PluginSetupContract['__legacyCompat'], + __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index ce682d8b30eb7..5764418234739 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -49,7 +49,7 @@ export interface LegacyAPI { /** * Describes public Security plugin contract returned at the `setup` stage. */ -export interface PluginSetupContract { +export interface SecurityPluginSetup { authc: Authentication; authz: Pick; @@ -166,7 +166,7 @@ export class Plugin { csp: core.http.csp, }); - return deepFreeze({ + return deepFreeze({ authc, authz: { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 24a994e836e87..74e75fb8f12c7 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { SpacesClient } from './spaces_client'; import { ConfigType, ConfigSchema } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; @@ -224,17 +224,17 @@ describe('#getAll', () => { [ { purpose: undefined, - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'any', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'copySavedObjectsIntoSpace', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), }, ].forEach(scenario => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index f964ae7d7ac32..22c34c03368e3 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { isReservedSpace } from '../../../common/is_reserved_space'; import { Space } from '../../../common/model/space'; import { SpacesAuditLogger } from '../audit_logger'; @@ -17,7 +17,7 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObject const PURPOSE_PRIVILEGE_MAP: Record< GetSpacePurpose, - (authorization: SecurityPluginSetupContract['authz']) => string + (authorization: SecurityPluginSetup['authz']) => string > = { any: authorization => authorization.actions.login, copySavedObjectsIntoSpace: authorization => @@ -28,7 +28,7 @@ export class SpacesClient { constructor( private readonly auditLogger: SpacesAuditLogger, private readonly debugLogger: (message: string) => void, - private readonly authorization: SecurityPluginSetupContract['authz'] | null, + private readonly authorization: SecurityPluginSetup['authz'] | null, private readonly callWithRequestSavedObjectRepository: any, private readonly config: ConfigType, private readonly internalSavedObjectRepository: any, diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index b8ef81c05f7aa..52ff7eaee3d68 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index f8ed58fa57551..95bda96d89461 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -8,7 +8,7 @@ import { map, take } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index d7bee93f5c94b..7194c642e7015 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { Legacy } from 'kibana'; import KbnServer from '../../../../../../../src/legacy/server/kbn_server'; -import { PluginStartContract } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; interface CheckAADRequest extends Hapi.Request { payload: { @@ -25,7 +25,8 @@ export default function(kibana: any) { name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; - const esoPlugin = newPlatform.start.plugins.encryptedSavedObjects as PluginStartContract; + const esoPlugin = newPlatform.start.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; server.route({ method: 'POST', diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts index a194e477da755..e61b8f24a1f69 100644 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts @@ -8,8 +8,8 @@ import { Request } from 'hapi'; import { boomify, badRequest } from 'boom'; import { Legacy } from 'kibana'; import { - PluginSetupContract, - PluginStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; @@ -26,7 +26,7 @@ export default function esoPlugin(kibana: any) { path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', async handler(request: Request) { const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as PluginStartContract; + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); try { return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( @@ -44,7 +44,8 @@ export default function esoPlugin(kibana: any) { }, }); - (server.newPlatform.setup.plugins.encryptedSavedObjects as PluginSetupContract).registerType({ + (server.newPlatform.setup.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ type: SAVED_OBJECT_WITH_SECRET_TYPE, attributesToEncrypt: new Set(['privateProperty']), attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), From f2713659f63fda25881e1c99087dc084cf4c85a1 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Mon, 3 Feb 2020 09:01:34 -0500 Subject: [PATCH 06/17] filtering (making it work with expressions) (#55351) * adding meta information to KibanaDatatable * updating filtering functions to use new information * moving filter creation to APPLY_FILTER_ACTION * adding SELECT_RANGE_ACTION and TRIGGER * making _meta optional * inlining legacy code for inspector * fixing jest tests * keeping apply_filter_action and adding value_click_action and trigger * utilities for serializing/unserializing aggConfigs * renaming prop to indexPatternId * cleanup * updating interpreter functional baselines * trying to fix tests * Fix legend tests * reverting update to multi metric screenshot * updating based on review * updating tests Co-authored-by: Nick Partridge Co-authored-by: Elastic Machine --- .../public/actions}/filters/brush_event.js | 14 +- .../actions}/filters/brush_event.test.js | 78 +++++++---- .../filters/brush_event.test.mocks.ts | 2 +- .../filters/create_filters_from_event.js} | 51 ++++--- .../public/actions/select_range_action.ts | 91 +++++++++++++ .../data/public/actions/value_click_action.ts | 126 ++++++++++++++++++ src/legacy/core_plugins/data/public/legacy.ts | 6 +- src/legacy/core_plugins/data/public/plugin.ts | 28 +++- .../build_tabular_inspector_data.ts | 4 +- .../search/expressions/create_filter.js | 66 +++++++++ .../data/public/search/expressions/esaggs.ts | 2 + .../data/public/search/expressions/utils.ts | 51 +++++++ .../core_plugins/data/public/search/index.ts | 1 + .../vislib/components/legend/legend.test.tsx | 58 ++++---- .../vislib/components/legend/legend.tsx | 60 +++++---- .../public/embeddable/visualize_embeddable.ts | 36 ++--- .../public/np_ready/public/filters/index.ts | 21 --- .../public/np_ready/public/index.ts | 3 - .../np_ready/public/types/base_vis_type.js | 10 -- src/legacy/ui/public/agg_types/agg_config.ts | 2 +- src/legacy/ui/public/agg_types/agg_types.ts | 92 +++++++++++++ src/legacy/ui/public/agg_types/index.ts | 80 +---------- .../ui/public/agg_types/param_types/field.ts | 4 +- src/legacy/ui/public/agg_types/utils.ts | 2 +- .../public/vis/lib/least_common_interval.ts | 2 +- src/plugins/embeddable/public/bootstrap.ts | 16 +++ src/plugins/embeddable/public/index.ts | 2 + .../embeddable/public/lib/triggers/index.ts | 2 + .../expression_types/kibana_datatable.ts | 7 + .../screenshots/baseline/combined_test.png | Bin 22801 -> 16994 bytes .../baseline/final_screenshot_test.png | Bin 22852 -> 17033 bytes .../screenshots/baseline/metric_all_data.png | Bin 32900 -> 24240 bytes .../baseline/metric_invalid_data.png | Bin 1920 -> 1806 bytes .../baseline/metric_percentage_mode.png | Bin 32587 -> 23705 bytes .../baseline/metric_single_metric_data.png | Bin 29643 -> 22004 bytes .../screenshots/baseline/partial_test_1.png | Bin 15140 -> 11228 bytes .../screenshots/baseline/partial_test_2.png | Bin 22801 -> 16994 bytes .../screenshots/baseline/partial_test_3.png | Bin 7049 -> 7054 bytes .../baseline/tagcloud_all_data.png | Bin 15701 -> 11827 bytes .../baseline/tagcloud_fontsize.png | Bin 13808 -> 10314 bytes .../baseline/tagcloud_invalid_data.png | Bin 1920 -> 1806 bytes .../baseline/tagcloud_metric_data.png | Bin 9163 -> 6712 bytes .../screenshots/baseline/tagcloud_options.png | Bin 18998 -> 15100 bytes .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/partial_test_3.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/combined_test2.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_1.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/partial_test_3.json | 2 +- .../snapshots/session/step_output_test2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../snapshots/session/tagcloud_all_data.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- .../session/tagcloud_metric_data.json | 2 +- .../snapshots/session/tagcloud_options.json | 2 +- 75 files changed, 700 insertions(+), 281 deletions(-) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.js (81%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.test.js (71%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.test.mocks.ts (92%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public/filters/vis_filters.js => data/public/actions/filters/create_filters_from_event.js} (72%) create mode 100644 src/legacy/core_plugins/data/public/actions/select_range_action.ts create mode 100644 src/legacy/core_plugins/data/public/actions/value_click_action.ts create mode 100644 src/legacy/core_plugins/data/public/search/expressions/create_filter.js create mode 100644 src/legacy/core_plugins/data/public/search/expressions/utils.ts delete mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts create mode 100644 src/legacy/ui/public/agg_types/agg_types.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js similarity index 81% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.js index e0854205b132e..67711bd4599a2 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js @@ -19,9 +19,10 @@ import _ from 'lodash'; import moment from 'moment'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; -export function onBrushEvent(event) { +export async function onBrushEvent(event, getIndexPatterns) { const isNumber = event.data.ordered; const isDate = isNumber && event.data.ordered.date; @@ -29,9 +30,12 @@ export function onBrushEvent(event) { if (!xRaw) return []; const column = xRaw.table.columns[xRaw.column]; if (!column) return []; - const aggConfig = event.aggConfigs[xRaw.column]; - if (!aggConfig) return []; - const indexPattern = aggConfig.getIndexPattern(); + if (!column.meta) return []; + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); const field = aggConfig.params.field; if (!field) return []; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js similarity index 71% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js index 215d440edd9d0..a6fe58503cd02 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js @@ -20,21 +20,36 @@ import _ from 'lodash'; import moment from 'moment'; import expect from '@kbn/expect'; + +jest.mock('../../../../../ui/public/agg_types/agg_configs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + import { onBrushEvent } from './brush_event'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; + const aggConfigs = [ + { + params: {}, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }, + ]; + const baseEvent = { - aggConfigs: [ - { - params: {}, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }, - ], data: { fieldFormatter: _.constant({}), series: [ @@ -47,6 +62,11 @@ describe('brushEvent', () => { columns: [ { id: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, }, ], }, @@ -69,9 +89,11 @@ describe('brushEvent', () => { expect(onBrushEvent).to.be.a(Function); }); - test('ignores event when data.xAxisField not provided', () => { + test('ignores event when data.xAxisField not provided', async () => { const event = _.cloneDeep(baseEvent); - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => baseEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); @@ -84,22 +106,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('by ignoring the event when range spans zero time', () => { + test('by ignoring the event when range spans zero time', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by updating the timefilter', () => { + test('by updating the timefilter', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: async () => dateEvent.data.indexPattern, + })); expect(filters[0].range.time.gte).to.be(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. expect(filters[0].range.time.lt).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); @@ -114,17 +140,19 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('creates a new range filter', () => { + test('creates a new range filter', async () => { const event = _.cloneDeep(dateEvent); const rangeBegin = JAN_01_2014; const rangeEnd = rangeBegin + DAY_IN_MS; event.range = [rangeBegin, rangeEnd]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.anotherTimeField.gte).to.equal(moment(rangeBegin).toISOString()); expect(filters[0].range.anotherTimeField.lt).to.equal(moment(rangeEnd).toISOString()); @@ -142,22 +170,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = numberField; numberEvent = _.cloneDeep(baseEvent); - numberEvent.aggConfigs[0].params.field = numberField; numberEvent.data.ordered = { date: false }; }); - test('by ignoring the event when range does not span at least 2 values', () => { + test('by ignoring the event when range does not span at least 2 values', async () => { const event = _.cloneDeep(numberEvent); event.range = [1]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by creating a new filter', () => { + test('by creating a new filter', async () => { const event = _.cloneDeep(numberEvent); event.range = [1, 2, 3, 4]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.numberField.gte).to.equal(1); expect(filters[0].range.numberField.lt).to.equal(4); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts index f0de2f88dcb82..2cecfd0fe8b76 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../../../core/public/mocks'; +import { chromeServiceMock } from '../../../../../../core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js similarity index 72% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js index 303dec690e62b..1037c718d0003 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js @@ -17,8 +17,10 @@ * under the License. */ -import { onBrushEvent } from './brush_event'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -63,11 +65,16 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * @param {string} cellValue - value of the current cell * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() */ -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = async (table, columnIndex, rowIndex) => { + if (!table || !table.columns || !table.columns[columnIndex]) return; const column = table.columns[columnIndex]; - const aggConfig = aggConfigs[columnIndex]; + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,26 +92,28 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { return filter; }; -const createFiltersFromEvent = event => { +const createFiltersFromEvent = async event => { const filters = []; const dataPoints = event.data || [event]; - dataPoints - .filter(point => point) - .forEach(val => { - const { table, column, row, value } = val; - const filter = createFilter(event.aggConfigs, table, column, row, value); - if (filter) { - filter.forEach(f => { - if (event.negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }); + await Promise.all( + dataPoints + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter = await createFilter(table, column, row); + if (filter) { + filter.forEach(f => { + if (event.negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); return filters; }; -export { createFilter, createFiltersFromEvent, onBrushEvent }; +export { createFilter, createFiltersFromEvent }; diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts new file mode 100644 index 0000000000000..4ea5c78a9fd2b --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -0,0 +1,91 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// @ts-ignore +import { onBrushEvent } from './filters/brush_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../plugins/data/public/services'; + +export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function selectRangeAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: SELECT_RANGE_ACTION, + id: SELECT_RANGE_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + + const selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts new file mode 100644 index 0000000000000..2f622eb1eb669 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -0,0 +1,126 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getOverlays, getIndexPatterns } from '../../../../../plugins/data/public/services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { applyFiltersPopover } from '../../../../../plugins/data/public/ui/apply_filters'; +// @ts-ignore +import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; + +export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await createFiltersFromEvent(context.data)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function valueClickAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: VALUE_CLICK_ACTION, + id: VALUE_CLICK_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await createFiltersFromEvent(data)) || []; + + let selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (selectedFilters.length > 1) { + const indexPatterns = await Promise.all( + filters.map(filter => { + return getIndexPatterns().get(filter.meta.index!); + }) + ); + + const filterSelectionPromise: Promise = new Promise(resolve => { + const overlay = getOverlays().openModal( + toMountPoint( + applyFiltersPopover( + filters, + indexPatterns, + () => { + overlay.close(); + resolve([]); + }, + (filterSelection: esFilters.Filter[]) => { + overlay.close(); + resolve(filterSelection); + } + ) + ), + { + 'data-test-subj': 'selectFilterOverlay', + } + ); + }); + + selectedFilters = await filterSelectionPromise; + } + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index a6646ea338c93..d37c17c224072 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -39,8 +39,6 @@ import { plugin } from '.'; const dataPlugin = plugin(); -export const setup = dataPlugin.setup(npSetup.core); +export const setup = dataPlugin.setup(npSetup.core, npSetup.plugins); -export const start = dataPlugin.start(npStart.core, { - data: npStart.plugins.data, -}); +export const start = dataPlugin.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 6bd85ef020f16..da35366cdff31 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginStart, addSearchStrategy, defaultSearchStrategy, + DataPublicPluginSetup, } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; @@ -32,15 +33,27 @@ import { setInjectedMetadata, setFieldFormats, setSearchService, + setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; +import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/embeddable/public/lib/triggers'; +import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; export interface DataPluginSetupDependencies { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; + uiActions: IUiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; + uiActions: IUiActionsStart; } /** @@ -64,19 +77,30 @@ export interface DataStart {} // eslint-disable-line @typescript-eslint/no-empty export class DataPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); + + uiActions.registerAction( + selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) + ); + uiActions.registerAction( + valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) + ); } - public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { + public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setSearchService(data.search); + setOverlays(core.overlays); + + uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); + uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return {}; } diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 6e6d2a15fa2ac..8f7953c408a97 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,9 +19,9 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../../visualizations/public'; import { FormattedData } from '../../../../../../plugins/inspector/public'; - +// @ts-ignore +import { createFilter } from './create_filter'; interface Column { id: string; name: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js new file mode 100644 index 0000000000000..3f4028a9b5525 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js @@ -0,0 +1,66 @@ +/* + * 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. + */ + +const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { + const column = table.columns[columnIndex]; + const aggConfig = aggConfigs[columnIndex]; + let filter = []; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +export { createFilter }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 143283152d104..b4ea2cd378d61 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -46,6 +46,7 @@ import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; import { getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -289,6 +290,7 @@ export const esaggs = (): ExpressionFunction { + return { + type: aggConfig.type.name, + indexPatternId: aggConfig.getIndexPattern().id, + aggConfigParams: aggConfig.toJSON().params, + }; +}; + +interface DeserializeAggConfigParams { + type: string; + aggConfigParams: Record; + indexPattern: IndexPattern; +} + +export const deserializeAggConfig = ({ + type, + aggConfigParams, + indexPattern, +}: DeserializeAggConfigParams) => { + const aggConfigs = new AggConfigs(indexPattern); + const aggConfig = aggConfigs.createAggConfig({ + enabled: true, + type, + params: aggConfigParams, + }); + return aggConfig; +}; diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index e1c93ec0e3b1c..c975d5772e0a8 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -18,3 +18,4 @@ */ export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; +export { serializeAggConfig } from './expressions/utils'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 6f0a5a3784b07..e66dff01b6bf2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -34,8 +34,8 @@ jest.mock('@elastic/eui', () => ({ jest.mock('../../../legacy_imports', () => ({ getTableAggs: jest.fn(), })); -jest.mock('../../../../../visualizations/public', () => ({ - createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), +jest.mock('../../../../../data/public/actions/filters/create_filters_from_event', () => ({ + createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), })); const vis = { @@ -95,8 +95,8 @@ const uiState = { setSilent: jest.fn(), }; -const getWrapper = (props?: Partial) => - mount( +const getWrapper = async (props?: Partial) => { + const wrapper = mount( ) => ); + await (wrapper.find(VisLegend).instance() as VisLegend).refresh(); + wrapper.update(); + return wrapper; +}; + const getLegendItems = (wrapper: ReactWrapper) => wrapper.find('.visLegend__button'); describe('VisLegend Component', () => { @@ -120,9 +125,9 @@ describe('VisLegend Component', () => { }); describe('Legend open', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -131,9 +136,9 @@ describe('VisLegend Component', () => { }); describe('Legend closed', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -142,25 +147,26 @@ describe('VisLegend Component', () => { }); describe('Highlighting', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); - it('should call highlight handler when legend item is focused', () => { + it('should call highlight handler when legend item is focused', async () => { const first = getLegendItems(wrapper).first(); + first.simulate('focus'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call highlight handler when legend item is hovered', () => { + it('should call highlight handler when legend item is hovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is blurred', () => { + it('should call unHighlight handler when legend item is blurred', async () => { let first = getLegendItems(wrapper).first(); first.simulate('focus'); first = getLegendItems(wrapper).first(); @@ -169,7 +175,7 @@ describe('VisLegend Component', () => { expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is unhovered', () => { + it('should call unHighlight handler when legend item is unhovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); @@ -187,8 +193,8 @@ describe('VisLegend Component', () => { }, }; - expect(() => { - wrapper = getWrapper({ vis: newVis }); + expect(async () => { + wrapper = await getWrapper({ vis: newVis }); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -197,8 +203,8 @@ describe('VisLegend Component', () => { }); describe('Filtering', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should filter out when clicked', () => { @@ -223,8 +229,8 @@ describe('VisLegend Component', () => { }); describe('Toggles details', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should show details when clicked', () => { @@ -236,8 +242,8 @@ describe('VisLegend Component', () => { }); describe('setColor', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('sets the color in the UI state', () => { @@ -255,18 +261,18 @@ describe('VisLegend Component', () => { }); describe('toggleLegend function', () => { - it('click should show legend once toggled from hidden', () => { + it('click should show legend once toggled from hidden', async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); expect(wrapper.exists('.visLegend__list')).toBe(true); }); - it('click should hide legend once toggled from shown', () => { + it('click should hide legend once toggled from shown', async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 0eec557dd334e..a170af33583df 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -24,7 +24,8 @@ import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; // @ts-ignore -import { createFiltersFromEvent } from '../../../../../visualizations/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createFiltersFromEvent } from '../../../../../data/public/actions/filters/create_filters_from_event'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; @@ -94,11 +95,11 @@ export class VisLegend extends PureComponent { this.props.vis.API.events.filter({ data, negate }); }; - canFilter = (item: LegendItem): boolean => { + canFilter = async (item: LegendItem): Promise => { if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { return false; } - const filters = createFiltersFromEvent({ aggConfigs: this.state.tableAggs, data: item.values }); + const filters = await createFiltersFromEvent({ data: item.values }); return Boolean(filters.length); }; @@ -123,16 +124,39 @@ export class VisLegend extends PureComponent { }; // Most of these functions were moved directly from the old Legend class. Not a fan of this. - getLabels = (data: any, type: string) => { - if (!data) return []; - data = data.columns || data.rows || [data]; + setLabels = (data: any, type: string): Promise => + new Promise(async resolve => { + let labels = []; + if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + labels = map(legendLabels, label => { + return { label }; + }); + } + } else { + if (!data) return []; + data = data.columns || data.rows || [data]; - if (type === 'pie') return getPieNames(data); + labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); + } - return this.getSeriesLabels(data); - }; + const labelsConfig = await Promise.all( + labels.map(async label => ({ + ...label, + canFilter: await this.canFilter(label), + })) + ); + + this.setState( + { + labels: labelsConfig, + }, + resolve + ); + }); - refresh = () => { + refresh = async () => { const vislibVis = this.props.vislibVis; if (!vislibVis || !vislibVis.visConfig) { this.setState({ @@ -154,24 +178,12 @@ export class VisLegend extends PureComponent { this.setState({ open: this.props.vis.params.addLegend }); } - if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { - const legendLabels = this.props.vislibVis.getLegendLabels(); - if (legendLabels) { - this.setState({ - labels: map(legendLabels, label => { - return { label }; - }), - }); - } - } else { - this.setState({ labels: this.getLabels(this.props.visData, vislibVis.visConfigArgs.type) }); - } - if (vislibVis.visConfig) { this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); } this.setState({ tableAggs: getTableAggs(this.props.vis) }); + await this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); }; highlight = (event: BaseSyntheticEvent) => { @@ -219,7 +231,7 @@ export class VisLegend extends PureComponent { key={item.label} anchorPosition={anchorPosition} selected={this.state.selectedLabel === item.label} - canFilter={this.canFilter(item)} + canFilter={item.canFilter} onFilter={this.filter} onSelect={this.toggleDetails} legendId={this.legendId} diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2af468ff77de6..d3badcc6bdc3f 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,13 +17,12 @@ * under the License. */ -import _, { forEach } from 'lodash'; +import _ from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; @@ -34,7 +33,6 @@ import { Query, onlyDisabledFiltersChanged, esFilters, - mapAndFlattenFilters, ISearchSource, } from '../../../../../plugins/data/public'; import { @@ -42,7 +40,8 @@ import { EmbeddableOutput, Embeddable, Container, - APPLY_FILTER_TRIGGER, + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; @@ -105,7 +104,6 @@ export class VisualizeEmbeddable extends Embeddable { - if (event.disabled || !eventName) { - return; - } else { - this.actions[eventName] = event.defaultAction; - } - }); - // This is a hack to give maps visualizations access to data in the // globalState, since they can no longer access it via searchSource. // TODO: Remove this as a part of elastic/kibana#30593 @@ -301,18 +290,13 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.actions[event.name]) { - event.data.aggConfigs = getTableAggs(this.vis); - const filters: esFilters.Filter[] = this.actions[event.name](event.data) || []; - const mappedFilters = mapAndFlattenFilters(filters); - const timeFieldName = this.vis.indexPattern.timeFieldName; - - npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters: mappedFilters, - timeFieldName, - }); - } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; + + npStart.plugins.uiActions.executeTriggerActions(eventName, { + embeddable: this, + timeFieldName: this.vis.indexPattern.timeFieldName, + data: event.data, + }); }) ); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts deleted file mode 100644 index 4558621dc6615..0000000000000 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts +++ /dev/null @@ -1,21 +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. - */ - -// @ts-ignore -export * from './vis_filters'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 4dffcb8ce995e..3c4a1c1449d47 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -44,7 +44,6 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { Vis, VisParams, VisState } from './vis'; -export * from './filters'; export { TypesService } from './types/types_service'; export { Status } from './legacy/update_status'; @@ -53,6 +52,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -// @ts-ignore -export { createFiltersFromEvent } from './filters/vis_filters'; export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index f62b3a0b393ac..351acc48e2676 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -19,7 +19,6 @@ import _ from 'lodash'; -import { createFiltersFromEvent, onBrushEvent } from '../filters'; import { DefaultEditorController } from '../../../../../vis_default_editor/public'; export class BaseVisType { @@ -60,15 +59,6 @@ export class BaseVisType { showIndexSelection: true, hierarchicalData: false, // we should get rid of this i guess ? }, - events: { - filterBucket: { - defaultAction: createFiltersFromEvent, - }, - brush: { - defaultAction: onBrushEvent, - disabled: true, - }, - }, stage: 'production', feedbackMessage: '', hidden: false, diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index 3f88c540be164..17a8b14b57d02 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -63,7 +63,7 @@ const unknownSchema: Schema = { const getTypeFromRegistry = (type: string): AggType => { // We need to inline require here, since we're having a cyclic dependency // from somewhere inside agg_types back to AggConfig. - const aggTypes = require('../agg_types').aggTypes; + const aggTypes = require('./agg_types').aggTypes; const registeredType = aggTypes.metrics.find((agg: AggType) => agg.name === type) || aggTypes.buckets.find((agg: AggType) => agg.name === type); diff --git a/src/legacy/ui/public/agg_types/agg_types.ts b/src/legacy/ui/public/agg_types/agg_types.ts new file mode 100644 index 0000000000000..1b05f5926ebfc --- /dev/null +++ b/src/legacy/ui/public/agg_types/agg_types.ts @@ -0,0 +1,92 @@ +/* + * 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 { countMetricAgg } from './metrics/count'; +import { avgMetricAgg } from './metrics/avg'; +import { sumMetricAgg } from './metrics/sum'; +import { medianMetricAgg } from './metrics/median'; +import { minMetricAgg } from './metrics/min'; +import { maxMetricAgg } from './metrics/max'; +import { topHitMetricAgg } from './metrics/top_hit'; +import { stdDeviationMetricAgg } from './metrics/std_deviation'; +import { cardinalityMetricAgg } from './metrics/cardinality'; +import { percentilesMetricAgg } from './metrics/percentiles'; +import { geoBoundsMetricAgg } from './metrics/geo_bounds'; +import { geoCentroidMetricAgg } from './metrics/geo_centroid'; +import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; +import { derivativeMetricAgg } from './metrics/derivative'; +import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; +import { movingAvgMetricAgg } from './metrics/moving_avg'; +import { serialDiffMetricAgg } from './metrics/serial_diff'; +import { dateHistogramBucketAgg } from './buckets/date_histogram'; +import { histogramBucketAgg } from './buckets/histogram'; +import { rangeBucketAgg } from './buckets/range'; +import { dateRangeBucketAgg } from './buckets/date_range'; +import { ipRangeBucketAgg } from './buckets/ip_range'; +import { termsBucketAgg } from './buckets/terms'; +import { filterBucketAgg } from './buckets/filter'; +import { filtersBucketAgg } from './buckets/filters'; +import { significantTermsBucketAgg } from './buckets/significant_terms'; +import { geoHashBucketAgg } from './buckets/geo_hash'; +import { geoTileBucketAgg } from './buckets/geo_tile'; +import { bucketSumMetricAgg } from './metrics/bucket_sum'; +import { bucketAvgMetricAgg } from './metrics/bucket_avg'; +import { bucketMinMetricAgg } from './metrics/bucket_min'; +import { bucketMaxMetricAgg } from './metrics/bucket_max'; + +export { AggType } from './agg_type'; + +export const aggTypes = { + metrics: [ + countMetricAgg, + avgMetricAgg, + sumMetricAgg, + medianMetricAgg, + minMetricAgg, + maxMetricAgg, + stdDeviationMetricAgg, + cardinalityMetricAgg, + percentilesMetricAgg, + percentileRanksMetricAgg, + topHitMetricAgg, + derivativeMetricAgg, + cumulativeSumMetricAgg, + movingAvgMetricAgg, + serialDiffMetricAgg, + bucketAvgMetricAgg, + bucketSumMetricAgg, + bucketMinMetricAgg, + bucketMaxMetricAgg, + geoBoundsMetricAgg, + geoCentroidMetricAgg, + ], + buckets: [ + dateHistogramBucketAgg, + histogramBucketAgg, + rangeBucketAgg, + dateRangeBucketAgg, + ipRangeBucketAgg, + termsBucketAgg, + filterBucketAgg, + filtersBucketAgg, + significantTermsBucketAgg, + geoHashBucketAgg, + geoTileBucketAgg, + ], +}; diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ca7c2f82023c9..cf2733b9a9f36 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -17,80 +17,7 @@ * under the License. */ -import { countMetricAgg } from './metrics/count'; -import { avgMetricAgg } from './metrics/avg'; -import { sumMetricAgg } from './metrics/sum'; -import { medianMetricAgg } from './metrics/median'; -import { minMetricAgg } from './metrics/min'; -import { maxMetricAgg } from './metrics/max'; -import { topHitMetricAgg } from './metrics/top_hit'; -import { stdDeviationMetricAgg } from './metrics/std_deviation'; -import { cardinalityMetricAgg } from './metrics/cardinality'; -import { percentilesMetricAgg } from './metrics/percentiles'; -import { geoBoundsMetricAgg } from './metrics/geo_bounds'; -import { geoCentroidMetricAgg } from './metrics/geo_centroid'; -import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; -import { derivativeMetricAgg } from './metrics/derivative'; -import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; -import { movingAvgMetricAgg } from './metrics/moving_avg'; -import { serialDiffMetricAgg } from './metrics/serial_diff'; -import { dateHistogramBucketAgg, setBounds } from './buckets/date_histogram'; -import { histogramBucketAgg } from './buckets/histogram'; -import { rangeBucketAgg } from './buckets/range'; -import { dateRangeBucketAgg } from './buckets/date_range'; -import { ipRangeBucketAgg } from './buckets/ip_range'; -import { termsBucketAgg, termsAggFilter } from './buckets/terms'; -import { filterBucketAgg } from './buckets/filter'; -import { filtersBucketAgg } from './buckets/filters'; -import { significantTermsBucketAgg } from './buckets/significant_terms'; -import { geoHashBucketAgg } from './buckets/geo_hash'; -import { geoTileBucketAgg } from './buckets/geo_tile'; -import { bucketSumMetricAgg } from './metrics/bucket_sum'; -import { bucketAvgMetricAgg } from './metrics/bucket_avg'; -import { bucketMinMetricAgg } from './metrics/bucket_min'; -import { bucketMaxMetricAgg } from './metrics/bucket_max'; - -export { AggType } from './agg_type'; - -export const aggTypes = { - metrics: [ - countMetricAgg, - avgMetricAgg, - sumMetricAgg, - medianMetricAgg, - minMetricAgg, - maxMetricAgg, - stdDeviationMetricAgg, - cardinalityMetricAgg, - percentilesMetricAgg, - percentileRanksMetricAgg, - topHitMetricAgg, - derivativeMetricAgg, - cumulativeSumMetricAgg, - movingAvgMetricAgg, - serialDiffMetricAgg, - bucketAvgMetricAgg, - bucketSumMetricAgg, - bucketMinMetricAgg, - bucketMaxMetricAgg, - geoBoundsMetricAgg, - geoCentroidMetricAgg, - ], - buckets: [ - dateHistogramBucketAgg, - histogramBucketAgg, - rangeBucketAgg, - dateRangeBucketAgg, - ipRangeBucketAgg, - termsBucketAgg, - filterBucketAgg, - filtersBucketAgg, - significantTermsBucketAgg, - geoHashBucketAgg, - geoTileBucketAgg, - ], -}; - +export { aggTypes } from './agg_types'; export { AggParam } from './agg_params'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; @@ -99,5 +26,6 @@ export { FieldParamType } from './param_types'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; - -export { setBounds, termsAggFilter }; +export { AggType } from './agg_type'; +export { setBounds } from './buckets/date_histogram'; +export { termsAggFilter } from './buckets/terms'; diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts index 4ce5bb29f8ff6..d01e059c6c616 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.ts +++ b/src/legacy/ui/public/agg_types/param_types/field.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { AggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { toastNotifications } from '../../notify'; +import { npStart } from '../../new_platform'; import { propFilter } from '../filter'; import { Field, IFieldList } from '../../../../../plugins/data/public'; import { isNestedField } from '../../../../../plugins/data/public'; @@ -89,7 +89,7 @@ export class FieldParamType extends BaseParamType { (f: any) => f.name === fieldName ); if (!validField) { - toastNotifications.addDanger( + npStart.core.notifications.toasts.addDanger( i18n.translate( 'common.ui.aggTypes.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts index fd405d49625ed..e382f821b31a9 100644 --- a/src/legacy/ui/public/agg_types/utils.ts +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isValidEsInterval } from '../../../core_plugins/data/public'; +import { isValidEsInterval } from '../../../core_plugins/data/common/parse_es_interval/is_valid_es_interval'; import { leastCommonInterval } from '../vis/lib/least_common_interval'; /** diff --git a/src/legacy/ui/public/vis/lib/least_common_interval.ts b/src/legacy/ui/public/vis/lib/least_common_interval.ts index 244bc1d0111e3..72426855f70af 100644 --- a/src/legacy/ui/public/vis/lib/least_common_interval.ts +++ b/src/legacy/ui/public/vis/lib/least_common_interval.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { leastCommonMultiple } from './least_common_multiple'; -import { parseEsInterval } from '../../../../core_plugins/data/public'; +import { parseEsInterval } from '../../../../core_plugins/data/common/parse_es_interval/parse_es_interval'; /** * Finds the lowest common interval between two given ES date histogram intervals diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 801329a4a79af..1e0e7dfdb0933 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -23,6 +23,8 @@ import { APPLY_FILTER_TRIGGER, createFilterAction, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, } from './lib'; /** @@ -50,11 +52,25 @@ export const bootstrap = (uiActions: IUiActionsSetup) => { description: 'Actions appear in title bar when an embeddable loads in a panel', actionIds: [], }; + const selectRangeTrigger = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', + actionIds: [], + }; + const valueClickTrigger = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', + actionIds: [], + }; const actionApplyFilter = createFilterAction(); uiActions.registerTrigger(triggerContext); uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); uiActions.registerTrigger(triggerBadge); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 583b21ddfa433..ec71a1e724c7d 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -25,6 +25,8 @@ export { APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, Adapters, AddPanelAction, CONTEXT_MENU_TRIGGER, diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index ffa7f6d0c0f44..72565b3f527ad 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -19,4 +19,6 @@ export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; diff --git a/src/plugins/expressions/common/expression_types/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/kibana_datatable.ts index c360a2be8c7f7..38227d2ed6207 100644 --- a/src/plugins/expressions/common/expression_types/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/kibana_datatable.ts @@ -23,9 +23,16 @@ import { Datatable, PointSeries } from '.'; const name = 'kibana_datatable'; +export interface KibanaDatatableColumnMeta { + type: string; + indexPatternId?: string; + aggConfigParams?: Record; +} + export interface KibanaDatatableColumn { id: string; name: string; + meta?: KibanaDatatableColumnMeta; formatHint?: SerializedFieldFormat; } diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 87f3173d5602472d092da015c8e58f354f0f9bb3..56c055b8b1cff85d7dd891750efdcf6aa198e937 100644 GIT binary patch literal 16994 zcmeIZbySpJ`!8&Q5+d!;jfmvXsdOVPEj37YmkJ^sLk+EhAX3r{DP03dN+T`OL-*P9 zeV*Sr>$l$Xo^}3t*LweWW-VvNnfso5?`vQCx;}9e{z6S2ABO_x#*G{JiV8BCH*VaC zxN+m=(|cIplOp? z`gZfxnW$cg2{R|>11>Hu^m0!J?lp%(51jd{b{Q2XC#P7r<6IPMt>j_T)#^i5wXDUN z$$g_{m)nAZf?2*#h<(t@t1F+mG&;M|&AG^1Cby;gr5eb?&FUg?gpd`(*50>KV&{IAl+sb>LVPteHlNA>9^=9)t$;hejI; zIq$N!wlUUbf0o5rT3S-A$ioMtg4%Rs#a-G@HDvz0ZT8OC_qhjy@Qy2(uGe?R;)cbn0wrGzJ2d#_uybTv*!HwnqhR;WTH)le#5fX zW)fpp(4(`tY44P$&u!~{plMgPKYS0me!iK+rX`2fJVKzH6d@F>nqITN9jC#hd+bR4 zdb;kedPGi_+Rwt{>5vcduLuYTygH+3x#-E%K|j{~s%w#rLb1AMtO-_^vvZ+G=l@0y!U z=V=3zfbJg!Mnkl^7#S8HJBEejmPe9vEvQK7SHr^-U~$@lPIJqRpLX`=ejXRRXu>1z zid}SZxf;afv!{Bdg5rSx9hBtySuAhrSfO3ejTn+vw8;;;j22A8CL$-7*EDXV=y>y1 z8d;od>Jf?$QLr^Q7-H>i-kSD$V-kP`*B%<;xpN5Vc6cscKU^+LtT~*-u2(Ssm07P; zE?&E=tSky57|yfAvUg@z%4^ptqiGoV<^h#xAUt=f%#GknpDyFRmU@{@o6^k8Ox%Z! z*< zUdhPICr}lMdM<5Qv^1B{?mX~~Ongxc3*aFuNQ9cv-a3nrVErU2})*jV}h8szDijg;*^ zn_e`?(>P6YbH@cRmZ_?ZT+)7nYU{vVbIr6bb&~RdC->#kg=|DJ>knC&FZa6i!DVw3 zcbcx>?lg(MyRaHPUxIKu-($1dgRA@G_=r+D^y#vDYzVS48#f2ucA~}Q;o)J*udNjV z=NZ*|MkmI_#XUKLTkUF$!WxPf!1LnFQh!|KovpTr-}p z7gOOcN4hVWVjgK{(FTi;?V2aLKYGc-4SIDBn4dg}Sbj)L8>^1|n~T*v6)o(wEp}_? z0Yuh7AQLPB)Pd)fe0aui6z;AEwb=m4jHzylC11_ro42!-ZP0?Df=u-8GNXpa2hp>@4H z!_Ro@R4gZy6L9`u_wd&TXR^R5$^;U<5+kbv1c3`#Qx(O+>&yATSY3yDx}ejo8cBqN z1x#=IwfzR%fwnCa#%G4Rp*UQJe;UHzOt!r%dJ$n;NW5U9ixNZ~1 zlQ^#rTRqXY9mzA+(=@cVr^3u~fPn$cU>f9h&w8YA@EN?78_!=UCD%nd=?U$a2R@&tUw%&kX*IwkA(3Uq z*!lY&)cUU5)|p4fbANxaB^H$h#Y)Td{PvEHr6RRV5mn3}y}BQRML7i(VeysqVAfK~ zNVChqgo}XvL~D;$z6*=TMY?$@dd1(nKIoya+v@M^665-T%5}Gwr;`SoLr6QPR;gm4 z$WvT0i_)u0-{D-B02(Gv&KMS_%9Wb=>mNmp=MP@RGShN(nV6Vlq0*sAIP7aDnhOl39 zZLK$avESq}RmA5h?BZlZqr@b-l_u1T+iDm)ky8hLF6@8eC*rjsZEyb+R&3au`uz6! z!s=>7U5p|kA_@y@2-BC1Ac1M@;?XckhaTYlp>2P2 z^&FK(jk>t2%)PPeCI8r@=<&d-@y)x4Bu-iG*gJF>@9LtNLu37 zd+vNOz4qcM^qkGJpR2|38{4%DYs^!liBw;Qi*=((v!RZy+M9%<)^rIhF{_FiZKd=B zxyB#1XKG!Dt%=BNnVCV^;Ld4h?|4Y)!UR&8wR<<-f~U|e-}5O>)^l@@b*k<<{yy#v zus(`?V8T%lEB}U60jsC5{?z)1&}zr&aB=XlN_>h5Q<=z}YV*mi7DN^3JBu%?F6V#H z_<1_M8bB#N3T+9tM8&<;VV5wU96p_}7+y|g8h?+wFkvw}rH*Md?cV@E60I)oDC&+R z508myp^1+tNBW=Q!1x{isK5M5nmx?ygUC~*cnC&qJwS!1@|wSyMS|Vdw7S@#9JcRW zp%;YuJMjrXl^9Dp2J{{Mnm{Ow#1DCo`j(-Y?m&j{y(~d|Xo+d_Tj)g1H66@h>N}Ai zCckEoSuIf^A$L$<-zcEU9gUUi+}F7afA62BUMyfE_+5tG+j%~h7%!b2QX)vnY&%9q z655}<{CfZt;>odFdj_g`KX{^f>^;Y~sk6DlVA-2U z2<0@cQ?V8G-epJiCiAl&8655T!?lcz#9)>7Ly0P>yym6lR!Z^5lVXUyo^qWq3qHFc znhLOgCm8*ziz@BsLY6U20QFcim-{_#^P5K3Rn%Ljep`gg!5rAD;4@Q&zZnV;2je;Aa1Bd;>%l1|l1Bk0AC(lwL%$ie`U0 z^e$AusSG|fkR=)m!TOII|cvO5;6%yV-_Y{Z9eTY#fD=DTv8JP1xQw?wT zJxnqOGa^wm0pa6-x$lhMB#d8vMF(&Xm~d{$C1|p|LGPw&UMl6qpNVk+AEQbQXO}R$|h5dvVHbaT(e# z)>|^`$J5r=M=Z~FdRBD$k3vAx+N#K^^^p6t)} zq3e0qKqGgd!v-1X4A_F8%qXs+)XN<9Z8dxMh*^1ZUdOlQR)RVeKjpd>hy@iB6LZ$3 zr~iZ)%JE~R`r@=h6XhQy7G5REhbRAcN;vL^=gotSYfklu`?P9ZQ3R|Fv5Zdj>FlvYMCG(l5s%lwId~Kku~^Z!6^iVBl_tDQ;WBM0(lhaa$e-AEnBS3WlB9vc z1}`33YyW8;+~0%yT=*Xd#RfYO1bl#q`?<@>Fd+exf#ZN<;(jSLNGa8UjW@t~G|r)! zu5RN*#&w$5F5qxj9|x=5jj9%v39r)@~NMDk^x}+uLHj*cPm2a=!-N3MtJs_;?R@ zw6}kUZDA3y8;wXXpxgjL~qgBrEG7#gO4+XYb(n&pI8bW<-8ItIc!Fp5B<)BPKa>dcHYS z`$_Wl8TMYThzpt=<^Ac!^NdMQj1ckczo5B!gSw?T`XixYz13TyJd7am7lo3ZdT?8e zgz5-I5|xszwcX!IpqKWg5uB$zC4~AO{{`c5b(NNBZ%GrhQwcuv=myM6*5pZ4dOAB? z=sgZ8Q?an$k(grH^3n=o&-0p&MlZeQCELOLJ2BMmr=2)vWti*g1;HSBckBhTcMB2f z&vZTZz*7;Cv`Wi9GWPy70hx?n5lMvro-&vD9&Sq6J`(y!0D;2jHb(R@I~zkR2Mns# zqx3n_1i@+w|I_-vG?+?XmD?SgpdRa0^9}q4JZ<+1n^>_Z>-lX2uMOT2Ld=Q`Hd>++ zTuH~mf>q(Ujq^#qCPpkO+lha99+dXCkkZB@oLE=xO*^u{10FLoQ#VO)DeVHIw?C>m zeD*q|gy0~!08lZP0dCoyB~DqeQoj@QTjTTR&)iE@zf2Z10O#nKIi)Rg9isq!eK9c0 zhlSON&*9m#$UL#0&X{Z|b7URGoyylrw!8bqoe2y8mu3e_ghB$&4*;}9f{8Hnz zAXDO!(mWefcq)2cGy{i$2NS@I@`?%yKk6(5$EoX)TkpxQ`ab_otwkc?+CHvpi-6+A zy%hC)|1(!ZoND1#Y0-2DO3NctqNV1I#r4K#>PG>uRTL~7wqC9c-(ZkV|X#`M;KXK>nzECNug1p%Lq7mS=Ufb z7n@d-2tWn&1rcN%Q4qIP&UTH^SPEs=31UzbzUdXB8Zq4&AY$QtGPBa-(3nm@m-d!e zeTb+;4#dehLlRz&73#F~_2DX-?CtGQNxBe0&yF|3!xWUbF~ij;eU6KIJ@b>+8oq;U z0PThZIv!X?2;k`m48;u4s9&bgDK0(*_KRvMRe;5KEatN~5IUcv^idN?_B--`IX}ud4#3s=5ZKw@}Qm*vH$Mby$0~8$Xe(um(xAG1DnOrqiYImBfMpJ7bt+$@8Tf zD2ktw_pFIWD{vPU7VPKDJECN116n`FO0;Nr>i@EMgoS|?6#>R_FRWjYApccL=l*%xPJZh@h*T(c}A1N2%W{TMFi=;a1;yM*#V`DD<*+V z0KJB{3C>gHNv#-cP9ooSY2f8{3$eoWig*QF^j@lfBY<5k8&?=|_DI-@q z^_!SuDU0~E?$yO$yyUeTNKUkzASsUA`~WQk zi0u0d_EYo<^A|jC_OPFghAv=lvAJHbio%kLUl9BG?{AsSQS;&`>Nj5iS5Ru5)aivV>$0&E#p-sS zWhs%ndvGLx);2TUwDJg1Yn-FqqcTh=i0n`kitc}jLUJ42)&?x(KqD$J$f02?sJ^B| z@S9K2ZE7HGshh6O9)?%Bjv3gFIVfp%bagE$bj4;b58^K5ChqRugZ1nGSb8-R!iLzp z>h_9H9Qvxrj{0U=Du5XtJ|UrEIWcR{r+_X&p1xUe>e+fvIu4K(zsHLWbC1!Bozdx_ zKBlk|5|XnWm{PHLl~{jI0eS?6tH`!MyHlaq#}!AB(NT~1A1I2)lXIELf#k^zJ<6Q3 zyvitGN{)=goh-LvW?&$y0Q@KV!w1oQnEEH@1@hW#ar*Q15vPYS=8v#qM#1J_a{&5- zh?jjNILAf@Fr_}dB9^j-2C)K@VjTDdrV6K4tZ^?jwp@Z?+W_550Zt_u2w%NP{awDh zL^YQbn;=m3ILj$MqqVh(<>{uKBm0Fpy@Y`fF6hE=+pRU)rTLF{kATvYtJVk$+plw3 z!iqtUYNehyUp$T=4@&hjZt{OG;e@}qyTCZH1fFIZAkq2x*x^-<{rQP(rqq5i@?Af_ zIgr!Ht5W>R4{FYu8CSXBIS?zyKW!6UCH`!!`-x1l2y0jd)AlYA2S^0gn;AddGD>#8 zs+kAACLV8qv|67pWhEH?XyO0>CfEn}{D9_y(fJH2!McI@#J%J&hzh`8d*Tji=CQXFFGYM^@?Y z_!sd3*umFhgoFoG_*iYcx1tdAF#A$YXHWY!PK$v=RWadHAU`Hv=tT#}9PDuv!%r5#z# z&FKn=U(tm-A*M);lWz!^jdO1JYW6mXr@IJTo;4^|eM{nDmyfyrzPa=Yg)>R1WB^h| z7-kQM);~>vn1<#pgfSFfN`|QlBb?{qQSyWW;mabHK^6yk6ucl*KlrO?ZAo3%JoaF{ z@ez9a##5aquBAZDN|kV<>60*iFRqkQECL%!- zXI~4G-5vZfDJv_x2eKKl^4Ipe^%@qY-xO%ZpYI(Ud;|*yvdO1#CmqDRs*dS>eZ45q z(r0Ez7G48H1-^6f^6H|Yq1g`e85B#n?YA5tK%mcS%~Ak3%BxNwa(@5Sayi`t5LS;_ ziibEjhS<8oY}OTSi~d0SYJ8PLcQ|6)q5f58MruM_q6^Tz7`RN@qmJd9BEuAHc|kvy zvAK|=Y&ttL+}xXS8!QJMYWUqZ{=XKwpROg;|I9gf`})FPs;2Y38XUk!kdk^tZ;cx> zje#KBjMNFoXwEm2VIMoKWrJ42lM|)R*50s&yWB;iOg`8`2)dYx(p0VE-L?-+!8r{_ zQ|w?uirI6xepbDS@yov#7Ty*Xvd6?>SZbNY8qM%K8u9toC}agIBU@MVb2rpCP^7?~ z!Bl*jf;y+$YS~x#eqz`Dfw~;mVU@oD9-x9v@qj&QqQQ8#v2KA;nubWqXTAu?OVY$mdwVTCf-5#?X+c4 z>_@JIDD*m(9>?K{0~$ zUZ2h1my4l7@eR3iJd6K|MSS>xJv20gSkS8wiO~d<7X=Dx`=n0k_>+5fGqpr#PY23s{>U|Cdy5#wv+>Al*l_w!I z>a-PUzDW7dRy)kUhd4TxbYE^wAh9d{5zCd5r^FOe2DV<$mypI*u(?q9~zM zgoM_B@H1Q!0XZ7fDX^tChstHOIs>^gDlMmvG7*P!zpsRpN{obg7ykid}}gF%E%lYumCO@pv1NNoc?l((NAJel9y?R01dm_*m15w5lE=t_10n>?*E;9 zUkPjLEJhrHKIfch&@|?gafj>WJ4h3MA^lNbG6y>>L)h(syolTCQ<&JHHcQ6XC+T?$ zxfo2*2Biz04Aeme^H}8+PK;uULU20*>M_s?()@~5Oza&_lAT$p#PiudKb!b%h6d$h z6VlLP>68^$LT+T%{BEAyu!D_}+hwNd@^9;V zXU!2%RJb=Tkdi$KqT~b^FDMG^97T`Kll!RNj8nGu!2bEy?d=5l`$Cvv3aV~WZbVGX z0zfqg4e;|s?0!e~z^aRTsg*YX!>Aywn?8};IRGt(AxH@!J<*hcJQnEoA%J>`Zxa%l z&0huc5&U`6C>Q-m<8Cu!v?eLfkU8klryx@Ch-I6m~vn&qP`jzVllwR?0yne^d@se zhlN3T9uoSr78_M1n>5aMnhpo#Ec`Xp0lBo|q06ti+l0*R>S_5wPI9l>KU z<&Ap0v=pt6p+p4|1`T)>Xz>(sAln(;1LTJSiqT@j!YVMMd8eAemkGQ*vr@ zT!2dlj5V{)gMuYOLKitK1_t$yx(*^0euEO5Rm(ZTBwV_QUEu)a9^Dh)Wwz*4Yc=`O ztKC84mUh!2NqjL(|fRDbvaxL7}maBJ}}bSnfT zOg}TZou(abuQ`T)q+?j3|D_JPbF(sukC;+cVO4yIS>gPN;a`y6Kbz9Bn zE(E*^m=`hab*?K<014b&STn>Ygi`Q1zSqa-E})(O=>i>6pj8T48@yK3yi>U?LfqdTn`AvCL%_ zr&srDxc(tUlJ*JbTSZI4^r+H@)gcz))F5nzu&uvfmm%-J#FUJau6EIPT1xy5t$#O6&`WWjt zKPp!6I3hfp9;jfeTrZ_G?^NUehysK-0c!nVAp4rw?-I;1@cI%jW^v7s8fXOVdq5(* z14M}K-6=O{(erqL4VA2NimrI$ODGsI4D75V-DJlixY&?MZ5nVr}GS1+3XeplXS z8)?n%D&=9EaYjJ?1B%eXoeTwGk_wiqZ<1fzk1BCEL1z`#XB=o25-9SKZD zCSXAl#*>GKH7J120lsowu_XzQ7dO-efF!H}XSN>SuNWZu0Xp{`0VW^)Z`!%-g&i(r z)73>RYye?}K_`#_)G?L7Y8v>gTDS^W{%t~NntKI=fi9<8*PHej+srSgY^Rv9-1epErz$JduT;IFsl$Z&^SPaM5? z%<}4LCkB{TB|X;2pqUkAFz@WqYQ|X3ko`3^S()}pzyKA}JXd;kh;kGb$hj>*0Nr2D zQ&^V)@Q=}%HSbpA{_fw-RKPbB3{V3OOnhPmFk2$X*i>vMt&C#<5si7#d(ID2%YJ72 zvho!R7s(R^SMSx-4Exyw;es#j1;0|EZbS^Qe^jLia`p&NwK0?9+d3$1j(^0gMAB6> zKs{X!q&rQmZKFY8Lv_r~Qp?-SeuIqUMKLpP5lkv2KTVBKtl{>?a+t29V@4v8gutu< zH=$4!EN?KsY_z@4X4BU>_!vDL$&?S08d!Mjp=NRLvpXIZQ;9BN2#}@m+b{lMpPA@Fr((9Vc_h;)#!5u{K1g{<< z43}_olhsr|E}@>NJ9X2PO6WYCq%D8H3};n$1X~sK&(!5aR&`GBUhLAHO!FOJzSu_& zZ2x$;^E;y?^;PApE|! zVvdaFto4!pgn$6$h7_kutSuLqdgu7i#Q+yk3Ik+E0RVadl*NLrYpjI@LZK-jmV-Nu z@Cp4cflZ7F)Kbp-iid=X>@)wY)N+rhf+93ueYqOkGsSCraOZQ5g>wkJw7K`s@rO8O zH<+dYxSt3_uw0TaO5zfW5R}(OoaI6J-G%~76oSx2?Qml(FR*b6<3hjlC164?%ECW8 zQkw#_kv+N#TqO`(Vb6;@#&w4ay-9@=>>DHM#9~1dKr?2j0+h7VgS~d^l7Iz$`WI}0 zP&^0%IHg?V0r%r`ZD*@-VU5}+cV%RTaZ>J30)xK)iQy-f8pqj1GXUq!LD}qC0Y6$~ zR28}<>gfjjIhv0C{%D+)fUiJ}>rLuiih4kMm8A}7utygS2#Ze~@c$MKD=4;Z-|hzH zAY8z>!8;s$-vP)uA*TNUV=EEs&{nG^hHKyti0;d?JsbgtIXQ426HR+tTc)iJ`$&nZ zGJ2&?+2A{9vB@0QQC!Uzp#WB6EO5M0bfMb5AVs{O6AchPnD&f_>@k7v1443+``@3T zG1OB4jDrW78>A|F!frb6?mz!$h&2aa#5TIG4+El)Ip8rSmtg^Ze?)1R)w7HM{{RoB zP{5>f^)WY_1|GKYO@hPSVCI~oA_2wk98&dr9X?#0||MJ<)U37@4fV%XrLwVXT0$ze{ z@yWe^eIO5x`uC&E1#r5I-umLpNq4@8=2|GM!{DjUEYgtV=Gf)w>64%y?Ry*Jla`An z8h5mh*VYOzn6>DO46PIHch#RQVO>l`v!*rk``A6Xq$HJHR^01KYZ|4!#J_vXx}rgg zew!$O!c4zR?dlmcy^0sxf74ya3tP&UQz;-1)bwfUc8)^8B`g_Zhh7A{rqzBsjI3 zfqiYUx3>}{PcLAgM?~^6Wmegm$EEWLa2f|B_5J;Ymo&nn$CK{d9k)6j)>p|Wi%_FZ zTT(Df5FK$}7AbEqN&>puP}LV&nqM&$?h6bOQ!c4L`EEBNRVTpN2e%nXJ=&8HsVe9TerbD`Fj#ea*!`RF zBYB*8i3p{weB$il$6EQSpPlmMmQ}poT1BRCA1`sOkLHmTSnVhTQQ%*|)wb*y*z7K& zkp2l@$o9Jqa!qCpMhguieLd_J*0jPlQ;m>qi%WfwSSkC;5~j!BIqOpf%on3{Kx$`T z##4PWr6S34mOS$Q-{H`MW=HE~?2$d2vCs@hA(fLO4Q*S=%oBUrR1pWF08xGh>pI-0 zh7BP|+91Vzfhq-#Axdn`yfJSPk9)w#;sBNk;hM9@ZAtmIPGjYsgz)QMmcQ-)S_OZc z>q9P$!Uv4*wpI_Y)KmUWPGUAJen#?ESM8hxgI&4W;O*mEDH)w+ zoLir!S-W;GUooez;(zwODzY3*iY*DwC#tsL8)DUk9u+>4q4BbyJeR&YBXM3a9Y1iy zTV}AF$9#BGFeBi&$|Kf$ykrvGR}Xbv^}dIQU_@>5-Wg7LC9_g=p`1L-*K?D7E?w&S z@UoWiYZ;ecQ0ziXa!xXlHb1%7ourCgt1+_>ywV@nMze3c4);KfM{eydCs&$Y;q_r# ze_8*&H8W(_VsOi(x7z_g{VSrV8g*`Dy1JEW>E|#ZgM-+Xq7&+(PvSPFj{L_IHIkv` z;+<|aMx%YVl;yFBEBWH;jl_S{Pv;<-?F8&L^qngk8AWeH^oI-YTqh^-@bmJ%x_WWX z=m)i(oEL04&0*67M$Lt&NoZY~U8}zuh z9?v_Hd7V5k`4J{^THF7tOX^NdKyXd$T@xH8`{y^{yfK~6C-oxH-2a=P>C!phG<$oY z>?wG%;)@qk?qz9B$EfU^^n1RXH&0Fu3q_0b7Pr=-|Ia{_kz14~&gh?3^nxmK>7Te{ z#eZ+CIAsL@)i_1}K8j!y^8VNm`=0*8XQ!SOwQXLzx8z29x*RUq^(P4XulJA2-q_u5 zJyLrn)1Hd)a|h_PBpQ@gmP`_wZ|&6Fs%3ooz<)z~$)w3c3Ce3p;1$yG)+y6=rmsfL zMlN)-MI-s@)?5JA9^K_HgZ07rG~IIN2!DG#{byk8K1(FGu&S7P12}@&E!z`oEh-JP z(}0Xo6K%iVx9oW}3JBMIpri9vq`rT3{H#?1=NS`H+o2}piL@>}{5+hV;;SAj`7ydQ zD~UgQ>*lS~{Jf^yJ;v629pbMzVg!RMNN9Ah_vn%r?Cw(zTon0ntN-kj3fanh zlUwxq0Fnq$?v=-Mhavzg-KK5`Gb`cj6hQE=_$nt#Yf?V zf8Nk!PrlfmS&c8`Gqdv}3V4jrLe#iy*z@kCu;e5!-hBF!I6Sv}!eU0i?YGKbkIkOn zHMe?B)(HRVl|S@Mswe5*7-$qjuP!|Y6cW(h$Md|17TV%3CJ(4qUsHiXbtjy-2dr2f zXO(%+&VU0AQi-^_nsw;IJK^^}lr;k^#6dQ^wrn&%-tor+Afkh*I9lZ~jy=OUJWmPq zdm29@V}-7m)6T<|K8+t#a--?I*v2i`T5ck}KME09^|#{3HQh?BP4_Ad{Qj9M|IIDL zZ-cOUq?=(ovy5iv7xAbW+75VJ!31k)edd>Sl*NImSx2I@*6wygTD|2rrHS$9GqIx1 zP|h@&(?H>xyJX9b!hdgT-J657h4pSD;xp@4yx#AI-J@fNN6g>=F0qpRmmi1jYu$Ut z!!sgS<`3$j8F?xKzSUr}b|Z;CcxuI?$*JtkjpD?<&P(dFcV25_Gt62#tZO(8svxmsb+|UPrC=x(v!u<5H@{YX0$~=rze@9}i8QQS5zGZM$(+Z+0|x&z((i@3a!G zl_iaV3&`z3vPIJww99nr%g4V%71A_Hj~==Trv8X@6qDg}JZC0qaC1)4l2;G5li2=a zTF*?wb$6JFYgNoN7NYgUnLM^gOu>cV#P#~-Ym;pGa0xdG=e5A{n?V`TH(cpsOBI(k zmS%nUuigL3;E}kmWq%G5?hJnUMDMr2ysxVM&`8S1sp$K-#ytVz|2MzKfM&v|_!&wk zS$lU>u0pSr1$)e|$m!zkE+*xpqAje4Fl5-z|cV z%}uhp$a|!-#?ClHWa}B9iVPKPio!BbLhCpY%Z^TQlOySU8GGLg+c@(@?vA|cm~xo3 z5@2%=C%u=Tap#u#l!0S#xLTLvV~X^4XZNQf-I?3XLZg}j{P=omjmLpO`=;W&%96)J zxjt4V-Th9PS*jLm%jw5P>EUZJDxMFRQd5YjCR6Ciqq?1rD{|hFC+!sX5@073A$e5o zTtAltDQW4I?{64trpH4vE&Hdsc+cxP@oH8-WiyrfD#!RZnpV>&xqf9=E#2w8@5!Y+ zvXQbfB3ffP6?Ty~L_F-i7jn^5;EtZJDX^HoYE6J;y$@G7aT$Lt7Wao~9nlRFzL*Pj z^^%t-Nk)zK(y(kvUM|a_)`FgzY4j4_v3BydVjhPPVu_3+Bh9~_Lvp}b-D*9{a;o*c zu>R`oSQ=iS9(6yl=mn(2fz7h(26O4Ago zNhD`m@u|1r)hqc}EVVipCv|H-JCSL_k;%EVfunKv>Kd3od~fIE7kHN^FnB&eY7GlI zK6iU(ja`1|jDCBKNU)IO%d2SQ&)h0rw|7$g*Xd9PcerB3F!&KRMpsQEXJbEjPHJF) zdSKhgZGCifj?eyQLswL;55#r9I@_|A(Qk@(c`DKkb+X5p5S$L3$uZ7G(SiUoidJ%} zmS$(kXZPKgxQy3P+Oe!CjwBy`S0-l$px?!l1pG!=tx1%)g$;+5!y{yYl*4w)&A6IjdZtUiJ zedS7Nuye6xZDb~o7K^)@y}dp!{;!utF4fX>IDHOFfA?zAto~e?h_#A6YT@$IXYkw+ z^{PMQfHNz7NlrB_zMs9mqQSyr05O4RJ3rsgI-W(eq}^XP787Jug`_w8tt9CCb{Vgq zECxgNhDXrt7U!;OpDEFM_2vXo=+Zhno2LLlI(-0J9gKY@H47Qp1m|6D>y(~2YJw(4C^5_yEhd|g?xAVR`&8Odj%b)$l#Uz$E~9^!ix zIDPF4V6TUo8@OGq^!5mElRBj(4VL~Hu->_HHF0TqFT86?3C&+gP@FX^znJPnH>CZy zla&LcjnA4VGUX{b2mANAlpWNP(<)IyqJ1fx%3Zxx!bW}uS%DYv{*HT}{Z9rC3&ewT zb%SoW7y(XDyZ#nl)cGh z<_dM8TW5PG)!cmcal=k0-nfdw&LUFSaY{5V*J9M_{ zV!7H%*!!;!^7Z1xfs=h-20?Lu`=nRy>97k#Q`AREME4-iZe=P;#7;$^_+-$3cP?!? zh*o{1KHY6-UGc%DLYMUUNcl(MwnI-3Yv*1R%8??So^e1TEk;KaeMpPXB1xUv6YX)+ zo2T&fHS>*&9_q=-_>y95UJx=YtRvW~S5E)y<%?BI1C`);+A7uD@w2JAUn^G}0R_#L zy443WX{P@#pC>qsB2)5KcDM$;s*6;2IvpnKD!3#sWR1y!u9Q!AMJb^(2gd^WSNZ4Y zz3J)U#&4{b>QPE-b@-`sai$bJfwptL1B0%EDYOGenE^Kb$MVC-S1Kagb4kf}`ypr< zQzwB<%G6{#7_2sibT-vr){Rq?wJXD8uby}DM~VCF`7?o_AQgj>462js%Dh5RpMDf7 zaCYKCNp+PAk-p<-l3C(6ZoNOyXjAw1tms-rIWT3bux|fPs@90>%5sKwzF?kJ%M8`b z{UUN(N@R z`sn>sQ3sXnbsz3&uWh9*yEWKSre3Z^$JzfeGcDWuxcb?Ht`tCotC2&SdU@gk3S}NlRmiZChnJ?U2&~cS-8HN{KdDn+k`zl-pG0Ew61)~Tv7gIZ9L6SN)pwj# z(4$&YilP=obvs)(u5KY+l~f(;SG3ma{5%ABVC_B*EvpswVv&-Nq#957*f<}Q7@w9)^WvEW-@2Nkk{!33lGdGZBTi`T>w>P39Z zFAD>}tzX@L6DBZ{WPg@D|{8953<|T;;^ZYdt^Ze|~9n8y< nf6jTq_McP!6S4o#66P8k>w*DYuM!2rq@1FxnoNn*t9SncAim`P literal 22801 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 W-FKg^bGCg43Sv)JKbLh*2~7aN3?W$n diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index d860bb73521ceaf40d4b03e271b068d5fb18fbb7..753ab2c2c6e9492950600b52b61a8241c79ddc53 100644 GIT binary patch literal 17033 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 literal 22852 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 Om~LLP0HLmWB>fNO{2NjL literal 32900 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 B)?oku diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index 795f2f7c832f335a1a1d78bfd9c820e1dca976a5..e0cffd065fc4ad31a2a7f09d7f58de66722a83c2 100644 GIT binary patch literal 1806 zcmeAS@N?(olHy`uVBq!ia0y~yVCiCDVASDY1B$H4iCDcuu~YQf??lv5P84^L>lmdNFQbpc^O2E3Xg`#Xo48c4WmT?s5Bfc9tTm~ aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b literal 1920 zcmeAS@N?(olHy`uVBq!ia0y~yV41|gz^KE)1{9egI&&`r1G~GYi(^OyVv%M>FVdQ&MBb@0DHC3!vFvP diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 580889bb7deaf3f06eaa5dcf06737370555241f9..14457f0a4d0ab750bd49fb09b1c1e946b3611365 100644 GIT binary patch literal 23705 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�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%0D!`SK?hshr3L$*aagIV( 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 b&~h=8fy36? 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 diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index 916a284433874aa33d618c30dd8751b32819e457..c4fc4d3979152affbe4386deba159c20c9e5c5a6 100644 GIT binary patch literal 22004 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 F@i*ZEpSJ)2 literal 29643 zcmeFZbyQY+yDq#;P!yyarMnf7ln|svN~8n?RJt3a8$_g}RHPfEOS%N4q`Nz%A-#?qN#$fq8bIxDgaoyK-Py8RtN!+|nbp4M%{t&?-tubK1nkp$wIqMf{~{EnxUgcQx6XEmh8 zlRd{3Hm0+!Q#|$3A9-IVeRv;dNyfYF{^z*R?z=G^gHykTqYXz^^=rD0LpSDnlG~57 z2WE-}jGP?K*9VLqH=YZN{q-};`qc05ANy}2e}CL?36J9MAF{k#n1B6jz48A0Vm}n7 zzb_W!e@_1Q#nTti{=OI!ILpbk^t; zxgN(7CxdQxcK8#IVPLs zh9|UKKGD%Hb#*^qLq``Eqk8LghvG>v0|Nt({U*Lj?$Ea=?d)$$3=Lu{Dz7vKaxuno zmPM?saoj0bYu?Dl7UvnPUF9+x3h+0ORa_b^yX4|%muewJc~Mr&^+GI{iB8AxP2=$b zT9EB zSYb-K_4=s0U{ysgPckO{hwW`k8h(KoIH7UJ$uqa^41Kf)H!>&Pi>Y$)6t}#*y|?u{li~S)-FlEmS$&emhL%U28Wgl{hYt$AnhKR>fm; zmCtT1nPYU$G^6e4X6)00Gc4PUA>R_@2DooLcK<@U>=Od#=Iq_2u}kd*rMH;QHWgK} zwdD2Vcx{Nb60H_G^Ihv{F1Ak9OT3)Oxp_9pz%)B%H(@xW%9w9LY&KZjF>FJuR$)qc zzPEp2V5_U*OxXD}Fb#cfZqB*rgjgjVl&cYgh{hFF1bB&#++xdw}Ov*%w9m2 zRq~tdR7X8_@e5PZ|EBEiy zZbra`mYb8T&U=Td@87@wk)cm??=|8^{T{Sux5dOVzUAww@$WTU7CfJ3JfUZ0O$-%0 z^oU-hJNubMFDxv4Ano9A9}Um7C9SzRJSN7wW)`34aCyZn4VOr2<>xCUZ!mp4rjxe{ zp`o;#oM>*XtmfPO0~3!K@4o+;-TM|zRH2(*oPU$Ruqz?CV4Z}+@5hf12crM>nU_BY z1YB>DjTW=rvtY93V#)KV+c;i6SWo4k67qRKPz z<2{pW*RR)Ux3#y&Z0^2@bl$zMJeQh1LeI#Y*{b-RK+@( zu&F7T@~5;#oC)W`fE-OeY*JEhArk)gouqc?hUe~y=?sRQgG}06q$-bZnT!;Ri+#xe zKzlHR5$mQ4X84%Y_2dz?S}r~rf>Rd$Om?3R+oj&ol&*hTMmM!_5}h=MwEO@CSsat z6PZQ-nq_w`p#+$Cw^HhDx8bH;Rjp>#zqSnt9X4v^#`pD_g(TF}2>-*>+k5oe3Hi?M zx3#ss{igWjN!u)aFX{`36f*XX9GNP)spUwF$e&3}%E}6Qz@|4JJ6Ig7K@gWCa)!SK z+p1H4JEa^V&Re(f=Hj)A%?LfUp;5c@N5&&5yj2H1nOSNT7cj#zpkA1!1*`HUBb2>! zFx#7q8010SZj`oO|MD-Tl6ywno8PTir=)weU4xY~2NcJCW{rL0?5gs*oN$PS7Zx8c zdMEa5+bHW%%BM>OTj)O7%8q?_xF8A#8VfZ|BL8_^`|SKYCY{y?uJDNvgC~T1cI#8&U6Gh{ z)u*O6)l1eI@NL&e>tTy2A3Gn95fz(`_+Jj$ttb5MU`Ot5ywEA&ygkHmu@Q+{E5-3j zQ4?;gQ)R7+VQA9sy^ZJZz!g&}H#yv2EbqoBDJeZB+;GeD8Xe`o*s5MQAu84|0p--m zCU2Lqc#?v@C+H>j1+=CFXJFG{x>%;9}aGNTSz|khOJy>~2 z(6O+IC((qA&-_JezEDz3%ag;_)WUM>)vsOK8g-j^{6vvB+d*rL@9B& zy2D!LF!{$9N0+_BsMRsMHxIbnYIQj{3)6z!x%X1_d~N$eYy3Xm;jy5ITH~`VJgAr7 zRA~xJf^Z!*%5VOM^3o61rSICM1ZA7E3w-QhZ*PUxctbED24i5Ajg{2@a#w14KK5MB z@(Beyy8;ewJOw9bpuCWJQ?2Ndz0vTlnswP{&LX3My1u&osK^L5rkf(;dL0wf?V7MyLvui2SKiAlT6?{Nyb2_r z19sdt%xCXzXcFrMoP%uFQf_7Ey1+485p1l_jq01&%^~{iPMyw`HlpW74$m9dJ|`s| zB&9@svFwjfW!p`t{qAZ=CM|eGVph4=MB=!&DlTJvHzkC$U3V|;(0;||Y+(DFpN^P> z#FhkP^0^Gg+~jV9KHDRiz5W>cIjRNtd{?q+cP}C1bb#* zwRkVRFeOWP9rH8JZlO~AqvL6?T^@iCurDq9GkRVdWBJFAZ@Kjq%NkuB{SgW$#cn%V z$fM~UZCQR@?VuLx?B1@N%V{RpmAZob3gEp}F$T&MrCS+t&p( zwh0Th5`)trIb$o@UwW3yK4D zK9_wVb~!ZI`K^VX_|(mjVh5)^{){$>+itaBPc7c5pFewm^YhzuKeV`H2 zPSroOS{hBi1c>2;JF2MaJqBL2=+1n{W)q*)e0Y4kr~m1(YZLJ~ks~egbVr+&O;xC2 zTHn@|Y&Ga%Oa)K4wu^~1XhSq{HtrffjV7|!a~m!(3-q6yd`=_4N9J4ael0cgtzxSD zOnRYV`{6-vvc2Bom}i;Gj|cqsCVgD_PbRDEXL`FL!xFXM`2U|TQL2JyorfUMp`pS# z=aZ!;mF&CgwWESB?<5eJiwcpsZfaF-Eg81Abg_P$2)W%gHK(dA!6MVxXvl_%uNw8> z*H}i8ef^X<+e|;cf36#50#b}|YCE_OM!&4wntCVCzBSS7whoHeT2&ot6wr>mBr(|rC+;jq+bxYhK*WVNvpm)BKW^BRz$Wr9dK12T$N+i~snzAuJdMnTbQEASxLnSE5bRX?A& znQB&}UJu}=u3=NhE-Wt2@AB2C%v-ROpVYTV`QOVOP2G_Bwbtt?M>;YU{4=|U04z&p zZhqckwj{}(LEuzX=Btd6+|%ewX4JIQy33G#DJaf#A=8W2oNIZQj6^?r^KeuM=KY#qz3N;U#jZF#5T`9Rb^*eZ@+$Ul8L1EF8M~U`l-p<q3x9S1Y8!;?aP!?#m{YzB62)&zJ)C?B3Vcq$FN{_1f-gHv*@0bZ1g- zm#zJb-E0_E*4CxCd-r`C0~C|kt&;xWP12|o6oN{?98@{kq?VQv$Wv3_`%++hY$wB0 zM`AzygVmQE7kn)8RY!W(`}Z%N5K)I&%+k>SLm-tI$$Y@*>v}&Zy|mO3F$ewRFI3?@ zbpDXT+4CLRRgf7U40w3oeg`#>e}0k#7A~$o;MZi)8j>a@4Gn>3Dfdzfo;-Oc3C05X zJ9@BEpAmNLTU4>hL?fV6_AMx4yCIN2dQNX%&IT-q=tp{bdg_@L1;jcp{RaMkRO=f; zg-q&;O-RsD+^G5agED5VaS zIZ-o=niU#y$ON9&xKh^pjx}lz)tnp3Xb%0dbWS#kYH#JS#KB$}Fna;n{MPw>%UL?J z8cq!Cc)p@k`HgiY-_v8aroISG)@Idm+p@Ud%Xd8$_ra(@(}wBl_v|RLPnq-BPoC*K znzZ26jP`@nlKVa~^2yVuUbrMO@q+|R?RAOh=O5g;hl`8?S--yOy>7NTm>&!E6!y!# zHNH3Lv$Mdgo=^jcfi{}nn`%*hogEV!JDApXZQM6Kgp{RTmDl=ZI}{`AVXi!vAm~Ft z?KB)%cZpseoZ@g!7WFMg5E1s0FDDQs{=xp@&Ksj;*Mam1NvFN-=Cxd4v@a-QvuaLQ z-SbIQeV$b#$R7YNWNUydGiW>bOcZ>4DA?Fd0&JH2cXgLxMqqF{OWwOm;bWUc;fE@x7m{^3Y~S7BD4 zWPJ6iV@q7v^#Y)(6Yl%8a=Ig2DZhQ>OY*&D;wy(&VB(b3C9CaotK)?j*cd|gtS~Dj z-#Bbqph^@GJ5%lH`7jggadHvNAwBCad{0xZ(e0lgkRx(YQA_JWLOD;h;CWZNVyeS_ z)&8OL(g0LEhrk=XjgPOdae*=*Mb0fa*0eIA2BS2@=O$KKRwBlkM>_f%Ykkyku}KF4 z@xsEbJRUr;-D8puiSg@I_VbT5U6mITa~ZokUT~o={PqfA1Dn={DN+&Ax{g7AGV4(lzrDgl4dn<3MyGoEETO zrND+Y1(8Vt+yoMA6JLg!hU;4t7BS89wk0O2$E%}dzG}H$1{>9v5YjEEbe{|?l^(Iw z!8-5T@GP2gyA`QNk5Jtj=NKB4fCfBxmL%TFqglbG0P%UpqU<_*f7UC8>Yr%}9C8CJ zG62X$oHyoFwZNri0JQNyFjm5c{GBn&HsT{SO|kqHi4P8*sB zBrd7~JV?k;=Z;i0tZYRI6lzlanKROIcF8wd&;|>+KlpbXr_u;wrPgp}Ptox9R>ChC z=>S0gf~?)3dpdLoqR=>`N=o2V@BCRDX?%1YJi*?9gVX94g)DQb1iFxXq+&7+3^75r=kW zX3xI>mCv6`nE`H0D~|UP!mL`hcU*e=O&_*yp808z5EQHI2c1*=JG%rqRVnx>NP^dN zws3K#7pxXc0do7Gw>itUyuO=tDImBW>Da1ya_q6ea}9V z(1?WIa_gla+RnK9p6N%wSnCFs2SV$WbQCZbtmeNH%Jc&=t5&ITB}us+7;tNXo<*>J zb|F=Vf{W(ERQ>wr^7#8!R~3J)XnW=M=ic^Bulr2FptroM7EQ#x#}2${*f-^;PsQ#y z9L3P{XtJ!Ap5pNDN?M zWYpiJ1Xj^tH~!(21#t|TASV*vdSM&B|Mjk^?7x7l%#G?9l@Ir1A6|^j%=3G|sW-3w z)igr9{p-B_^!FG}`y`d@X9rKr&DqB3cx^sFP-Ve^fFl#j>klar=r9J*PVo_hy)#)a z4>7?ry^n?-QtwUqctPc$^x=@j1L; z#qP!YF7qR6_5>cQrY7P@{V5}K-%E05RT{wCfDlh4(@ql(!WKqRjP;^cqP%IJ=-6{gwG1{^$;B6b9 zQ*oSRc&ct4AouNV6jYo>Ls$vpT^wz+o7w*KQJJV_m45?IV$SOlYr~Hwt;()n6Duoo zK-dAjZ=GIJ_DP&Ur-B_(gdcCsLb`UlixVPSea z=972EOE<2SADKN_J8qwLn-Xb^zx!_You{(~d?zQwO)tQS|d z+F5;Jo8sG^6*-b}qnC#4t&La;BG@*4bCd1llS=ku8C+0k-Xh#T&@4UadVIXLr9th( zT2fD*hyq3Y1T6SMPU*o7%G<>4kOVfx7v$E;j9S*BGn6y&QbyOU4nB;mHNleT78Em8 zm}Sbw7B=qz&Q1UE#Rjon$aIMx81*hAx2wPbWIrn)u;%vQ8XPRqu$@p zV)oPVTQuMHxjJ>1YEG&CJ-2!>E5oI*`!dRX7%>!XTm_m-cN*K zWFis;0)>EPlIcgiM*d@{xZ^_+F{0O;M*`n#m`Dhs6FpeZ6*SDSKjy7*EhkPXhsumt z$Nhr+4W@hbK-N%%Bo)m-Pv1BId=wilCT8bhLsQf9j4xksv>6#0>y;qQO;=ehFhp-p zGeUqcz=^~ayW<}mT&pB0Nnfp^qT-Y8=Vva%^z?u{CPpoq;Md}5UpjW61`mtL*t>OA zZAlRk{sbwLcXgw723w?5`(gtWlE%|L>$_ z;JBzroveRQ56W2dbu_dYk#JWYPvLWd-&J{;74Bal<^sb)gXDUJ8x;cQp)B1T{+^ED zo#4eZI+=*b$Y~>L>g4DEKfg)m&q<%zl%G8VZu|XvT?Pt@i(BK{xD$CU%`7q}MvL)x zU%wao=pPbLr}XL-EbGaW09Y0(zs+om!Z`3YG{YLh?^cd}J!D~0wOeWHA1mKB1IC%; zaE*?Tu*TwGfRcpY-cPjV1o+((p=|YJcB`g9p4S6Sifbb!`mLYs9CUXTp-{zEoXCFG z{w>yY68MRrhW92T2cnX%2vh3nNHo$?y;r*)sh$uO;sFle_w=5g zC(hYlzTzliVoupl|BWH@^~815gj>8BnZ64@3#DRDo+%ERbr zw?UzTE2XZ!J{qQYghAky4hj_KW(x6v87#nlX0*%-LK5o`K)gIQlrt<+?1wP)8I#Zi zlnn=U^UUgmTqopsoldb}G5|8j0c8APHTjKv-RU__vB^v}VQ%FEa2O?ol|gdXX3^(5 z3Ymvqz7~Q&{6sS_G1LtNTn_|0CG)Xg=#N2=4h_&i4?zXGCicHwLJLey9D+}$$K6iD zXTONex*?R-)-P6y>a&MRDj==dJNJpo#0z3$qhKV@*j9k#Ixhq=2c$|O*l+BIsdAXu zq`|py_w{Ci^YYkE<~nOVORYBVoP|$MbH;t|n44|o-&;G6!N79~G8wU_j zA(4iT4iytqyYLp~wL3sb&vsU903NEmw=}e+F|xP^Z)!cwL>sUX zIS^ooZrc*WtTU9$jeY)c;Riiiao9*ge4K$q@L)wE3!p?0<99CF6%`+K!hi&12CTn( zhk7wI!(c#ayr4UF6dYv#94CVPuxtxdO;T+?gg5T~u6Hopoz(ni#1MPi7BPjvcue6N1YZUbbG~b7LA|KoiPA)CKM?J zx{|ZiDRe2QsR!?>Mfgfu#o?Nn7TO}6b1M?dbnca4_~*A z2VxBaKue?>Y_J~r5pqs^<}m26_eb~|X$6Had&-@scnFK)Jp)@1+$2k6A^4wuOrF5! z%I~|6@8XqSw88}h3`TSTR7YL1uKu8TZX2RYn217Yk6|ns4!eU=OfPAt!KeDksUWt(wG-OsS70Z>qSu{9E zFg?8-hLregBX-?%_pfOW=38;6U|~%ow5_wQo*wi{NWgeB#by%^lV+)$b(%ZJqM>4? z<$%yVx-_j@ng9A1Z$=yF<%edD`^$32wk8Mi|Ncggoz{+y`aV$5(-fyIbGqv%zh(K6)RT0}24C43=7OrhL%q@p@qMn?r2w6hLGO3X19% zG~@TvLqduA@HbfH$`qAaOi@-5xr! z8Nn=MTCaqFyOZw!>1eX8m_RWiYWl#&r2hNBb*Yokwv%;HN&pRLW^EJnR`H= zQMf^hlmY|*`4trQC;$8pC>*NI!SPd;wXFf00#k(qrsd|u;wdyClM#1rEMl%nXV_&r zdivxqU*5la_e4rc3TT&q`Uf*}qbd=%sTaMf4s{=6D}+h@Niu=g_j#;#j0(#jI@1 z6wmG8S_*<5fYa|>fNYU-+nhMX_25kXbsy@d0^rzdmA002f#K>95&IGVeIg%F$eiOK z1W#u}xiTKRLj+4nyAGBG0-erla7_SwmC6bVzby^|<42p!yZpdUEe?JnO_}e|2F|ld z9nwT4_r@g|Xta4&A|*N-3fvkg-`D!S%ELeihxVBuYV6OuyrGBk3No(Wi5fVW3@|OB zz0g3A1zxF>p{1dDCe@4{r2}c?MgMYe$C}=uqPiR=Q~_#$jK1N*=9-<+=Fjq9 z#0aye$UR|w2`C0w19UP)4;MMHM&w(}7;u>md-%i9&>>c<{i?f?5UH=M#x);-kUoxR z{p(I>TjJ1ybifD6VEx^{DKcjCylkTtB(k2*^URmi3y;T(B ziz6i{6z7u7AyHGosWfurjK@4F+uPeQ2djbhNoEsdekZi8tvA{49mhMIoH<<^hthB! zbZPef98Gn8Of0Or-@6B5XH0HYFgF5B5tGY++7l$NI&9Dl8~ZA+1P+zkpp3V5u}-#j z%g+QiG+c!g`2m#k+sU^(pD*U2AW=}=!s1u-&fX0ou16nvK~SXuE@Pa4TL)`zqh=-@ zys$~u9vv!ogxQfLN_|d|tiSH`1G{5`e?)l9=} z?4ErFDuN&VCYjInh^@@5dxo58TOlTM55p{t<&DR;bIM09dyYdf_NqO!oBI4 z6-7-C4-W#tRhCMN$#23w=uU63X)O;K4-c>0xV=ROEkupPZnw=zQ~pa=QJl*Hk_4{V zn;V^kr{#wZh9$`0Zq!0GZ>A{8^cQkKm)p3MBRX62YP1v+u{_|p7m{Cb^0<@}$U_Wa zn|5zLI0?*UivY%5T0M0t-Bi_v{P!V1PX|N|`h)}Br2&54*Fd}k`={vnwr}<&M-?}y zBbEpqB)-seqHR$zmIXox_c*^7;M}QfI0;atihwORne^=t+S*J?O3ymN*KANXtML{3 z`f`igw0z5vzdPwzxpM2LMnRz!TLe^A@NFzGL(}CeN4E$uYi~m(InZ9C58w(2WP3_- z@&u#YrC@GH>zV-W@ek5K(WUfY7@DuF*>O4#YHHYN-Uc%GfD*El<6+77n2JA;S_S$d ztk&*TVQw@Gs)1o?VwfpYCeVzUlvB(wlVEB&=s zK?vuRQ;~xKQtwHa$%u2+iTeC^%{M^4asx;x+xswXzkKBCg487$k5yEf#$8a@Y@ku_ zP=0x!29wMtAAzZjwPT(TN%`fYpb#Hv#)xA1AUN zc>ssLsRT4p{W@WtS88H2F>B2cFK~OhNS|Q2GLf4tlqq&sVd22>~WV0!*3@w_|?m`I$(W6*tbxi1u*!s1im}$!_m%t;rw}nl@7uiUg$bzUJ z3^S3?z5tN~!aj4_^f^#`qJMP_U9%q8U281FZk|j>G(PaQw5+|g;U6D`FGlKzMcBka zM14NO6|r@cm2Umgjd1*Eui{=%ESrDZYmn4#5>Y)@!&6gh9s-nEY@$e*+x0=pcQ1+@ z-v?L@j%jy==%+hl9269F=~@4!$GfM2!o`@g8tAW@A|xNh$V*0!rdg2(n0;$i(yJP+*cBZ)brid=xQXcmp`n_eE$aus&#wKQ8Z}fuqQ@HUkjoap9ccM>ufVq**R_M zP-^;V!Z%cILR)PqJE4W_K}2_jsvUHbec0eOP2qrUpH{eHE5hh#@3ZJ}`>idp4XrV? z!;aV06Qwb2gK#I(BBYwLG!h6jzvyf(G|pKJycI?~f??*HyC(^c`7Lcn$2x4Jf=8^P zOOB*n(=PNQ2NV-6-@n&IHHDD*>tE5y(0^uCZoBk-pO_mvZJmVQ@6UN7mD6-Sdy>So zIS(ohWNrzva(rz>3WooGfJjNRpPVq+*$s9dL0WbPR&KLJbf04Sw_FZZaj_L?rU1I3 zQJwh%_e>6-8SUE}RZeWs#B6WW;Ie-!4bjhMZkNCwvHl(l#${U?vbY>zm_Q^8r&0Yl z*OU~&NymV6Q2Uo5VhRiAOV(*S{7W~D{R%ZoJ19Hb(UCb>cX-)f8ZL}Az8?o}#EZ4#<28rZE%L~MaRJTWCh;NICag!89>mdHtA z;8E`?ucdFvn?qo{m7WCyAD;B?LlcRB@)E*P*o`L$17TLJlB=bOSGkXeNR$lsj6Srj z6sBMjV!G$rlzqky98atV;GfJp>exPUxLEIS*Ld&`=+uMOc|ZGX00WPtKGZMfV*_tk zmk8)jZ#O*j2b`hur+@yAqPnlJS(%EuLuYr;A9}o|o^+veP-o3rmWz`jw1TJtw&%xmV@u3U)UN}K!+bZ!TBk24%VoCw1%}o->zt~)P z!~SI(nOH$INDXdA5UMC|S0%y;KnW}bZvH440GJLCWrw;$mIhT^#3RL9)V}q#Nui?> z=PNml1s-WqkTaeZENWIiEsj&m?Pw}Cac%|AS)|Zf z8XV<2F+JK_YYmDq%ZHlBIlCP<9;hNu4QC5}WpZF5v;=bJ0~mSpUv@V;SMM4@*xM|E zFl`QPavEdVj1(08KyG2)4)ph*KQ{xa6KTM3+%NwS!Kk{^*pvH6C{9@auGx5ymOzDt z$%Gd01CKTTOPAd_=Yv-R*dU0YX5$9bKvyr@X4Bd3y3IXC)tvdjWNpxoJ=koF>Xq*cXrxY*O>*wp|!q)yycYtb}1_Gb>Z1{SPBco+QS z_IqxsQ@wwpKp~n#-Y{zNsL`elY=xAxv}u58LzW4sqJ?6`3P&P}|%g{)Q& zBtdJFmNO-5Y^3H7PEAG8N=W$nid1stC#HRA#iz$7W}LCiI0-^sxixr-&z_0DdPPXh zEmdZbgrlxbOa|0&Y%F6=PG@O3&&=wqS79J_wV+^2L4$Vi)|pE|L5RhCkUOV?VQc68 z`rburUjCI*Y@9(ae65$V=7o1&_dVWG1R*!FhC`-!S{PZSJ=%>I|gCg$em-#T+>u6s`l zb-m!QNUe;ku-mXGIbk6R_Q^m|=Hca?{#YUbgTT-M=S=smdy9NJ2>r2S4J#ZS)i^Xu z>Rp0=m6nx#ete?2WX)^MOKNJG0~gm^jga#8)Cz_FM35B_y}&_khUfhCE5Ps$^3;F* zkpB|lf4BsuMcDEmFQLQfdLaHUPeE?Czsm8iPk|d6Io~S$FZU(52mR~laKqM(Kc{p+ zTyC?%g%@}QJ{quG`Dwz0hXy0(&W#IDDr1m+gh{%+CqgE6VuFLKX}H~Im-kMY;Upnf z-n{UEh{vU^P4k7C{=orSOtPPGwVp>7ynIn!4Hh?uIo`16Qh$7Wf?LUJz&|%PRfyi;+Y3Ps%okwz$$s`3de)l2h$vN3 zkJu}iIDOGRH%|`C?(O}O^zyxDxcSkQAXvQIcEdNcgqFU&8rhXI)IU#XBrPL@qUPh< zNdwuf9jAnh3AGctl6kU(s zczOzCQ>g25#g^Hw@dj4QG`+mL-BhYd$a5#*T>Q!vZ+j;v@|SmshQEe^3{mY&xu+f7`l0SIJSU8e8p+PtQ z&ZBn?HFd^Zm#_+$6ykq>d=~C#y&+;;()slC$cVjnBB2qEBmAV}BAilJ(qB(CD7>N; zX-`x*IEq|NgY5+!y2aXO+zK&Oe6^ zkM6*Gp*H;r%;X)_meZdP|LYgkzm9Rj2)lTPydA4Q=Jy>!^gMKLOPXkPfcR;xyrRuv zcc#1R=u~d&Kc(=m_x;bg{WXjHr*EnJQ`uuXS_-}=jgD!ml|Dz8*X2@wtN-_>+GF*; zh)4g!KK_3Afxw?*VEya*&u|S23E#qlmj7HvA`S^oi~3A>9?(HA#ywbL_}thXs{}MF z8aC{p93>P@az2#dvO;YrWWb&o~ga^eEsh4qxySqe>*n#`&k&CON@+Zj(cbP z@XnfDJ3R{vx&F|=0r&1qTR+7Ul`QDNyTQPaiHdfUW~oTw;K)(W2HdGpo5pI{nnh4h zORVJ8!#Y2mmqkQU((MF1_EgwSv`|t7oxbwTE6kJ}S){`fzwt1)qWn|YbBr}Z#=D6Hd2B11(u7dnuEvALGEv%C(&CZ zeM2Z`LZsuvGhVypZ#8veV-e6tAUQBteNyVm8BSXr(x0Cww>fjfyejlIB*`-&gnjMN zBLeZYXGM9#c@~WDn%0-ilPewDZjRUXhN>xg#JDj)u>QWG01c^{5y| zA<7RQQct)fia0vb;InAa-1(N+?O*?Dhyv%+*RMhDo)R27&RB#o5|8+z{eL7|p)}D@ zf9`#qKU{KthnUNBW-))p-qDe5s)H(nk(pUf zvZpu05@9bmjTawWMfYoNGv+F^K4jQimreR}nw!gum@+X(ESwVwGB3n<&PUMoNeq6X zB(mAG*}tn%mva>+$EFQRHghI{T?#AEy@`Th@K{0Z;nSCFlDWZk0|UgF5I~vEIARo2 zt+uwdD9U9S!45<-`yW4h*7U8GGKNjiKPyXbRi4i)UQ>2hTq2B3mkumjWq(68Z}u3c z2SeXtYFhvE$XALSL+;PdiW*+S%V=KSSCdpShYCz+A3hxLe@WP+j@>l9Hs{~(6ReKB z;pe@;dJDD^c7Jhz8$B~1K&tv!(0g}RhDD=N=D{1GBy8W_f`@eS9F~1gJv=Y56KxdQ zjIelBRdDHY#5~Qnyh+Sul2lrHSpr2sz|ndxLV-2uNjh|3q6Ff(NpF{EPr_JvRs&k| zo$4|@(FzT=D?#RSuQ4gH-w|-z-bK8!SDyBMxLqWNIi$5?bSG)sk9L z11#)MNspHB~_@%e+ZkQULD&UhmSrLzj1rRByOD*Nmk9 zwA2;NX^(8Op|!U!cR@P0^r_bB0I}q|TR`t0uER$4a+aBIaa_1?79>X^*N*oIH91-T z1_}SWYct_3E&L+GwK85T@qBDt^qBD~f&JB|C_{M`#1f&T&Aq*z*UV@XH2?Czo`!)} z`y#}D9rR|mFDJYth3gx7;sy4sBZ|Ud{(xV_pfD!6T{Yj%jOeeeR+R{S-qvRqbGY|{ zg0k*#!j^7^tx)j5D_yZE{AA1$+UR02b`mO$_<+oz|8<0?8?tJjv~3w=`P;U`~Reci*w8{ma0 z+P$)tU+z}pK|PWB)|D<0aiFmjTnm8;%>E2&6(j^O-6be`))8sKN6%0$C)Un3{(%k8 zj8#61%Xkz~x9IuZ+&_M!u?~7AQB!U4R9Q)Bdu*TnORAwI*xbd7Cxr1lagod-4)4N} zny^Sn{JQNEf$AY&9V-eWy4SZlSmp~$q2qVE@JaNzSjO3i3Efd3wq*5r_q3Vh*&bSD z6+b5OiqWV;pd=Azz@TY7W{w84?h6qtDA`ktq6l1%mZ%K3J2_r#2B!7DPKXfKYj3zV z)6&k@&fO}#G)#RplTkJBky^fwCkkg;S{4P<-RLg^;!`Ut!Tn3C8hCCo-_v``4i4-w zVcFLxeLnTS&O5R=#oJpI_~sv;ny%dyrDc&{Ihf1+j*1-A!vAz}nEw*5!yLT6dd8q? z^4dK&enP>UjhE?ij8UL7!xcit7Xru}dt7E(0roXnaz?yT(Dd%VH8G$KK?(bROe{!e zXR?8eiTmIR8d2Lxas+CyHs%71KO?n;UbYf_ALj zw&8h;VQWu>_0s5t^lo*(yvro5COcMMefjQdzs<3I#um<5%62SvQmqGrh1|ha z$2|l)uP=&BFAm&)YwI_~dQ&7)pUei}^3StU|KjfKmp>*QGBVS9w^n{lv~b2H%WsgO zmKoll3UT#>hHCbYZnptqj1+IAe4ONmgMaXj%64_M-l{tR&=;xW6t)DjYq!*L+5f&u?fyqQ9*h}w>wlke!MoL0ET2@w*-l~O_HRe2p|&N zw_W7{0hFBS?q#U?)(L~DSS58P&a7V#4*sZ6WrEV@6a^i?=1WYnfVVaW1lTm4-@1jn zK3;IKu(HzcOhqfzRuF~*dWk#a;UrGkD*faoE}@*~)h@-S_U_<|`EV@DM~=Oa0wK0H ztP7x^ZkoLb4dt%;+}H|Z!6N*sw*(MtLaRRbaEyzK0F~1to5Kl}znZYBA;~b* z^s5QBcdIdl#V)Z%xwH3ypupDNp6+qTnDJP3?6^h#%e%!IWzQNWKP8!4Tfb;%7d5xB z(HYuSf;KY$*ZuGQoF0$!EDt~p2>cDT!d)0f+~!b!!rRv$pQN+J$jHQ>3C41n(em;J z0O=JTYZnQaa1;n49r=^7I>gpfv83l+Z z>8Q26UjF%u`lEuV*G9Fra02;lwMf6E&5LyjRj*dZqMD*rv45r2fw1i6ZkA^r7{;Yn zgeR8Ceq{Oa78VvXYtOF^4H^2a5C&37A_;Eaz(wSd`5n)zd-X3XyL#r3|02@ zll^gg=w{YTEx5Qtj;kh@$#`d(l@J20f!olh&rDYzhJ0HnKp7%B(byu2Mjn*4+ zy}(<8U)g&NzEd!--2zCub=nhwgCCV=(2oU%9ry!IuWH73^H+&<4Eq{$7C6Jx;uq|7 zKWb0?*M<^LQF;jZXfUUKd}0Cw|KhvRHPm?CBE!=o<}_snuZ^gj>G_V4EVZJ`c6O>> z>-q77986)*am8WT@Hm0SrXLH|hl&^IJx?xtchf1s)n(?^^zm+kdGQ-ciO`*%Ja?!@ zXO@X2V*JN?f8ut_?dsXJRv)q>j0XC%bL#FtU360V@t%wkf(o33^`;xV5aaeOhjb;~bm?7VDGwhx;(V`m zY@vAog2{YK_Y7*`cx;GN@@I*Cmb z8)VTkGzI9l>-+n?A(HXiZ@vX+{=$H)wijN4(>C+EHq&gmXMEv8FwyqL=H8wd9UUDN z6O)(PZD$c6-0)bBZp0GZBe?sKHFMyk#n1}hV)_Nwr;b<)7|Kg+Ll%-ucajtShr8Fl zjg8$zitnxk=FjjVDng}rLE@}D?UeXpNMg{iNe+dB1tjo8D?@BT%n&x~M{c^j`}6I& z?+)iP51$?$*ykI!>Uw?hJjNa=6gtNE!f7i*Q5Pq_FthLMQvK}YJ{K1kDpU~g*HJn) z{MUZ^BS&hzKA4bDg%TEH^FA#NHRt?@xc$kKrGXNBvtW4kgzb-<7>h&!pIEG6P@6{{cMJPvi zMb(h_y|#qCL^eYBNqd4vi|CAjbNuX$Cf`rRu^#F`3kzG3yrLrg-uX3XVw;|x{vMXp zjA>BCO$rb@bY1gh57i%bcHCaOyG-a`^x53jjR6p1`k*h#%PfpLA1ZT9INwW5mDDUk z>TH$u_4Ue=o;%{`w>^S`Z+bxD`h~HQo{@o&2=ek6z~p01RGPC)&^gCw65zRD^A%$UqeHiI!?|kb=cuDQemzg8^itN zWNhejL;A&9d2kR3`+LPy%Ud+E1QHJ(h$35%o|Pp=$ILvl;}EzA+TlhG-_1|JZTo;rMfKrpK^5S_nA8ghbQUMi8unJZ zpuR`EkZqy&@IXL2F&ED*1X-S^R+opj_Kwp(M9QSlh3S7WijWZW@7kNVNH~`UKKVos z7<~##^~ajForFY(+#gH7c*TwKTUzb_zbum4Ymu|bj~KTXm;%M=V?3a^5rDe4_Bow} zgMuWu2`>|<5zq(X1f0;PtZi9<7!n0coWH%d{l%%IGp@5UxqL7uKJOisKl)k{)XpKw zfp|J#X5@zza~h@Q*MhTsfKZ1&z!mZ~-Uh*xUM5d z`;MwR+3E(+T?$VuJ+W!!3n4-PZBb@fgqj%*?_0dLh;D0>x?zaMMig9mc$A6i;8O_Y z>N_Y$;p_Z>m7!pm?}1&q5`!0TzLxvz0tk^a({whwkVyIGuyOJ6m@FElUbg@dZr1uH zhK0vU^n0Q}TXlSj?8RH1v+)mK_Lk$S*xKGxE24-QXbu*KYUm-tO@j1vzszA49|sF( zFdBHO{HIJ$8*SITBB&&-U*3kdCTzU;?Aj0p0sdT7etv!}0|!T(A!hF|pq>X7lGl&{X6F@@mA&OsXEdf9ynuE< zP3%$^gRly+5f%^@YaA({$7zp51UgW8<3oTu$ScUt*O`1{-)1^K z-4y&J3%C>Tz&eHuWoa+sWPMbCov62+4+vo>B?0>fAkDuTruu`t%N(y3q@+J55T^b3 z(c|ZOa`Bywy7`+_tY3rcx`jiLg)WhZ0tNE^{V2jP84p*+18D|iMIA$mi#l*Y5Gb(f zhb{(*Q$n`_gGj;@6E1E9Vi52(G7~~bZm)|zxcN#Yi`Z{&lG|;J&(t-x--VYsUC-`= zWwx|-)DI02PStr!|HDi_O^!NvAvU^RWtWnNL`wd8wacKN!Nwt{30$MFd+>c5UVLD@ zm5m4Fo=UzG^WEzP?MI^2DmCu&=M^|@Y)G0}6qNN2c@x*%h>?8w58gYtca4)-g#e?^86}J)cdzE0N8=DvN9-Wowx}SsMn1>HV-Qwe!1QD zx$=ZXA^TZRwdURahvHsI-GjRQK;Octz+Z_XpPicnvCVJo=QTzc8d%iS)p=&qQMyJ3 zAfNCJX|n+Gh?Ocl^Z5tM+`eIm36$2mKPPZ_#ryIVuKB(t4UjS9w?#(i0Q@N7k7m*5 zR&#g+v`JZZ5x}2Z69U@p&ilwQF~3(6k)njY5VQaf{gKq&5}R8S@Yaw=8t7x7hv93y z%)B4ZA?Su8kmu8r^jl>7&VK*wwV^&B_ekjdLx)36nEe-*ikDsq;U}jL{+tYc>-^kGu16$j-@Yy*lPrsFN2ejDiN`^p&dtZfikauuXr?w= zEAqNgC?(j(uBB8RhP^*dlHyL{7 zwDb=Y@(^+(ZlV>8n;UC~sH{!Z^LWzS&mY3jS3otaPFUC=BqWRJC(~!{%L)@WN|zu<|w`ZQ_8uSaLg5PIyz`Uh_h4_7X4{7wiqJp(F?So>tYu!9(a* z0Rd|N+_=fe7#OEQc`7*O&R$Yp9zAr)=Mt3p1!pH%2)Y*T*s&5p{D-%CJVQP75Vgsa z@_m&y!e)obv_e2AzfH?TQ_ul^+DCnA&KAU5D2lJw70^7i!bAa$rZcCPg>I24x(a?5 z;Fv6>tZdRAuX4jpHwzm!4J;{xZqN4k2vyu%mG-r}8^(sZ)bs8AM#NE69zSue{hl2A zg8yad`L4sfN`E9H5MNKt^M}m4FQ$qI^ASg^_cs=rVca7Z_tiitkQJ&kk^wyu8wVX4 z^0|He)p~ouf<*n0&C_(faDI>HOp4~pZ$fp+ioERCbZqvQzO!D*`+@U%(iNZChH2~X z%eF{KMXOU5x3?$Q3aINK-kh4AcDo^2;d<_3aUIC735OPno+V!zRgSvh6Q{qwSe5i4kkO00qz2qD*>mIpf>_Y)12j3L= zI5M+6mPABE9JsIg8?d#XOKh{1jg2-XOqA$FkSM zPSx~q(Lj*ZM(2|6c65*Q{XW)uR?PT9Z{?ABNg~Aq>LirGN~?GMSq70}2preaRQL~Jfg`S%Qtg+gL}bi=-ayhQA-aQ0LPCNd($z+5 zD$*gBV7r5eQZmky>c}T>LsT~}I7C=sSQ51)_)U!BnX>zKjL3LMfo>iiT4UqmrJ7m& zjVhxI$!Euck3cXa++gQnmzsXJEpn@6&AT!;=eE2UQ10A7D-xxf<7jYL?fUfGiyaGv z;9k+A?Lm*u=3-h~%^K7;={+8)jG!`&)Y~mcAov^v)?96>TPO1X0!$0xw|Ni)N)V^m zb-Jxq){DKkOb*jNy1bGs@L=!kNpptGt{G{EWW#ED|MfwEb;$2oA^(`B2vnEZ%3D*u zrSia$!sPq8iA9;M$I4qA9<3b0z_Xm=ArH8tx*-EE6^)FxeISxyr2@A#-kGt1WIr*% z9kX=1m|Fiz@o?6{c0pX``T`K`9}Ilno-xv1NE_W;;LUEmYrzxR(PK0R;1p8t=^1oR zKb&R^cwX}{Q*GP&35dY{T=nuRuW!2V(&|y4}99P8yl(L#h4!?l=qMr8fUr2~^ceqH>PI}U*7uB$}cBlUr1RpG2;Lt&OV&P!q= zBEgCN7IqbI7KBLGp_!Zb$hJSO#8y73xiuLVH4SaF%vYPg@^_suR@yA}5zr@?$tTB5 zDS_~n0w*2x(2NF-QxK}NRevu?=*&-utk=2TzY{94k_4SfOg2c>gB8{QmxRNl2do)czaB^3lAko5 zwQIGoVR=rEZIUpwKO7G(M|;-c`I)xBlG}!mRQ+y7TCZO@u*M^| zd;9iny!|sDnjk8jLJg>5U;)MTF9{U)55p(Jp|wlIm*slWqQ8V)0-!s6r_G!62M@xQ za|%un1}j7`#&Hm({7`^klIJe1dyoNvlFopL zH#M?h#sqglIAUM`SvepSdgNbR_%6 zp{Wz3CtP&F@RCmV?3!w`x+NCpDM`I8@6o;oNnJaJ~?>`Toza_TW~wrnrbH|ZZ@L@ z^l~7h+HmsJiJg9s>k=ECjHpML`POVOpCAQDD)?x`jlSPYpfz$4KS@XcVN$Y!olH0N zi4UB6_H&3(Team4$P8y`W5Tq4fN))%7(;a@k1BkOn0utC~iSIXvcJiNeLV6;dh04CAS zfq^%XnQAS*8{{DOaeTZ%_2grdEEx%dgh#L_!2`pVoY{ zd341-1qlU{zYlf*z`2IB(?b0G-^_}Y;577#>rbEtpv5__`?0M~&wdwDc1HJORb(Q@ z&8l0j0k>@QV_7at{M>DOy=V9_DLq!sO%?Wp+{@-GU=SnIZ|skPdqxUmB-JF#t*gfo z{ub7O-vMLx-(Z3$`+EUcTab>4jjdvW7BsrcQB$Ycx zeM)Pd-Zcgte*5v!tzJj7L^=B3w%ms(CtX1KzE79VPAnyjN?>xhU9(;5Uod|h-^R{2 zP1s$!?dSWP;=ecMeBKE#3p^Wo-Eu8vd* z_%bv8?g;q7tT1+VfL1+q zs!{3{UuS2HikEyW)Q#7Q&Km)Wv1hs0bTu*vf1;Z*M#Ug8S_(bD>88yBSEi#V}>p(X$zo1 zNDS0}HOe8vjFiL(DOp**Qqt7apB|jvjHal2ke6ax`fna_BEv$LzcwIO!~S!SnfvhP zJ7Hmb=>D?Z*Z)PTp(uO_grV!`ovkmC_!`7j6I5_6;n!V&^VX&*%Q8x~&$qgCPlw#X ztt>9WV*7!9b`_+E_c%P3_Ah(t#qc;}lBG>L^YJdKVGd9bGZNs3b-Trd6*(O%^3uA~ zj#P0ZK}%g*9b2=Gt++FO>XZlz%XIGaEknmoZ#Xq0MK2o$^EwepSY4mYN!%t0JeYDt zVI{U9kq>|W77~wAxt(2HJb3}5tDdJ_posQN*7V%*ktCh_m|h#Sl1N^f*}hHU1;mNkYpay_^Z5dHfd90-js z)#uf7<7W7)Mohj)Y^Mk)bj*>Gy!I)G8+!8~tN+_Kn;j#k&Khy79Etw8{)D_A+-yPn z^>#yFsGentsL$IH29~Gtl&yq+(DT#pWTU&lP= zgoy+rgh7>MnNkuYy_G^L{>3ZcMUVSAIXqF$3^mIi;Op}p zUvmv9K(y+NKA~T!=7m1``em2W*>(d&0I8%VW}>z=JZ&VRtP@XAkf6dfY@SK`cA>?gBv&LiI5o4udGW;+vm<*P9gS{}iAon!zG$l1T zK0Ag0aEkan6-g5=?xALen4+RzlJ?KWBJ<G8aDSNe!V;pmWM%iM75<AL7#VrOy7Z`W|lO83p9Wwq`frrD#HzwdidO?X|MZAdvI( z96?i~huw-7BQH*Ph@2h&7395xbIuk#6|Cjeaog@d{sGU5P3sUfQo!)gnJk6m<*a5W4LHv@3%zW1e)Yyfm0E zWyg*I{DQdw@%l7FD>;N`EsH1Zj}ChO>%0ySjp)eYnBBiH<7yuU<{;#X}Mb zO^GHUiqu&t9nU}C&q27(o3j@5LM@hT*nVz3t4IJ*x#RVLs9WN}I>uNTc6WU%8wKX? zoFZcCJ|+Z@;mHd}wE?x)ji@Heur(vVt427+exVCd)d(Pe!ra`G7d8V|Jd308mNi6h z7=?-A&Z_VC&d;q_#-aNL^wZ|>jM@z35d!RBt8b# ziiN!88e0#obLGFH!I;Z@*m`SW8PWM^gr!g)0c|*@TkZa`|>Bkc6lw$kLEL2=KHV%r`onG%>eX8*{{WR$05og2@UWjr)gFSl=r?2BNT~y`A3Irz0Nz>`j$zj%m+G+r^ph?S=em z-il`nXLOX5J}&uYnN0I##s5WIMAlwPyzy=!Uf&=54X;=HiT|OvNYR*hO-x!$ys@t# sUL772ue<*JCm4l)j~E8we;qJP)}>zT#t9Pse+ow3JqEjP>% zyBsiGYk&FX;giwShkwBMm3}_C)4YodYz$E|Pt_D;NDw1e;U9xxqv9ak0kaq;J!pMPB3 zeE#E|C;ae_i(BV^`f+jY`j7XAb$@&K^@$xn?i~ByFe3Ehna>I?n;BeIfnVG~<>UV) zhW~Bk|I&{AJFx#!U{yi-h-;3QEAi7^shh=~v4$ff;)Eti^LFdSQ{&O^TI>)NVr~N& zg-Jo}QT0)9!Vdc69*+|LErJj&0?3>FsHZAhVWl23l=_l zV;Ywb9V7e!T=~^3f3I5w$}*VT4BftJ>t~gvO_Rf4}1{Ali5^*52Tcg%2!lCyzGmSt}q|UyQrH{9hqlk{XFHYpposL?Yw7`)j zI!aT^l6Si7s+mI$$rQDm3-aJgGh@`F|)VpTsM2)XzrPOHLq*Y#bQmudvecI zt<3L2CdnCy+D{dVguRlt*t~pM^h-;sLT=A1M-XU56ol5gqc4Va<8#k3G0Z2zk;3e( zd53ZTb&tV#n#DzwsFak7UR!a_nE%HAa0k%ZXDc5lvif4TGg`A>oyRjc@)e{{S$QW9 zOIW64zCSe8EqN-aGdO?@Z|;g6$9wgo)q3+o!Xy=bF)rp6&XYatT599;$@J~1i>77n zP0@I`nN0qc&94ksg!tJ94?6F(Sy88O;jLP(zop&Xr;fONy3e(`vnuPU!oAG+4SF+iC|L$#rW-CwL#g6m`L2wuzlXW};FqC3;k2Rw7l zuQCpqdgl)0*~9EXfETgF$KbR-{?dWfIrk}dEqiH|^}|MD8pNvwyU}j-^sWW#-ivlE zINtgGLJ{R@9Zo zv7oqCHXolP#oa@ccmDe_X5W9<{0wG*6I|4GkNHd88_-Ixs&GsW%jgZOgJl~ce6=e2 zm4#1LEF#4%<=T-)`_0dp%X((8B-^LDRz7~97F^*ZMoWYEX2?Q|4<79f$}zqplHpPF z{L0$bKW+FzG=4FbZA|tX_b)#aQrQp72HOvZ1^t$GD@J9xeFdij^RDF6XCj**L?4Cn zLhD6JEa_I_HB`Hd%h0nSm&5SAX8u!r&CB(XV^}elfehokQ0?3^`m=Pf z`2@5)8nU2?maWz(8#qRZG?!bsi!kc!H0lvjOgqy{?Z8cHTG8A(Ou-ex_B+USJjexl7&&|G z)FqRdkIHZfC0}2dSo>5PN8r5@=~hbFx)bp!>JrWDcI4xEXc#7JUoALAl*W6f6IT-^!BAx z)%5muNk(Ka=K>n$`K7vrxdY7BN0he+|%9y%SRZB2gqO=Juu#G?XiCL6t zC}8H5f_anK*J^R_^;DB&iJlcN$3N5c(N|T$pr0lo5)a-@ zKE}gy`8EjjpEFkgeFfwgjbja0HhNM$(M(2t%@*3sdp-Y_lh(Cy`Z20U&FOw2Vzvl} z1CP8>s+~1{!;1lVH3USZ_=6>fWb>Bt4nEymAMPs!ssfPisPb&N21{~i&KJBtzYC9o$^f=@gR157} zY0a@kyt@u`bFAq1BNLH%8N!uI(JlkyW_gQk`#miRZ-n=FM^AMGkf90q+s0rKE?;7& zE_XSy<5jG=N6wBnKdeNed(I{2upM`A`$s%l0A}$JlQF9M;#rAIY#rtT#HGoCd`fHP zBqXF~l&w*SMH?UKQ3CQJrx!ol8_<2Wgp}|!tJA$7kEts`sT4G|%4%#fb9|N{hop2I zgcJS*+^Ay_$ka7cvPB5|?E=cv6H#C>3B__lnPGS4mZV^E*LkZu*OeoBi)X4AO3J-c9zy3q-b$ZL{w)JYptZY-6I(jaPht}d&@EzWDDHfTy}?!+eVcp`>n`= z-s;HM?K-K=8|l|7)L37Gasu~`D=EPZ&VRbkr;Rs^)HQpqUz{%Q_s9$TE>TZJPd-)oiT!A9(#933RZ5igexxfXP;Nk|ynF|IW=i+Z+$k0rP$&GxJD zg71~q*|AXoGz&73pAcmZD#U;H@$Y!QoG>3JhH}F0ONI4toL^vL8E(--05Tz_AZ1}_ zHaXO5kv|vgsFNkNg8zLgHRf1~qJ^bVwj^O?%C#MuN5Vi7#C0-K80i=`i|5nt2qW?} zR}5(*6Yo^n{G9>U@A^bUPZ@VPDwzxQ6a*WtZ4`M@eV@VRee0)-3_jIpiT!p&k^SgmD{Is_Q%wo& zLrq;Ib3u-4fj1d|nVc5dE!O0P%z_t{9}$D&myYrgqLU;oxj38Tf?t)CVA}|*w1VyD zr=2K3iBjLz$s^G(1^aRaC%`TcU)t|phA7zT_brVwdCd1zae|?#4a=DW%t_6=bT40pnAlr-yobQ88lt zu>>s32EofcD3Fa|Nfmk`(Ic7D2>1RYmZ&Uy!wJLfh5bmCLQ4#C{Z?3_zp^E7@XnZBRgYiwpo z@0NjNi8#>dVM|ZQnddahjNU9nQc1?3EQQ3RB$4hJ#p|->9ARMdt}TA z!fv2Tf}(KqbDf2FDNxqoxl>wfz7rR%FoHTAn;kiw)#fg~R?>VWl-A{>hf znv*;`@_GA8!``0}i*Ly=F8i3eTHhFKBhhA{%Em~(8qF-x=2z$__X(KCtFafHHrj<8 z`}9G!IY8XbIv1BolfT0znY%zU>Nn*bCwpTBp=q+O()lO+bkw%Z!xiSOLyBGp9EWTZ zUmCKVCE|~F1?9vP%`8ZS;NEJnk@5fJCNB@q{Y#K$82vLzQ78S}_y?r5(_eWPt9y&6 z!HsikNYiNpi>zB#%9Y-h45Tc?V618h3Xosm87;^`V|~zAbS6}(^2#kgJ>_Y;h=#k+ zMMy_=YiTaoW@p^PP@e{uyR_hv3rk<7U$UAdnG;|H4-G0liU4!eaj_IhS>k1m{-4 zvg=2~J!!5tl`-l0GhIgiVd3W;IuDk=B4qWHJ&+dBJ!_Xj20?o=i2v3rLx)d23@$G+ zL5yuhJA4`6zJ>k15h%m`5dhIR+1_Bg&dM9~3q<>D*M*bi-N{7zd8^ zY)6xUUo!mBF@Y0tr%W~Mi0B=l@vMG?2iK7)ti#dufKA3b-U!Q$+%>^ zL{b`ua1x+{63(3dfT>wSgSG|{f1rPL!m7o!!z3kPh9$g5JiaS6;B@cjiUXcoSq7a& zPVxYjS@sU&k2?ido4hfTKx@N_(=9R3g5n@om{-ephBg96LlSBpP_k$G@&*&V0(Q$k zqh-!VtK5&b+WRXqeq5#0X|h9n*sV<|E=#c;5YLCbPl%ODmDEm#t_wgFvor05?>}~1-n|)5e&JV zp%Ts97#H_Y>4~B{(dzpB>Bcn6^+KT5+_pPMtzQ5gG3|xo^eiiuNzlMHB`pOp_F(5ljL438O^UeMD`ge(q>lhi5+v2i)Ui6~m0c9IqOjVFI@ zCe(>1Hz<*+G~e1E5z9+cBCBw{%8!oVB$y#nl?`X~J0+949PNhgvSvV|K*U`%2wmN8 zLRad5xJw<{HECHC5NH-umAw3lJX5|Fr+$+xQKR;q1Nm)#y;Nw(`tY=?62V)(fB~|- zv*BF}0W%-+;9{ohRqNpcK=OTef%>99d8M4X?2v%wU#~_W;zx)SI02M+B4)_Qq3IGG z=?ALP0Mt10SiMeJ8&|Pk*-*w$8iySD5Ck4Vn)nBfJ-n2YYK&*90LKC?A;!3`sSK9u z&=o)_SLn;FEnC2xELVbuEj5ieUn5B(-ux;C5YY=Am_FQRI-K8_cO0o%Z9ADO&PdTm ziJJ&TVGD&tb6=h3@2x!xjf6xmkMs?YOh96@0LXa_K314Ot8$oCpmfy9+ppbwZ@5wE ztH&$%+HfalDW#Rr0u6I9)c75+!3K4ljo)NB15RcML6N(!sXHl$cw=?ZA#Xa@n{4Aa zZB~cl+Et#ppxY~Fn3v{m+0}GuI&soo@9)F-&Hw0%Q&=+1tcTRVC5TBr2Ok|;guk0a zKJTW0!hYVS0B8|TZNE^RPc`g8x!oGs62n1DuiT6jaVT2+P>O*opVJ3@Irhg$Fba|~ z+a`!DK&b;unY~hwbzcw}o>y9+mx1c)2N_fYSj@qwrmSBBW_|%17XbB=JPl_-h6bNB zElq#g>5|J1J>;}CZdOr#ce|X00XYSB*FF^@hs)DoZDaS7$Dj4kC6SD#q>Jr@#v%J8 zc|V%X=CUI1Pdg3>;iIyRHUMj1<_v>6z?1{lIqC^XX*L0_X21PnyyoSyFvm=Z9Z1db zn(ZefCrj1;v3sWD#hVSTqUS`3a(}0#l9CdxvjBy9wl~(Usi6T8YN%$X2WI!AD#L%- z({L#*NI1fOcn4P@u=b2WqN>5csCp7#(@5jwB5P?PUV-@Hdpg!^z|ZpPOqEe&>8J0d zgESZEddY&C^uH_~5C)#i+ID8FAaUO}e4UOD7;rco+;g9A>IY8tVbZIVM+`*I1d8{H z9NN+;i1amM5QfiiHWxnFuC7kHasT*ZPq#z3dQ7*a;qF61rpq=v?SW%4Qj(H-3MEVn z#@?a&T&;PEUHE)m)6#^b%-GXQ)XwqjPs;A6ttA?D;CY6Ey%e+hr7-aQC8q`ELUTt@ zv~7dnveW!XD)$?`%6!PiPiN)836E07%twjP`k}Tk`}yHTJon}t2e&ab-i9l)l5Sb^ z$(E*hMGFK5rS#N!n|XOL1Vn_YjIQoljuUA;E(4*Y$E(aD$H<|vD|6N!zg1nz{kFE@ z)XxZC4`gD!Ke;EtSpON=7%A{jFN?dc)9rJzvd^~{SR@6}mBA|vXMQR@*!HP<&5nXB zetKPP9(91Z{5!`qCf1|IMp!|Up-Y52v4&@H3p3u~Uei5P0OEWp4L7sEp|kq!nT;L8 z-+WZjh1irTn*wZVffY46N<#|rP5=T0dS!57^<$9Zw-0_L+))>&_U{)Kft~9Sn{fj3 z>bk~@6UUv|6LxCrW3jluRtBL+gSOzi45;XVIYMhq9k8E&D%u!Y$+4fOPiKDpish&R zDN+#OXK8D5N=Cc#yDO`)F`DYwAy(uvP-r$aR7pv@G*uxEAQY|W9GPusL>T&_o!SZf zBw$=!X+0&AaOOZk@Y7GesdH;XJEK^$gL<|^Fce!@D33g^jAr=F1l_EUN`3LRyCSxz zxY&N)N42Q>(zHv2uS-?fAp5)<>zJUmCPNyu2a^R%qLf&69v18Z*`QLo$lBjsUg?rc z&*@&mvIFbl1Lz=^Orz~33`vY8VOz))k7q_B0)(#NL^+lUDu z9ZbeIv}Ic#ht>jRUQ83$^lx5kUl|Nt?d>2DF(AQ~Q+5(k0!X3LKs z4ZwMnub5BuR~kiDuPr$*?30;{`}S^9hLQ%j#u)4*z{0T4_qMb0j&m2eaY;!O5MkEL z@}$)(#b}xdX)9|TzaVr5sHBUa0r;ju_iZxII&-$Mt#nJU2$_F^niZ}QGiBU!+jFuF zBX{KPb=z#@V3U=XyPhq0RI9rNKRqb)TW=n_ zu}E#B({_SWO~FE-Qudmr0xpnXmJ92fVX+2+N;rgd#UgQv6s3WjvFTs<^2*3AypmD= z2|qMYq_+Cq47QA4^kkJz_}f;kE-|k%!uUvZk6G;^L0y%6SG85=>$O-l)xp5zmLU=C zT)?@wlIkz`sgH$vzrKr#K7XB|h^fN%GNz0l?^+z5^&x-|Ea$XzYNTvcLI@M#Ce7IC zy^=@qS_cK_xqN)BvvYOX#C-*ALviW#YAEz+ys)wn2&L5UAMk%A~Li%3Wg zxX#$p_c1_W*!m2=t?#d$x^ij1j*sV^5Tv5&t0XK(PpOQBv>7#Z6+V#3%1Q0Ce!H1@ z$bnfiowo`uLWbxu?=yW%D z=iV&&d=*JGwRm*-ayNY0P>o&vnGo9rwvHO$O$?#PzH2`J(98I{u~@-;qb=rbnK zo(LT!+;FZygtT*R#!b4i1tTXFudi0=O@z!_*0)yE?{HN4x@$IeFVuzc!VVqkMqz?T zyM^#pjLyz#r;eT+K(Cr#5lp(RD`+{Mtn30*LL+fy^6Oje+ z0Gpc~Inht4v=LsD;hpcHY!QZ96l~gF^ltrD9~`1LJO-d3aA(y#4!&dS z%rd7t@hL3{D~el^n)sdOUs82fMpPEZ)NVZ9wa_+CN-@25txTZOD~C1Wd@@HTHiw)h z=YT`dtm0uE1#U9j<>6nW8W<{`Q8g<|ek}2->vN`0dR4wNiqCxilJ)`Ga6k#j z51=>})p_5OtamQhG#iC0bQi3@W2)&e@{n-n_F+fdlaLUJvuCyU%4u{8&j^dvt1U+w zhaW?YcUj`^)mqvPD*^%*ka-@6jnUXL*7U}1#N)SOo zOTzFmV%&9#b#E8zJr_q-Ja*3NwD$XHYp&1dsO7PU(oIrxvwkw=+qe51w2e1aVC|4V zr)XR!U}EuCK&q@8A}LJq&fbxkGrW-oAh7}Yjg zf#bd*?XNY}xTOv=fxI1Qnrg}6jx$VBlHURf-E&(|@xC$O?Sk_K5pgap$HV!K9TTe` z6m2@Hk47gtvGd4-C%oQ2UUl!s^Nr9{b~~>>(J2CYgTpUhfrzv2xja82)BZFf$#b~D zgmYchBUQIHj1MEu1gzEMH9U4cB2`w0S5VdC2IGCKIug`|BC7S;<1X5UW{}^iua=gU zV_lJNPq8;M%yu@$9wsIwWk7dg6G;9mbGRpd)vTG1Z9_#TZ3&o-RZmaxsn=l{+uwwS zN;1{_mL%dUJe?`Bw{Hs(WrVYWK^=nSnPwFU3;_G!08TfA11twSen?9Jx@gvOrd?C? z4Jc#MvhUT_GAq2sGmU#nmL!0-Vz@CkY>d?AgAWR;XAmX3L9r5Ne*G#phi__R;1^M> zf;RgDeEPa9bTWl^{%+$-fbVg+yOSC-H8Pf?w15)-aaE-hza<^dsdk-FXYUE2jWXA2 ztek_s>El&Td!S1sHR75?>p{)4TbsYSJl_8Fz(u#s(+2*_0ZI_T%o72DJq~TRt91A{ zRV07U*a+e_K%^}jw-T>Bd;T0EeQc#ymQ!ES%-B4ULj2(Y1@%6OyXWVhaOGA-Ezz%GT3wuU>Nh$?h>Ocvui5%?miPqRfyZZZCBcd84_ySO?bcSM z$z#r$2S%xH63l5bP}CN;Su?Ji>ZTcA2{a zTHzZF?@k&z3x~sXgUUt?kD?DmbzyBq0fiHKoIY35`qTdTG@vo^ zij{mXT-Bfs@Njxb-h7D)xR~qj%f)7CWT2zG{pHN)B3v`lj8Zxw*Nc zqv0qZ=7IPyPF9YCQno2NI+{GkB@K`HHo4&TSI?`fZAGEB^X;GbOi`R>y2T6ZJ0>0N z(z}$*VzEqh%4*o2IFIQbZHAzVx8v$_4qQj3T2Ytr>XiUJYKE3t>e?|obar8E$jFh) zk}q1UrpIBYdKfQ<+>D}L+P-T(WJj?qWncmOi`QeemR@0pKKW~l{x0uX0M4nlxIq0M z{{tvuaj-8;4I@yUxhSTMHOEz0*%%rLRnEEaKlY$*-i$9Qe&1jM*cJqsHB+tQ);Rll zXV8Z-g->lU?_WAMIy4anz9!6&6?ZMnkbq8F_(V8~cVq46D!8zI=7>EPCIMUPo2sCc z5HTBgPj0+IP&C#^btT_sv-<%!J~k9wSdf!xI0mA4`?{)6s!+hX1mDrahiM%-#deiz zt-rHo>OCjzlM)oQckW&=5qD;;X?h=6i-n=*>+UGg%14}#WK#?6C?T^+h5|XwM$PRd z)qh3Y-p+2~opfn#n%~y_9PCV$nt4#!%zNegpVPJ%LRGMdcB%b-9>UDbdo!?qD{Ro$ z)E)MAc~EX84d_cvZbA$C<0M+Umv?QHXo`N>^qcAWzBkt6>qmE}GWubMui{sT-AGgA z5~ikL&p;Yivc`CXY*yjYpvEe2V#8OPY(<8BzCF^o*LP~>9*7lvxR}dwTWVgmtNcFb zgga-=xS|(FeerjP{TId+5Fn8-LFM}3kckPp#j=M$Z;=h0Q|F-RboJxUXoIw)g*&IO zPuDznZ7z*T3x;~QcM&qNEnM7qwf6v`qv=$W``U{~i5GmUYHMqU%a#&HK#yqZ4)tc$ zOA{JguTx4+?EG{f$H=$eu0oRTAEu~BqHKQe&2`w!t1aulZYCBLCvy6<6-6DBdkZQU zktzL^tl9dtr%mU-PWKWq^zb`&rxeqo!fJIH1hiw{NrDW`hrTZdH3r zGNlzf5x{kUrnuIQsjIu-O!FGWZNa%dAxXdW=^QfG*d{VNPQe@u0_9QwbY+kXSqGK! z#gSjd$YnK~xCAJFBA3OgvtWEIN@ia`_b=3&myFc>JlF=wDo$W7_Sdi`WV;RCouHnq zpeOH)h$C5*%WkZ3N9ki7hX?~;W~eWM?b{bS)Z?5e3EW%(UXZUxgxgaP56&0O^ z`a^vxs?&*7RHuZ_p9Oc`Gk&P0qLNwAc=*6D;M8Knxo5=9tmEVDZSVHx=3CT%vAnL` zW_WnokO698Buh5Kx)Hv4VvG7**a_uvmk0zSoNon}fI#$fG20auKc(l~mw#$&dnKlM z9~~ZX9B%}yP9nT_Ze2gOw6I&Ibj$@d1V6t|$NcMp>fHa$i;=%hcKuRMkoIPtH(_pn zpC5PoU-zi4|L210KTe?FodKC;t%CgoQ|@5M(z>V?HH0`ykgAm6C#G84MJI=ZQifw+qiiLdt6M&4yiE z40U07C|?5pjM9T?UT$=-K0b&i&(S9+zocaa%Uy9|4nqYn>PcZ8K_zUcQcIL+N#EK6 zn|A*6Uovw={o}O$q?Pb$TiI$@Njj@9GNQnaug@7~M*dk&7xb*jn4h6{jn#}CmMpbb zRzC5@RmXk2m|G_;wAB^4048q8G70F zx8`C)f+YwparQdtC5G{8;M}DXd&K(lAYR<3m2QlFa*D01pZAtL^H+4_>T1C@p^ZJ1@)mSW6#&n-` zp8V{Oh{d}e3)8g=;9$ptek+3oHk&KM5O!;Tgr|umR)mM~9^=KLnrPgEkl7k+_MVLI zLlC=9iu>xr7(qq|B>uYo3jMggUI^IuEbL<(MQE6-3-bv_t&#*k}`GFmSnTJr?Gf(xxQF;%>`ilnb~_8|q_iZQ+U zNP%4#%!ey@-kChU043?z+-gC?`f#0UdFQ6SlNaUa)JeHDuOAvo>=P>Lv@=o=n4uM@ z1bEzMyHPU8tJldHH+b%?@9fri237?`Mzykn5>>r$VA`6T z+k4LsF?JWIs4{aXC1oyiMaYUT{AKhl(L1JL*X18D&(5^k#Bc?Mu3Q?eaS#9N#q;dI zp#Hm~-C=L`@^j}OZtc%+E|&a-`Y9S2(~YH$DEuU4*+dJ_=n|F{TIWg7455pjLbJoj zu_Mp4np@xoH#8K@s~_=t*@b$@-*YWHo0pFkZjI;Y8)fTvBqbNI@>$ zrmV@xe0A-W5u=O@?}X4^|99De0SjkSB<=zLWadzcyUg!Od0B?Gyg>QQEfxpCu4%Y| zMa^2!@h=jxQyS9sOGY=h8!RaO+oiKerJAvAW<#WhgsN_dvaL`PY(I<$D##T`)VF#y z!!D3;^@fJOyE)b|om0_NaY@lcTVBdNv1Ey;(^ z!Jt>~IL!xc%c8O|)({dsgGlWcK&nN=6Mt(vImbUlz(iwGiyb+}svc+`SL2}${D8^m;R5bYQlk&{|n?_r%q8(`Mmy{it4_su#9z-fmQL zkaKT<%7$fgsESwbcrG;BPcX6OyX~lWz-yT&X8o?HvAiNK)1aw$(QNa+T-)xJMUG?l zLXa|C^6f>D5wZH*FYe#>U|3-U%d1@jJC*fhuj{LLt8Aysm`ZJNWIDpHOBbs%T^BHt z@TX@LLD->l+?Qq5yK1cTiyd21S&huC3e-h@wL!PPbNlwj)}G@-nx2cMAmMKcD3%O+ zb#dzXeryl@ykYOmzimbId!i&``&TkHIMzo2LWi83b?2$`?nICW$IxiTo+FNHDqXX1Js`_F_1-$1y8JM zxkxE#*(RifrxV*dlM zrF6AR+><|x?v>H@-dZV?1GZ8Q$c}vV;LKmAR1y*Ia!t-_44E5X-w54|zaukNqs$Zi zQ;fBn-=g%Xc7?ghr%&Z(Zl*q;A9X!yp3gU#W6H=Ze*I7d|Ec`M@6${azRM2+`2WyO0Wx5 zsmx|chi93vAi7MwHMfd{tLI%PVYPHWK?$|88tJWH4-&*HvE~_J`c1t?0$E#APspzE z2fB=);x=ds!L^jh5sgq0`7x@Kl2Hl%%qM6 zqlFpULRGrO`AticIcANxd9-4kTlK<{xJU`_b4^;vje^LWPzu)%eFH1}cCqyc!G>13 z=Vdp?=yXnrjP}Cs|C6b{$6de&7n`h!&aERDxk=A&3nq>=^GD~kds?dWbPjP*f`1}Q zT%Nd1>!H?2*ZCO?B|2i#`4D!b06Hu-`WEjEjndvhHk|swi|0dLWtAEUQ{3DFaIxBx z6$Ji!h2OCgwR_9!;W7yiP~DqYj#?rdfHGUdtD2GGZvY)4yv4qrOT()Y-0)nB&Ii(h z0-8S{KwqG9h4mVWCbfnu>>~wNJuA(TQoa#fXvZx<^?laCqOj&Ww~GEB4}FOXB8VsaqgM+Y z0jz#ho72!nDCzSRswbMe|Ijx}gLX-rp{67OJy057`jxsGRI!4EMzF?9T1Fjdr^AW8 z1n0IsGdIyM4Giu_`x$z`$^(GGl1otxCyEhs5^lskdx|LQ-g)F}|ai zA7(JmhjAvVv>h^5S7jaekwb7Ml}X=91_<1aA9>HlN60GEs4E~=PslV{Wb88qQyc(O z&vD_?Y()fn7wk}C;*>Gie=z#e-T8Rj5_DjUI@YWEK*rtP4g-N(W=+it>=6bom^ zWcY^d92}h12YiUV<#TI@Xo9sGDyRhAo$N^x5OC_vXZO`yWgYd>s>4?Y`2N;SN)McT zJ|A*Q*hCLaTj0}&;JHRa8XP@S6*B1za$MS1tD`O`^uQJb#*1rkNl${<))t-D*k`O_ z%fjAa6O)Zz-DiZ<-7f-YqA2G7a#J*ti63qk}I#2YUn zU;TFMy|;P>4%Y|l8mg+cEO!DjwfZfze0g+2($~C7!PGble={`1R6*7pvtTUJ1^6gU zb0cVTEBBMHU;lN28sKV23=B!x{C+}!g$SWp48W$`utGu*&Pol^qxpnhf45v|EO9V^b-DXupv6U=DrH6S|t<^{iu@wWRK-pEj@wXYUd(|$dvq*Pho zxA;}$A)anIHCJ>fD__;3-Z^Rp&Y>GTa>wyA5EN!nA{9e|KY&w=+CqOMigseu6Gz<% zKNh^2$5{Aw^0z&}AbyhjF<~8@dHV@q%baN@=dL+9BSz zEvBcm%D%CgiAF;hdn5Mx`|Jd21KVNCfy~V_gdu2m?oFdM0H9cwG>mBX+hG6sM#)wn zsI|2^+Iz+uwe&l38GH>Bhp~<)n*;T5LQ3V_)uZbb<%Pl&JM~SIbh>^FAz$fHX+k_uTN}NrG zPZ$s6qkPRi5sa-zWt#XIx|y@9DovS1KNRGtU1~;;@uP!04{WvCTKbw#C=oMb|3lw8 z1jR*4O~aJ?yw%aAh~sb zA93M_zh>DJgV+)|T#^x!4@)G_5tPMs}UL;(@EZf_6v zUbp@kLAW20I7dFiiOELmX>$iokZ12)^>xFA;u+XH8~NH6i~!}AUw0<+!)ZY6);Lbq zc5eOME8$jDVhC$F)QZmgx@V@+EhTM4Aw>$Tc3OP3w;dZw&!91$ zY)G(EFOX<692P*b8VxQ&RnwrQ9}DWV6UZ9c6r~i*LgA~C?oDEl|AR`9;_O8wSw;C?*DSvmpCHvfXPY7=>AYC8&3GQrVIr^gMga;ZZP0F@ zt(0A-t}F}WmYo{CAd`Z;Ip&Q}4p8ULt(mbC)EY4!2|mOWvpRz;Gj{?d-mzYw)J=2LxT4h}`)vS+^KD z91UOg&wUk_ZS`yTfZFV?&O=s{3tQrLQc_q7u+v{EIO7>XSBRz;wC1O%ArW@>1eZ}J{$oDB}5 zNBTgifucZ-tfsJy@M<7XNjx?}Zle~sLE3mnU@eOcX`W?N;@SP^ugg1@^mh4yhAXs= zYe)fLYcg;g+-03{MG_x-?Opc9ExVR-CkNDwC{B0q?|LB!Qbxq0hu41IR`{M_Hoayv zW7}r!zDu(-Gk?zl5n*Q=ln*8yT)T@;(NC9dkm6lm4%k(*Ie}8Z_oFkQ1B$brmA3jC~ib6r_n*0$WYS?&a)z%wsV~rX_(b zw1fLgKzpQsS)Ju#^rV=fd0*lDkTEz!FJR+|TGi60!x2W&_CbE>me&+b;dRdtQ&S5l zY8B_!q3D|e7@Praiov(H$?hDG7Yxmyqhh4ARb)NVDV25SE)I+F>UtRrn9Hcy@kCFp z*Kh#a=FU@`N%-RloA1A=XA_w(fODydDP_h9yzqipkjjWmgsQ@Mm4jhq@x{0elR{Q3Fg2OfIXS7C!K<&zzCKi{S_4Vl>%cSRw=FPL7H$qZrsvv4qi1w z=1D`=6}OTSLvBTqlsa!>(!0V7s80+UfjK%JE28>p*Egj|QS30r<`1TWEzu{8{_p;X zHrQVvvHI*Kqo6dXRUQ}{RnT9{cC1_5eMX3Q=^Rg8cec$$Os8w4v${Sl;OW3M74Ibs zkARb_Zg5$|OJv=u4G^2$6(&e5FyD|%|{#kXX}jDL9Bm*7R9$y1pCld{)=oRn#mcG>AyRY8MFU@D=@ zt=ui<6S$pDi3A^Ih%{u~_kv5sd$y6dCq~xNCWa`h3M7I~sjALDtk&=|B%y{-uS@sB zSYFC6as5gXLo;YLsvbAXopd5oQ9U=mH5JOIUz4;PH{1F^VWs{zl`Hdv3kH&<-f*r# zm{k#F&H@Hy*@4~@hM+%4yZEndVOoUj-QYhWAuON40tR8D93c|92324(gRVhqz+412`{iZHgMaikU5a`pu(G$sCz1-7?De6g<6VgcXFQytOQE2s7(`Oc zyaZ-?Ens?O(8f+3*}SzuSHu8uu(w-CLW?u3c#&*lIu^4#+N*SXm^+OAxvX298(3== zwwyN)+SoWVAII7;5lg<|?LG6t(+H@wU96h#c9p|4eL3Ia4|&eH#ASzh+1hp^?nqWa za^%vnq52KFEPW(kkq}xoFL>;luQyEBfm7W9#^X1}0y~&)&@t(eB;K ztG3PEi_{PaoKMQom5XOKf9EqZ>Z^mo)jJ8Q{&VHFhRiyVL<0)3q&0bn_j&e z;pEjB1jBjIdP$!{kkk!Q^yQ+0p8V>Vy5l}5HX%dW%+`_kv?Gp6^+v{RA1n`ITuVS% zzb8qV`u*-4h-s$@*;hri07Q|n8V+c`1KV1Y&jw*51g#&NzR|bZ7Kr4M%Ds%N9Emc6 zsycPc$?nOUO$$;t8|P8f`{b!OteHiiA?UKB00!QZFOEtD<}6pO>~cNPn-7oZ)y$|*n- zZZIB99JwH4ndaF#LtplD>*xi~kb%E$ZIZW-=a?-esSsd#Mzt#H4Ka39Yg3r!u_s~D z9R#mH;6kSO6e%N0&##5|jfQj|MSKg&ajcVT#0E!NfMkE_>6Q&^!Ye&XRyf zlox`XR<0k0tf*D_b{ZiE?A}uv?o>*yf2k)wTF$2li;aW+!6gX)NaBn4Y&C&{;1lZq zeB2@;oV@zf!`U~Xt^GsX5cpo6A2hsP^~0F9jHQ-$t(o$vAJr4dQ`)+TvJU`w7dk)} zOZD5TZhV_QnAI^zl(j7Hr&UbA zLN((?;1CDf*iIJHn?5y76}2S<#^KGS^d8BZ{@K7xbOUAOOjN!f8}`GG3C+ zI$Z30U{wk1Y_?+=y*uX&{2iu93D$iExDdN&djR?S`wV_!61SC05*+T)UAHF|7z6Xa zRH-&zDpGx%iJ0{)2@7PqQE`(b?H~b4Jk~o%y%` zprGy|Wm=u~*In0v;)0{q;1pGb`(Xtlx59so&xVHvBET#jjaPq*HXcQn}G&&!157G-GQ;v5-ub7~I=j z5{A{+)iTbm%`N0-H56ow2363|`0*8gEHnvM`4Qx?b^Y)lE`u~LDB1%h;lhG7L=RSq z@F8_`Hb0tzfl}cDwdwis?S{v?jn~*k8EBgELMm5j!fj3A+T7PJHeZ8uoqlzL#ilvx z@ojD!A(bl^BWz>x9A7rPiz)5>``>S!JZDu75v|m%4chAbv+gBqiGReyV~0FEQd8^L` zG|`@{FLZ`tJAdNi+y78eT}FXQdB6rM3qXf%3>e@?gSW#LejqG~aj5FePTVqaS3KWk zwqz8!yXA^CsZ`zEq+^xxOD?He*N^3tQnh}|Buwm{7TnyOZM94F**mS3de=0m<2~Q7 zc!vUxUg5x`nZLPewtblV-9V=ZxW6ElvuL5@5Z)NehW&^{n$@lkDV7Co$}A03!g5qE z^?biD>ANHeZC&zf_4^j9Xr!Tm7O6jAYf;zf8UAEXS;0m%zIW7r6k{nL{IhRlq?ms8 zS$0ZMpW~nGyq1oRo+*v1zJJclVyx;FXkcZIXa%IKPLVMV>gAQ#7|)JnbN{S5y5Ysx zOeb|@$itI|yfb;-6i?nu6;DY`#roUVGP|JdREaz(}c+&%JgUS6y5{2#SY-r>pmU=ve| zs=12z;|CU1K7FJ9V*=poZuNXERB;b)9AasPGsH@3mGS@*(tZDUw%F01jS^MPBtt3v2L4jeouvwWQXB(=(sHk61T3TbW zDXx!B$~mx$v zs*d;KVoGg`u-KlA^xdKvQ4{Xr6sS2@d=vJ>hn1XdiFT&*g{bN+$I-o5JWBRZNdDlr zsBKKb(&DG~Ug3(-o5M}svo61%hvJF7z4IykM_I(8ey67PQC3N>_qVjK;aqT)X6_ZR zayIt^w5Fz}r1jT(@;hH@@or;yE&nm!dRKZypA3pG|NVgP?)12>Y7lZPT@nV^fmJ*x zce!rCcyoSGd0TPSrR{LaARHCeWh4{UC|xyMG7L2H&hD7+Vkdem&a z1Z0KXi#Tg!Y*d8Dm?<6&b}*JZZ<8?`;!4d%oj?(WvE2Pobu_d|l-!pp$}b|K9d^|EHt4YJ1I=*#ai-+{(e~)^o3FrFg@d`P0P>omb*nM!Uf#X%7K{K`dZBiZvw3!- z`EJnE`EaENqrv0KLuG+QUnx|JunIA#-d-efh8@2@dvUH{+rC53&WI4by#mFh%a;W- zi-84N%dIDwHc6upw!7}e;8WL-eR4M_0X^Mzf_Bxv4@j)iPu{1!iQk@Yk_xc`iJF3a zysvF%=c}iuR~j17vjjMd23BMHf#S?@#ofa8_Yx26A}=lEa~@axFHUa8$CP|NcoMF3 zL;h&0Cn&hjH9RFb8J(N^$fe({Q!p${8_my;lsXn4^*_Ezv$6YZZ#74!?AZ^2+MTcB zEv;>OAYHGIl zj336&1#HsMVTv)vCf?qkz}t4!3+d+!Ekhrcw_uzPcPp8Rd60aOs;3bED4QGAp=c02 zzQt+ixS%nZ}uzi<1G1zM#T5our*p*imLlgvR2Z3sgc ztiT3X4blaS?;hHpruq{0CcqRX13BEq;-`y zCTS_EARWH+DLg}Pv1j)aVdoRQ@6b-^{Af`mlgOTV6B|gC^!*Kks^uXML2$0I(ixQZ7Q^67=sKDbKN||7_Etim^c|TZ*X(9cGG%MAC*B2|VGrk0pLcK|Cu~u!1daz#*b53+ z>s`;U_iER>t$do6Kx+u#*$T%R+Snv5S0Dd9sGV`oa;4@FbF>}qkera74^UKl)cA;f zrED!{vp`WU-@70q?_(YW+H!XRMGq{ot)60Ip7ZE9)N|01HCReWOD^OM-Tvsl*F{H@ zTK_n~wWzikrf~U3jZvd^Tqu_DkO2#uo>T~2I$^okVR){#x zI;-)+trVKz<54MctJMfUP!=C3$|7=yvSTWsc$hcj?OoRqE?E4593#`Rf%P8+-}Yb0 zlqHBn$(FWeKte7}d$E8NE-$RiQvu|dfK>}kr=i_Oe|(xD<6z%n ztgoLxKhHYt-(fJ2q$8iR;qa*6X|ieV+<3kA+M4BHxx29LK)gqt9*9HFmlub2EIIl4 zxAF=KT>6*gXK4|>{{E}D4g7f8!w!sC~zRswL*fnyD4Bb4xK7=Ju~djx410p zk<>J43<48U{xmA>B*e+$yiPz|kQ*X7i$I7XFR{bjC@WWQ!c!$&=N?l;Nk(XuI;fb@ zQ(ryIvFqzMWyaFBkI}`&A~Nvt73h0AQ<(%hHmO$l`}c(zM>~;Xjooe)BgJPjMBr|% zeSc}C%9t*tfLxL~o;zy_=gH_O*k2BNSe~OMKxrU&xW5Y)1JMF968xe2!;^}8BjO@Q z;YaFomjqnfhUh3QLmbqkOlSP4kC75kzZL#o;BdmTAI^h7N6kfzE95?Sb#~4K6dnD6 zkDxjOrvl26l#+6RHE0h;(6I~I2n|;GwdsS71W<jy zYMQ6U5u%hKYLVrb5AyR9AZNe>y0gD74%Y2232rVgN(0i}3s`;@9?L0fX5DBq|0c0p zES!!-PWRlU>xKhJ0$Z8=(ph%gN5>3`v;oX+rXcuT`(!!!7MDyv`PO{RegX=`yvg9;s4XQcKI-1)8y>4w0bi;q7;?nJ z%41@D>~aezB0`(|bEV|t!E&GgKfi&?KmWiy*G^HERkK!(ykrbE7?DGtDn9;b^Sr#gGm6!mqt*VC3kD8)`icA53#^&}0a6#geUZ>= zN6B{3cU42>koj3YD;#k@-ZI~KeOYNlR#xf!jqf3RT-mblwRi<3zpVgwW=_t?KR$S0 z$lts=@C~CMPI2tA4Qp3<(OB%{G}L&tp0+ILSVT6RDMA!4EaCO)UQ z=4DHZK#ip+GXKw7uMP5+=A%ap+fQ^GsWcVFnoDhX?}hALk5t@$iA27=uIQQQwz6V7 z_wJDpE$vJ~+Q>O8WWII9@Mq>=g`?M^M`WIsUo7!SDIXdtTLftOnf|!ws)qS3jWy*# z5d6DSR4wmry!vbgYi&@@Slen8=8LfIEP~1@_U*5PJKtb7@Q%`EUHEChzK< zoqN|Wg~3Hb5$IxfM&BJ4mB%PV;7#8`&X^0i~4TU)TI*ou} z(P~hwBQm1NAl?bd2Z(j2%4=ExPRm^P;5>-Z5}TAC1L}bqxc*GfqRo3@&697M(dsJe z#4SmlDDRY6eskD&I9B$y2U2eT;L1!}SVZDs-hpf1RacW^8OqbplDvV?4G3!BTBi+dn-+uy z1|3}=3z^jt>MsdYUmT@vC5bzb2E~!Nby3NSqSI{^ckvD#p%Pj-by+VM1!)J%cI5#G zmiPttY4Ttdx7TN9yA38LVsYxBM=?81zu#`5&Le%bbz=mTQt(H{A6N z67N9+)4vi_TnDSDH}ac-D}JWJzPJ>P{Z3F*{q&6VR!&;}97H+friMnyN7vCU*%FKD z_JT-7hpq`kz@H!EPiI_tqH2S!r9)eVPn(O|<{Y5+)P~|AdL`o_Sk{uKbzXtn>KV*?gz$sYh+MBqUzH zyDW8%K19_t@QhkKN7M4GnwuMYOKwzrwi9ceg8YGW2FKBLs~(Q9i_-)%4;R@Xq?CQbq^cWE7z{ww_Fyw>prAP zdZC=maql%Q4V|P1n#n;zFlS5V18Q|Gjcno4(mT%rr>+qQzK_Dh5DWeC0rPC1QLT&* z$LpffWPH!sJvBaiDN>R8T<)Zxp!YxU#6*3N(UD?~om3!>j*zYh|4NI-FFwX40cW&& zbeX0)cmP7t>LzDjcS7>{0^T^w% ztD#?4{FriF$MC2&Sb5I}v2%2`z)mX;X^J<`&@$&-i+JIL^f~*DWP4a;bl|%63`BA9 zAmom0m{kmpH(!fKP@MXI?^ZpIAjjKHI%yAgms*q39+qx7^@i8TjJVVUOm)bh&Qcd=nG%MeON)R(M$%1I3Ji^gBKeZEa6q&j>LYGT&GVYeF*;JW^B zq(MaJP(Nv~&W+kS>js~WeRVTjQ zxhJI4k)ix#<@7TrWP~X2J4ba}1ZCA9>xkVBX`KX+6@YQl!?sxRF8o*6Fgbt>N8 z;v&$?Zk2&OQ$T-x9&Ok$E5*OFsNT-HF1yY@9(F8G7pwL8l|@+|fxrmr5}-s0t;wdU z(9u^tq%XHG4t4uXp7pC{|3;efo2eL|7|)}v53XT zL06yrnHiXbl+>ixnJzdS&QxNxeVK_VqQs>vZ+Xb~<;%ZF6SHbCr2EJh_X?YPr~{MO zSS_C==f@SpBq1#=se8dC(X9yF?SrfvpUofe^R66WgfXeGIie!ZsPdK=VqLt~>HIXUNh z$29K#x7n>f4W6f^%L4DSSC48tbV9Le(w<;y7z743&5N{?Uu4$l$Xo^}3t*LweWW-VvNnfso5?`vQCx;}9e{z6S2ABO_x#*G{JiV8BCH*VaC zxN+m=(|cIplOp? z`gZfxnW$cg2{R|>11>Hu^m0!J?lp%(51jd{b{Q2XC#P7r<6IPMt>j_T)#^i5wXDUN z$$g_{m)nAZf?2*#h<(t@t1F+mG&;M|&AG^1Cby;gr5eb?&FUg?gpd`(*50>KV&{IAl+sb>LVPteHlNA>9^=9)t$;hejI; zIq$N!wlUUbf0o5rT3S-A$ioMtg4%Rs#a-G@HDvz0ZT8OC_qhjy@Qy2(uGe?R;)cbn0wrGzJ2d#_uybTv*!HwnqhR;WTH)le#5fX zW)fpp(4(`tY44P$&u!~{plMgPKYS0me!iK+rX`2fJVKzH6d@F>nqITN9jC#hd+bR4 zdb;kedPGi_+Rwt{>5vcduLuYTygH+3x#-E%K|j{~s%w#rLb1AMtO-_^vvZ+G=l@0y!U z=V=3zfbJg!Mnkl^7#S8HJBEejmPe9vEvQK7SHr^-U~$@lPIJqRpLX`=ejXRRXu>1z zid}SZxf;afv!{Bdg5rSx9hBtySuAhrSfO3ejTn+vw8;;;j22A8CL$-7*EDXV=y>y1 z8d;od>Jf?$QLr^Q7-H>i-kSD$V-kP`*B%<;xpN5Vc6cscKU^+LtT~*-u2(Ssm07P; zE?&E=tSky57|yfAvUg@z%4^ptqiGoV<^h#xAUt=f%#GknpDyFRmU@{@o6^k8Ox%Z! z*< zUdhPICr}lMdM<5Qv^1B{?mX~~Ongxc3*aFuNQ9cv-a3nrVErU2})*jV}h8szDijg;*^ zn_e`?(>P6YbH@cRmZ_?ZT+)7nYU{vVbIr6bb&~RdC->#kg=|DJ>knC&FZa6i!DVw3 zcbcx>?lg(MyRaHPUxIKu-($1dgRA@G_=r+D^y#vDYzVS48#f2ucA~}Q;o)J*udNjV z=NZ*|MkmI_#XUKLTkUF$!WxPf!1LnFQh!|KovpTr-}p z7gOOcN4hVWVjgK{(FTi;?V2aLKYGc-4SIDBn4dg}Sbj)L8>^1|n~T*v6)o(wEp}_? z0Yuh7AQLPB)Pd)fe0aui6z;AEwb=m4jHzylC11_ro42!-ZP0?Df=u-8GNXpa2hp>@4H z!_Ro@R4gZy6L9`u_wd&TXR^R5$^;U<5+kbv1c3`#Qx(O+>&yATSY3yDx}ejo8cBqN z1x#=IwfzR%fwnCa#%G4Rp*UQJe;UHzOt!r%dJ$n;NW5U9ixNZ~1 zlQ^#rTRqXY9mzA+(=@cVr^3u~fPn$cU>f9h&w8YA@EN?78_!=UCD%nd=?U$a2R@&tUw%&kX*IwkA(3Uq z*!lY&)cUU5)|p4fbANxaB^H$h#Y)Td{PvEHr6RRV5mn3}y}BQRML7i(VeysqVAfK~ zNVChqgo}XvL~D;$z6*=TMY?$@dd1(nKIoya+v@M^665-T%5}Gwr;`SoLr6QPR;gm4 z$WvT0i_)u0-{D-B02(Gv&KMS_%9Wb=>mNmp=MP@RGShN(nV6Vlq0*sAIP7aDnhOl39 zZLK$avESq}RmA5h?BZlZqr@b-l_u1T+iDm)ky8hLF6@8eC*rjsZEyb+R&3au`uz6! z!s=>7U5p|kA_@y@2-BC1Ac1M@;?XckhaTYlp>2P2 z^&FK(jk>t2%)PPeCI8r@=<&d-@y)x4Bu-iG*gJF>@9LtNLu37 zd+vNOz4qcM^qkGJpR2|38{4%DYs^!liBw;Qi*=((v!RZy+M9%<)^rIhF{_FiZKd=B zxyB#1XKG!Dt%=BNnVCV^;Ld4h?|4Y)!UR&8wR<<-f~U|e-}5O>)^l@@b*k<<{yy#v zus(`?V8T%lEB}U60jsC5{?z)1&}zr&aB=XlN_>h5Q<=z}YV*mi7DN^3JBu%?F6V#H z_<1_M8bB#N3T+9tM8&<;VV5wU96p_}7+y|g8h?+wFkvw}rH*Md?cV@E60I)oDC&+R z508myp^1+tNBW=Q!1x{isK5M5nmx?ygUC~*cnC&qJwS!1@|wSyMS|Vdw7S@#9JcRW zp%;YuJMjrXl^9Dp2J{{Mnm{Ow#1DCo`j(-Y?m&j{y(~d|Xo+d_Tj)g1H66@h>N}Ai zCckEoSuIf^A$L$<-zcEU9gUUi+}F7afA62BUMyfE_+5tG+j%~h7%!b2QX)vnY&%9q z655}<{CfZt;>odFdj_g`KX{^f>^;Y~sk6DlVA-2U z2<0@cQ?V8G-epJiCiAl&8655T!?lcz#9)>7Ly0P>yym6lR!Z^5lVXUyo^qWq3qHFc znhLOgCm8*ziz@BsLY6U20QFcim-{_#^P5K3Rn%Ljep`gg!5rAD;4@Q&zZnV;2je;Aa1Bd;>%l1|l1Bk0AC(lwL%$ie`U0 z^e$AusSG|fkR=)m!TOII|cvO5;6%yV-_Y{Z9eTY#fD=DTv8JP1xQw?wT zJxnqOGa^wm0pa6-x$lhMB#d8vMF(&Xm~d{$C1|p|LGPw&UMl6qpNVk+AEQbQXO}R$|h5dvVHbaT(e# z)>|^`$J5r=M=Z~FdRBD$k3vAx+N#K^^^p6t)} zq3e0qKqGgd!v-1X4A_F8%qXs+)XN<9Z8dxMh*^1ZUdOlQR)RVeKjpd>hy@iB6LZ$3 zr~iZ)%JE~R`r@=h6XhQy7G5REhbRAcN;vL^=gotSYfklu`?P9ZQ3R|Fv5Zdj>FlvYMCG(l5s%lwId~Kku~^Z!6^iVBl_tDQ;WBM0(lhaa$e-AEnBS3WlB9vc z1}`33YyW8;+~0%yT=*Xd#RfYO1bl#q`?<@>Fd+exf#ZN<;(jSLNGa8UjW@t~G|r)! zu5RN*#&w$5F5qxj9|x=5jj9%v39r)@~NMDk^x}+uLHj*cPm2a=!-N3MtJs_;?R@ zw6}kUZDA3y8;wXXpxgjL~qgBrEG7#gO4+XYb(n&pI8bW<-8ItIc!Fp5B<)BPKa>dcHYS z`$_Wl8TMYThzpt=<^Ac!^NdMQj1ckczo5B!gSw?T`XixYz13TyJd7am7lo3ZdT?8e zgz5-I5|xszwcX!IpqKWg5uB$zC4~AO{{`c5b(NNBZ%GrhQwcuv=myM6*5pZ4dOAB? z=sgZ8Q?an$k(grH^3n=o&-0p&MlZeQCELOLJ2BMmr=2)vWti*g1;HSBckBhTcMB2f z&vZTZz*7;Cv`Wi9GWPy70hx?n5lMvro-&vD9&Sq6J`(y!0D;2jHb(R@I~zkR2Mns# zqx3n_1i@+w|I_-vG?+?XmD?SgpdRa0^9}q4JZ<+1n^>_Z>-lX2uMOT2Ld=Q`Hd>++ zTuH~mf>q(Ujq^#qCPpkO+lha99+dXCkkZB@oLE=xO*^u{10FLoQ#VO)DeVHIw?C>m zeD*q|gy0~!08lZP0dCoyB~DqeQoj@QTjTTR&)iE@zf2Z10O#nKIi)Rg9isq!eK9c0 zhlSON&*9m#$UL#0&X{Z|b7URGoyylrw!8bqoe2y8mu3e_ghB$&4*;}9f{8Hnz zAXDO!(mWefcq)2cGy{i$2NS@I@`?%yKk6(5$EoX)TkpxQ`ab_otwkc?+CHvpi-6+A zy%hC)|1(!ZoND1#Y0-2DO3NctqNV1I#r4K#>PG>uRTL~7wqC9c-(ZkV|X#`M;KXK>nzECNug1p%Lq7mS=Ufb z7n@d-2tWn&1rcN%Q4qIP&UTH^SPEs=31UzbzUdXB8Zq4&AY$QtGPBa-(3nm@m-d!e zeTb+;4#dehLlRz&73#F~_2DX-?CtGQNxBe0&yF|3!xWUbF~ij;eU6KIJ@b>+8oq;U z0PThZIv!X?2;k`m48;u4s9&bgDK0(*_KRvMRe;5KEatN~5IUcv^idN?_B--`IX}ud4#3s=5ZKw@}Qm*vH$Mby$0~8$Xe(um(xAG1DnOrqiYImBfMpJ7bt+$@8Tf zD2ktw_pFIWD{vPU7VPKDJECN116n`FO0;Nr>i@EMgoS|?6#>R_FRWjYApccL=l*%xPJZh@h*T(c}A1N2%W{TMFi=;a1;yM*#V`DD<*+V z0KJB{3C>gHNv#-cP9ooSY2f8{3$eoWig*QF^j@lfBY<5k8&?=|_DI-@q z^_!SuDU0~E?$yO$yyUeTNKUkzASsUA`~WQk zi0u0d_EYo<^A|jC_OPFghAv=lvAJHbio%kLUl9BG?{AsSQS;&`>Nj5iS5Ru5)aivV>$0&E#p-sS zWhs%ndvGLx);2TUwDJg1Yn-FqqcTh=i0n`kitc}jLUJ42)&?x(KqD$J$f02?sJ^B| z@S9K2ZE7HGshh6O9)?%Bjv3gFIVfp%bagE$bj4;b58^K5ChqRugZ1nGSb8-R!iLzp z>h_9H9Qvxrj{0U=Du5XtJ|UrEIWcR{r+_X&p1xUe>e+fvIu4K(zsHLWbC1!Bozdx_ zKBlk|5|XnWm{PHLl~{jI0eS?6tH`!MyHlaq#}!AB(NT~1A1I2)lXIELf#k^zJ<6Q3 zyvitGN{)=goh-LvW?&$y0Q@KV!w1oQnEEH@1@hW#ar*Q15vPYS=8v#qM#1J_a{&5- zh?jjNILAf@Fr_}dB9^j-2C)K@VjTDdrV6K4tZ^?jwp@Z?+W_550Zt_u2w%NP{awDh zL^YQbn;=m3ILj$MqqVh(<>{uKBm0Fpy@Y`fF6hE=+pRU)rTLF{kATvYtJVk$+plw3 z!iqtUYNehyUp$T=4@&hjZt{OG;e@}qyTCZH1fFIZAkq2x*x^-<{rQP(rqq5i@?Af_ zIgr!Ht5W>R4{FYu8CSXBIS?zyKW!6UCH`!!`-x1l2y0jd)AlYA2S^0gn;AddGD>#8 zs+kAACLV8qv|67pWhEH?XyO0>CfEn}{D9_y(fJH2!McI@#J%J&hzh`8d*Tji=CQXFFGYM^@?Y z_!sd3*umFhgoFoG_*iYcx1tdAF#A$YXHWY!PK$v=RWadHAU`Hv=tT#}9PDuv!%r5#z# z&FKn=U(tm-A*M);lWz!^jdO1JYW6mXr@IJTo;4^|eM{nDmyfyrzPa=Yg)>R1WB^h| z7-kQM);~>vn1<#pgfSFfN`|QlBb?{qQSyWW;mabHK^6yk6ucl*KlrO?ZAo3%JoaF{ z@ez9a##5aquBAZDN|kV<>60*iFRqkQECL%!- zXI~4G-5vZfDJv_x2eKKl^4Ipe^%@qY-xO%ZpYI(Ud;|*yvdO1#CmqDRs*dS>eZ45q z(r0Ez7G48H1-^6f^6H|Yq1g`e85B#n?YA5tK%mcS%~Ak3%BxNwa(@5Sayi`t5LS;_ ziibEjhS<8oY}OTSi~d0SYJ8PLcQ|6)q5f58MruM_q6^Tz7`RN@qmJd9BEuAHc|kvy zvAK|=Y&ttL+}xXS8!QJMYWUqZ{=XKwpROg;|I9gf`})FPs;2Y38XUk!kdk^tZ;cx> zje#KBjMNFoXwEm2VIMoKWrJ42lM|)R*50s&yWB;iOg`8`2)dYx(p0VE-L?-+!8r{_ zQ|w?uirI6xepbDS@yov#7Ty*Xvd6?>SZbNY8qM%K8u9toC}agIBU@MVb2rpCP^7?~ z!Bl*jf;y+$YS~x#eqz`Dfw~;mVU@oD9-x9v@qj&QqQQ8#v2KA;nubWqXTAu?OVY$mdwVTCf-5#?X+c4 z>_@JIDD*m(9>?K{0~$ zUZ2h1my4l7@eR3iJd6K|MSS>xJv20gSkS8wiO~d<7X=Dx`=n0k_>+5fGqpr#PY23s{>U|Cdy5#wv+>Al*l_w!I z>a-PUzDW7dRy)kUhd4TxbYE^wAh9d{5zCd5r^FOe2DV<$mypI*u(?q9~zM zgoM_B@H1Q!0XZ7fDX^tChstHOIs>^gDlMmvG7*P!zpsRpN{obg7ykid}}gF%E%lYumCO@pv1NNoc?l((NAJel9y?R01dm_*m15w5lE=t_10n>?*E;9 zUkPjLEJhrHKIfch&@|?gafj>WJ4h3MA^lNbG6y>>L)h(syolTCQ<&JHHcQ6XC+T?$ zxfo2*2Biz04Aeme^H}8+PK;uULU20*>M_s?()@~5Oza&_lAT$p#PiudKb!b%h6d$h z6VlLP>68^$LT+T%{BEAyu!D_}+hwNd@^9;V zXU!2%RJb=Tkdi$KqT~b^FDMG^97T`Kll!RNj8nGu!2bEy?d=5l`$Cvv3aV~WZbVGX z0zfqg4e;|s?0!e~z^aRTsg*YX!>Aywn?8};IRGt(AxH@!J<*hcJQnEoA%J>`Zxa%l z&0huc5&U`6C>Q-m<8Cu!v?eLfkU8klryx@Ch-I6m~vn&qP`jzVllwR?0yne^d@se zhlN3T9uoSr78_M1n>5aMnhpo#Ec`Xp0lBo|q06ti+l0*R>S_5wPI9l>KU z<&Ap0v=pt6p+p4|1`T)>Xz>(sAln(;1LTJSiqT@j!YVMMd8eAemkGQ*vr@ zT!2dlj5V{)gMuYOLKitK1_t$yx(*^0euEO5Rm(ZTBwV_QUEu)a9^Dh)Wwz*4Yc=`O ztKC84mUh!2NqjL(|fRDbvaxL7}maBJ}}bSnfT zOg}TZou(abuQ`T)q+?j3|D_JPbF(sukC;+cVO4yIS>gPN;a`y6Kbz9Bn zE(E*^m=`hab*?K<014b&STn>Ygi`Q1zSqa-E})(O=>i>6pj8T48@yK3yi>U?LfqdTn`AvCL%_ zr&srDxc(tUlJ*JbTSZI4^r+H@)gcz))F5nzu&uvfmm%-J#FUJau6EIPT1xy5t$#O6&`WWjt zKPp!6I3hfp9;jfeTrZ_G?^NUehysK-0c!nVAp4rw?-I;1@cI%jW^v7s8fXOVdq5(* z14M}K-6=O{(erqL4VA2NimrI$ODGsI4D75V-DJlixY&?MZ5nVr}GS1+3XeplXS z8)?n%D&=9EaYjJ?1B%eXoeTwGk_wiqZ<1fzk1BCEL1z`#XB=o25-9SKZD zCSXAl#*>GKH7J120lsowu_XzQ7dO-efF!H}XSN>SuNWZu0Xp{`0VW^)Z`!%-g&i(r z)73>RYye?}K_`#_)G?L7Y8v>gTDS^W{%t~NntKI=fi9<8*PHej+srSgY^Rv9-1epErz$JduT;IFsl$Z&^SPaM5? z%<}4LCkB{TB|X;2pqUkAFz@WqYQ|X3ko`3^S()}pzyKA}JXd;kh;kGb$hj>*0Nr2D zQ&^V)@Q=}%HSbpA{_fw-RKPbB3{V3OOnhPmFk2$X*i>vMt&C#<5si7#d(ID2%YJ72 zvho!R7s(R^SMSx-4Exyw;es#j1;0|EZbS^Qe^jLia`p&NwK0?9+d3$1j(^0gMAB6> zKs{X!q&rQmZKFY8Lv_r~Qp?-SeuIqUMKLpP5lkv2KTVBKtl{>?a+t29V@4v8gutu< zH=$4!EN?KsY_z@4X4BU>_!vDL$&?S08d!Mjp=NRLvpXIZQ;9BN2#}@m+b{lMpPA@Fr((9Vc_h;)#!5u{K1g{<< z43}_olhsr|E}@>NJ9X2PO6WYCq%D8H3};n$1X~sK&(!5aR&`GBUhLAHO!FOJzSu_& zZ2x$;^E;y?^;PApE|! zVvdaFto4!pgn$6$h7_kutSuLqdgu7i#Q+yk3Ik+E0RVadl*NLrYpjI@LZK-jmV-Nu z@Cp4cflZ7F)Kbp-iid=X>@)wY)N+rhf+93ueYqOkGsSCraOZQ5g>wkJw7K`s@rO8O zH<+dYxSt3_uw0TaO5zfW5R}(OoaI6J-G%~76oSx2?Qml(FR*b6<3hjlC164?%ECW8 zQkw#_kv+N#TqO`(Vb6;@#&w4ay-9@=>>DHM#9~1dKr?2j0+h7VgS~d^l7Iz$`WI}0 zP&^0%IHg?V0r%r`ZD*@-VU5}+cV%RTaZ>J30)xK)iQy-f8pqj1GXUq!LD}qC0Y6$~ zR28}<>gfjjIhv0C{%D+)fUiJ}>rLuiih4kMm8A}7utygS2#Ze~@c$MKD=4;Z-|hzH zAY8z>!8;s$-vP)uA*TNUV=EEs&{nG^hHKyti0;d?JsbgtIXQ426HR+tTc)iJ`$&nZ zGJ2&?+2A{9vB@0QQC!Uzp#WB6EO5M0bfMb5AVs{O6AchPnD&f_>@k7v1443+``@3T zG1OB4jDrW78>A|F!frb6?mz!$h&2aa#5TIG4+El)Ip8rSmtg^Ze?)1R)w7HM{{RoB zP{5>f^)WY_1|GKYO@hPSVCI~oA_2wk98&dr9X?#0||MJ<)U37@4fV%XrLwVXT0$ze{ z@yWe^eIO5x`uC&E1#r5I-umLpNq4@8=2|GM!{DjUEYgtV=Gf)w>64%y?Ry*Jla`An z8h5mh*VYOzn6>DO46PIHch#RQVO>l`v!*rk``A6Xq$HJHR^01KYZ|4!#J_vXx}rgg zew!$O!c4zR?dlmcy^0sxf74ya3tP&UQz;-1)bwfUc8)^8B`g_Zhh7A{rqzBsjI3 zfqiYUx3>}{PcLAgM?~^6Wmegm$EEWLa2f|B_5J;Ymo&nn$CK{d9k)6j)>p|Wi%_FZ zTT(Df5FK$}7AbEqN&>puP}LV&nqM&$?h6bOQ!c4L`EEBNRVTpN2e%nXJ=&8HsVe9TerbD`Fj#ea*!`RF zBYB*8i3p{weB$il$6EQSpPlmMmQ}poT1BRCA1`sOkLHmTSnVhTQQ%*|)wb*y*z7K& zkp2l@$o9Jqa!qCpMhguieLd_J*0jPlQ;m>qi%WfwSSkC;5~j!BIqOpf%on3{Kx$`T z##4PWr6S34mOS$Q-{H`MW=HE~?2$d2vCs@hA(fLO4Q*S=%oBUrR1pWF08xGh>pI-0 zh7BP|+91Vzfhq-#Axdn`yfJSPk9)w#;sBNk;hM9@ZAtmIPGjYsgz)QMmcQ-)S_OZc z>q9P$!Uv4*wpI_Y)KmUWPGUAJen#?ESM8hxgI&4W;O*mEDH)w+ zoLir!S-W;GUooez;(zwODzY3*iY*DwC#tsL8)DUk9u+>4q4BbyJeR&YBXM3a9Y1iy zTV}AF$9#BGFeBi&$|Kf$ykrvGR}Xbv^}dIQU_@>5-Wg7LC9_g=p`1L-*K?D7E?w&S z@UoWiYZ;ecQ0ziXa!xXlHb1%7ourCgt1+_>ywV@nMze3c4);KfM{eydCs&$Y;q_r# ze_8*&H8W(_VsOi(x7z_g{VSrV8g*`Dy1JEW>E|#ZgM-+Xq7&+(PvSPFj{L_IHIkv` z;+<|aMx%YVl;yFBEBWH;jl_S{Pv;<-?F8&L^qngk8AWeH^oI-YTqh^-@bmJ%x_WWX z=m)i(oEL04&0*67M$Lt&NoZY~U8}zuh z9?v_Hd7V5k`4J{^THF7tOX^NdKyXd$T@xH8`{y^{yfK~6C-oxH-2a=P>C!phG<$oY z>?wG%;)@qk?qz9B$EfU^^n1RXH&0Fu3q_0b7Pr=-|Ia{_kz14~&gh?3^nxmK>7Te{ z#eZ+CIAsL@)i_1}K8j!y^8VNm`=0*8XQ!SOwQXLzx8z29x*RUq^(P4XulJA2-q_u5 zJyLrn)1Hd)a|h_PBpQ@gmP`_wZ|&6Fs%3ooz<)z~$)w3c3Ce3p;1$yG)+y6=rmsfL zMlN)-MI-s@)?5JA9^K_HgZ07rG~IIN2!DG#{byk8K1(FGu&S7P12}@&E!z`oEh-JP z(}0Xo6K%iVx9oW}3JBMIpri9vq`rT3{H#?1=NS`H+o2}piL@>}{5+hV;;SAj`7ydQ zD~UgQ>*lS~{Jf^yJ;v629pbMzVg!RMNN9Ah_vn%r?Cw(zTon0ntN-kj3fanh zlUwxq0Fnq$?v=-Mhavzg-KK5`Gb`cj6hQE=_$nt#Yf?V zf8Nk!PrlfmS&c8`Gqdv}3V4jrLe#iy*z@kCu;e5!-hBF!I6Sv}!eU0i?YGKbkIkOn zHMe?B)(HRVl|S@Mswe5*7-$qjuP!|Y6cW(h$Md|17TV%3CJ(4qUsHiXbtjy-2dr2f zXO(%+&VU0AQi-^_nsw;IJK^^}lr;k^#6dQ^wrn&%-tor+Afkh*I9lZ~jy=OUJWmPq zdm29@V}-7m)6T<|K8+t#a--?I*v2i`T5ck}KME09^|#{3HQh?BP4_Ad{Qj9M|IIDL zZ-cOUq?=(ovy5iv7xAbW+75VJ!31k)edd>Sl*NImSx2I@*6wygTD|2rrHS$9GqIx1 zP|h@&(?H>xyJX9b!hdgT-J657h4pSD;xp@4yx#AI-J@fNN6g>=F0qpRmmi1jYu$Ut z!!sgS<`3$j8F?xKzSUr}b|Z;CcxuI?$*JtkjpD?<&P(dFcV25_Gt62#tZO(8svxmsb+|UPrC=x(v!u<5H@{YX0$~=rze@9}i8QQS5zGZM$(+Z+0|x&z((i@3a!G zl_iaV3&`z3vPIJww99nr%g4V%71A_Hj~==Trv8X@6qDg}JZC0qaC1)4l2;G5li2=a zTF*?wb$6JFYgNoN7NYgUnLM^gOu>cV#P#~-Ym;pGa0xdG=e5A{n?V`TH(cpsOBI(k zmS%nUuigL3;E}kmWq%G5?hJnUMDMr2ysxVM&`8S1sp$K-#ytVz|2MzKfM&v|_!&wk zS$lU>u0pSr1$)e|$m!zkE+*xpqAje4Fl5-z|cV z%}uhp$a|!-#?ClHWa}B9iVPKPio!BbLhCpY%Z^TQlOySU8GGLg+c@(@?vA|cm~xo3 z5@2%=C%u=Tap#u#l!0S#xLTLvV~X^4XZNQf-I?3XLZg}j{P=omjmLpO`=;W&%96)J zxjt4V-Th9PS*jLm%jw5P>EUZJDxMFRQd5YjCR6Ciqq?1rD{|hFC+!sX5@073A$e5o zTtAltDQW4I?{64trpH4vE&Hdsc+cxP@oH8-WiyrfD#!RZnpV>&xqf9=E#2w8@5!Y+ zvXQbfB3ffP6?Ty~L_F-i7jn^5;EtZJDX^HoYE6J;y$@G7aT$Lt7Wao~9nlRFzL*Pj z^^%t-Nk)zK(y(kvUM|a_)`FgzY4j4_v3BydVjhPPVu_3+Bh9~_Lvp}b-D*9{a;o*c zu>R`oSQ=iS9(6yl=mn(2fz7h(26O4Ago zNhD`m@u|1r)hqc}EVVipCv|H-JCSL_k;%EVfunKv>Kd3od~fIE7kHN^FnB&eY7GlI zK6iU(ja`1|jDCBKNU)IO%d2SQ&)h0rw|7$g*Xd9PcerB3F!&KRMpsQEXJbEjPHJF) zdSKhgZGCifj?eyQLswL;55#r9I@_|A(Qk@(c`DKkb+X5p5S$L3$uZ7G(SiUoidJ%} zmS$(kXZPKgxQy3P+Oe!CjwBy`S0-l$px?!l1pG!=tx1%)g$;+5!y{yYl*4w)&A6IjdZtUiJ zedS7Nuye6xZDb~o7K^)@y}dp!{;!utF4fX>IDHOFfA?zAto~e?h_#A6YT@$IXYkw+ z^{PMQfHNz7NlrB_zMs9mqQSyr05O4RJ3rsgI-W(eq}^XP787Jug`_w8tt9CCb{Vgq zECxgNhDXrt7U!;OpDEFM_2vXo=+Zhno2LLlI(-0J9gKY@H47Qp1m|6D>y(~2YJw(4C^5_yEhd|g?xAVR`&8Odj%b)$l#Uz$E~9^!ix zIDPF4V6TUo8@OGq^!5mElRBj(4VL~Hu->_HHF0TqFT86?3C&+gP@FX^znJPnH>CZy zla&LcjnA4VGUX{b2mANAlpWNP(<)IyqJ1fx%3Zxx!bW}uS%DYv{*HT}{Z9rC3&ewT zb%SoW7y(XDyZ#nl)cGh z<_dM8TW5PG)!cmcal=k0-nfdw&LUFSaY{5V*J9M_{ zV!7H%*!!;!^7Z1xfs=h-20?Lu`=nRy>97k#Q`AREME4-iZe=P;#7;$^_+-$3cP?!? zh*o{1KHY6-UGc%DLYMUUNcl(MwnI-3Yv*1R%8??So^e1TEk;KaeMpPXB1xUv6YX)+ zo2T&fHS>*&9_q=-_>y95UJx=YtRvW~S5E)y<%?BI1C`);+A7uD@w2JAUn^G}0R_#L zy443WX{P@#pC>qsB2)5KcDM$;s*6;2IvpnKD!3#sWR1y!u9Q!AMJb^(2gd^WSNZ4Y zz3J)U#&4{b>QPE-b@-`sai$bJfwptL1B0%EDYOGenE^Kb$MVC-S1Kagb4kf}`ypr< zQzwB<%G6{#7_2sibT-vr){Rq?wJXD8uby}DM~VCF`7?o_AQgj>462js%Dh5RpMDf7 zaCYKCNp+PAk-p<-l3C(6ZoNOyXjAw1tms-rIWT3bux|fPs@90>%5sKwzF?kJ%M8`b z{UUN(N@R z`sn>sQ3sXnbsz3&uWh9*yEWKSre3Z^$JzfeGcDWuxcb?Ht`tCotC2&SdU@gk3S}NlRmiZChnJ?U2&~cS-8HN{KdDn+k`zl-pG0Ew61)~Tv7gIZ9L6SN)pwj# z(4$&YilP=obvs)(u5KY+l~f(;SG3ma{5%ABVC_B*EvpswVv&-Nq#957*f<}Q7@w9)^WvEW-@2Nkk{!33lGdGZBTi`T>w>P39Z zFAD>}tzX@L6DBZ{WPg@D|{8953<|T;;^ZYdt^Ze|~9n8y< nf6jTq_McP!6S4o#66P8k>w*DYuM!2rq@1FxnoNn*t9SncAim`P literal 22801 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 W-FKg^bGCg43Sv)JKbLh*2~7aN3?W$n diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index ee9182a654d1e8590a02aa23d87abb57144a51a4..7b96f3ec43c7ed6cde24842fb49ffd41f02133be 100644 GIT binary patch literal 7054 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 W-lz9Dga5PXQMT{eb_F{4^}hjykH1|2 literal 7049 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 diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 03ffc7ac7b1a58b38d4e54eb63ca198a3101021e..a7088de3849a54e103677f10d3d4224921e8feac 100644 GIT binary patch literal 11827 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 XKy)i_PxajewpesD4K>QuZruM5$4YVU literal 15701 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 G$o~LiX#uhT diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 3a7315df405dfe11b32534e49b44700e67e60d9d..8f93ba81ad2adc2d9bfcd4645303607fd962a233 100644 GIT binary patch literal 10314 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 bZTAH~-nel&0XzVK!A_nCJ>K-q=^y_K?U1dR literal 13808 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 aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b literal 1920 zcmeAS@N?(olHy`uVBq!ia0y~yV41|gz^KE)1{9egI&&`r1G~GYi(^OyVv%M>FVdQ&MBb@0DHC3!vFvP diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 5cdce6929667362eae577ab636fe34f789debb6d..98890b9687ac95a52ecf4a4299df070eb5e86fa6 100644 GIT binary patch literal 6712 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 mFo1ISpN??m3@2hhkpk)MYIF} literal 9163 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 diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 394b5585097a24da5e3c10ed2c1b50bf8a9cc1cb..479280f598aef7d02121f8874c21c223c9f642dc 100644 GIT binary patch literal 15100 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 aHE&iz8^0!7DlG?zEGmi`4~yiVy!s!XBR4+) literal 18998 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 V8LWggH9(gQQeyINi$(N5{y#)d+qwV% diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 98c7844e41f19..84203617ff853 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 26f111e9edcf9..9b0122c157481 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 7f64f97845191..2d6e756a7f0a3 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index e171a65be8bab..37c6885d76cb0 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index ed8b0b258fd90..60a0e450906a2 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 8a349aa5df060..6b2f93b47c0b2 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index c1e429508c37f..4241d6f208bfd 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 98c7844e41f19..84203617ff853 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 1325c7fbed03e..ae1e817424cb1 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 2b063b518665a..c0da479472880 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 6152fd406961f..c5fbcd63b0685 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index e4c6b09a264dd..b67b074449403 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 98c7844e41f19..84203617ff853 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index 26f111e9edcf9..9b0122c157481 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 7f64f97845191..2d6e756a7f0a3 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index e171a65be8bab..37c6885d76cb0 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index ed8b0b258fd90..60a0e450906a2 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 8a349aa5df060..6b2f93b47c0b2 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index c1e429508c37f..4241d6f208bfd 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 98c7844e41f19..84203617ff853 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 310377eadd165..af9fe198d88ea 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 1325c7fbed03e..ae1e817424cb1 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 2b063b518665a..c0da479472880 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 6152fd406961f..c5fbcd63b0685 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index e4c6b09a264dd..b67b074449403 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file From c76519e15c4ca10c9e2c7fc5b0dcb8216d7661b0 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 3 Feb 2020 14:02:28 +0000 Subject: [PATCH 07/17] enable darwin for the node installation in the CI (#51705) * enable darwin for the node installation in the CI * refactor: avoid hardcode strings and customise based on the OS flavour * fix classifier Co-authored-by: Elastic Machine --- src/dev/ci_setup/setup_env.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5217fdf002be9..823c70e80fe7c 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,23 +56,23 @@ export KIBANA_PKG_BRANCH="$kbnBranch" ### ### download node ### +nodeVersion="$(cat "$dir/.node-version")" +nodeDir="$cacheDir/node/$nodeVersion" +nodeBin="$nodeDir/bin" +classifier="x64.tar.gz" + UNAME=$(uname) OS="linux" if [[ "$UNAME" = *"MINGW64_NT"* ]]; then OS="win" + nodeBin="$HOME/node" + classifier="x64.zip" +elif [[ "$UNAME" == "Darwin" ]]; then + OS="darwin" fi echo " -- Running on OS: $OS" -nodeVersion="$(cat "$dir/.node-version")" -nodeDir="$cacheDir/node/$nodeVersion" - -if [[ "$OS" == "win" ]]; then - nodeBin="$HOME/node" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-win-x64.zip" -else - nodeBin="$nodeDir/bin" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-linux-x64.tar.gz" -fi +nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-${OS}-${classifier}" if [[ "$installNode" == "true" ]]; then echo " -- node: version=v${nodeVersion} dir=$nodeDir" From b64f0a76fd4eda536f6fc0cda891a45620249e9c Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 3 Feb 2020 15:33:57 +0100 Subject: [PATCH 08/17] [ML] Setup apiDocs to generate routes docs (#56006) * [ML] setup typedoc to generate routes docs * [ML] remove typedoc packages * [ML] apiDoc * [ML] update optional params * [ML] address pr comments * [ML] change names * [ML] change description for GetDataFrameAnalyticsMessages * [ML] add custom order --- x-pack/legacy/plugins/ml/.gitignore | 1 + .../legacy/plugins/ml/server/routes/README.md | 16 +++ .../plugins/ml/server/routes/apidoc.json | 21 ++++ .../ml/server/routes/data_frame_analytics.ts | 107 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 x-pack/legacy/plugins/ml/.gitignore create mode 100644 x-pack/legacy/plugins/ml/server/routes/README.md create mode 100644 x-pack/legacy/plugins/ml/server/routes/apidoc.json diff --git a/x-pack/legacy/plugins/ml/.gitignore b/x-pack/legacy/plugins/ml/.gitignore new file mode 100644 index 0000000000000..708c5b199467b --- /dev/null +++ b/x-pack/legacy/plugins/ml/.gitignore @@ -0,0 +1 @@ +routes_doc diff --git a/x-pack/legacy/plugins/ml/server/routes/README.md b/x-pack/legacy/plugins/ml/server/routes/README.md new file mode 100644 index 0000000000000..1d08335af3d2e --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/README.md @@ -0,0 +1,16 @@ +# ML Kibana API routes + +This folder contains ML API routes in Kibana. + +Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotations in order +to generate documentation. +The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. + +For now the process is pretty manual. You need to make sure the packages mentioned above are installed globally +to execute the following command from the directory in which this README file is located. +``` +apidoc -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md +``` + +It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format +as well as `ML_API.md` file. \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json new file mode 100644 index 0000000000000..8292e946cd344 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -0,0 +1,21 @@ +{ + "name": "ml_kibana_api", + "version": "0.1.0", + "description": "ML Kibana API", + "title": "ML Kibana API", + "url" : "/api/ml/", + "order": [ + "DataFrameAnalytics", + "GetDataFrameAnalytics", + "GetDataFrameAnalyticsById", + "GetDataFrameAnalyticsStats", + "GetDataFrameAnalyticsStatsById", + "UpdateDataFrameAnalytics", + "EvaluateDataFrameAnalytics", + "ExplainDataFrameAnalytics", + "DeleteDataFrameAnalytics", + "StartDataFrameAnalyticsJob", + "StopsDataFrameAnalyticsJob", + "GetDataFrameAnalyticsMessages" + ] +} diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts index 7b855e5f87cbf..67fa2fba46f1a 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts @@ -15,7 +15,20 @@ import { dataAnalyticsExplainSchema, } from '../new_platform/data_analytics_schema'; +/** + * Routes for the data frame analytics + */ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics Get analytics data + * @apiName GetDataFrameAnalytics + * @apiDescription Returns the list of data frame analytics jobs. + * + * @apiSuccess {Number} count + * @apiSuccess {Object[]} data_frame_analytics + */ router.get( { path: '/api/ml/data_frame/analytics', @@ -35,6 +48,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId Get analytics data by id + * @apiName GetDataFrameAnalyticsById + * @apiDescription Returns the data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -57,6 +79,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/_stats Get analytics stats + * @apiName GetDataFrameAnalyticsStats + * @apiDescription Returns data frame analytics jobs statistics. + */ router.get( { path: '/api/ml/data_frame/analytics/_stats', @@ -76,6 +105,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/_stats Get stats for requested analytics job + * @apiName GetDataFrameAnalyticsStatsById + * @apiDescription Returns data frame analytics job statistics. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', @@ -101,6 +139,16 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {put} /api/ml/data_frame/analytics/:analyticsId Instantiate a data frame analytics job + * @apiName UpdateDataFrameAnalytics + * @apiDescription This API creates a data frame analytics job that performs an analysis + * on the source index and stores the outcome in a destination index. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.put( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -130,6 +178,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_evaluate Evaluate the data frame analytics for an annotated index + * @apiName EvaluateDataFrameAnalytics + * @apiDescription Evaluates the data frame analytics for an annotated index. + */ router.post( { path: '/api/ml/data_frame/_evaluate', @@ -154,6 +209,22 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_explain Explain a data frame analytics config + * @apiName ExplainDataFrameAnalytics + * @apiDescription This API provides explanations for a data frame analytics config + * that either exists already or one that has not been created yet. + * + * @apiParam {String} [description] + * @apiParam {Object} [dest] + * @apiParam {Object} source + * @apiParam {String} source.index + * @apiParam {Object} analysis + * @apiParam {Object} [analyzed_fields] + * @apiParam {String} [model_memory_limit] + */ router.post( { path: '/api/ml/data_frame/analytics/_explain', @@ -178,6 +249,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {delete} /api/ml/data_frame/analytics/:analyticsId Delete specified analytics job + * @apiName DeleteDataFrameAnalytics + * @apiDescription Deletes specified data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.delete( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -205,6 +285,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_start Start specified analytics job + * @apiName StartDataFrameAnalyticsJob + * @apiDescription Starts a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_start', @@ -229,6 +318,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_stop Stop specified analytics job + * @apiName StopsDataFrameAnalyticsJob + * @apiDescription Stops a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', @@ -263,6 +361,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/messages Get analytics job messages + * @apiName GetDataFrameAnalyticsMessages + * @apiDescription Returns the list of audit messages for data frame analytics jobs. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/messages', From 7dc8c6f9ba15d4566147dbac144b32becdf2adee Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 3 Feb 2020 07:51:53 -0700 Subject: [PATCH 09/17] [Uptime] Fix flaky functional test for #54541 (#56449) * Add timeout block to race-prone functional test code. * Add timeout for pagination click functions. Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/uptime/overview.ts | 12 ++++++++---- x-pack/test/functional/services/uptime.ts | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 73b91a61196bf..9a879032fadc1 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -53,8 +53,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); - // there should now be pagination data in the URL - await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -67,9 +65,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0018-up', '0019-up', ]); + await retry.tryForTime(12000, async () => { + // there should now be pagination data in the URL + await pageObjects.uptime.pageUrlContains('pagination'); + }); await pageObjects.uptime.setStatusFilter('up'); - // ensure that pagination is removed from the URL - await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', @@ -82,6 +82,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0008-up', '0009-up', ]); + await retry.tryForTime(12000, async () => { + // ensure that pagination is removed from the URL + await pageObjects.uptime.pageUrlContains('pagination', false); + }); }); describe('snapshot counts', () => { diff --git a/x-pack/test/functional/services/uptime.ts b/x-pack/test/functional/services/uptime.ts index ed39f28aabbfa..1d8e0c97b99c4 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -38,10 +38,10 @@ export function UptimeProvider({ getService }: FtrProviderContext) { await browser.pressKeys(browser.keys.ENTER); }, async goToNextPage() { - await testSubjects.click('xpack.uptime.monitorList.nextButton'); + await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000); }, async goToPreviousPage() { - await testSubjects.click('xpack.uptime.monitorList.prevButton'); + await testSubjects.click('xpack.uptime.monitorList.prevButton', 5000); }, async setStatusFilterUp() { await testSubjects.click('xpack.uptime.filterBar.filterStatusUp'); From 1c849acdd57859256d94080682ea933680c9cef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 3 Feb 2020 16:00:17 +0100 Subject: [PATCH 10/17] [Logs UI] Add smoke tests for log rate and categories tabs (#55088) This adds function smoke tests for the new log rate and categories tabs in the Logs UI. --- x-pack/test/functional/apps/infra/index.ts | 2 + .../apps/infra/log_entry_categories_tab.ts | 28 +++++++++ .../apps/infra/log_entry_rate_tab.ts | 28 +++++++++ .../apps/infra/logs_source_configuration.ts | 62 ++++++++++++------- .../page_objects/infra_logs_page.ts | 10 +-- x-pack/test/functional/services/index.ts | 4 +- .../test/functional/services/logs_ui/index.ts | 18 ++++++ .../services/logs_ui/log_entry_categories.ts | 23 +++++++ .../services/logs_ui/log_entry_rate.ts | 23 +++++++ .../log_stream.ts} | 17 +++-- 10 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 x-pack/test/functional/apps/infra/log_entry_categories_tab.ts create mode 100644 x-pack/test/functional/apps/infra/log_entry_rate_tab.ts create mode 100644 x-pack/test/functional/services/logs_ui/index.ts create mode 100644 x-pack/test/functional/services/logs_ui/log_entry_categories.ts create mode 100644 x-pack/test/functional/services/logs_ui/log_entry_rate.ts rename x-pack/test/functional/services/{infra_log_stream.ts => logs_ui/log_stream.ts} (70%) diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index b706dc8cce546..597b522a88c51 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -12,6 +12,8 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./log_entry_categories_tab')); + loadTestFile(require.resolve('./log_entry_rate_tab')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./link_to')); diff --git a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts new file mode 100644 index 0000000000000..c703738e37228 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts @@ -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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Categories Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts new file mode 100644 index 0000000000000..95228a520aaa2 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts @@ -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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Rate Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 183acf3a980ee..ecad5a40ec42e 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -10,12 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - const infraLogStream = getService('infraLogStream'); + const logsUi = getService('logsUi'); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'infraLogs']); + const retry = getService('retry'); describe('Logs Source Configuration', function() { this.tags('smoke'); + before(async () => { await esArchiver.load('empty_kibana'); }); @@ -32,8 +34,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log indices to a pattern that matches nothing', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const nameInput = await infraSourceConfigurationForm.getNameInput(); await nameInput.clearValueWithKeyboard({ charByChar: true }); @@ -47,13 +52,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the no indices screen when no indices match the pattern', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - await pageObjects.infraLogs.getNoLogsIndicesPrompt(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + await logsUi.logStreamPage.getNoLogsIndicesPrompt(); + }); }); it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); @@ -63,16 +74,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the default log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - const logStreamEntries = await infraLogStream.getStreamEntries(); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + }); + + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); @@ -80,32 +94,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log columns', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); await infraSourceConfigurationForm.removeAllLogColumns(); await infraSourceConfigurationForm.addTimestampLogColumn(); await infraSourceConfigurationForm.addFieldLogColumn('host.name'); - // await infraSourceConfigurationForm.moveLogColumn(0, 1); - await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the changed log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - // TODO: make test more robust - // expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp']); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + }); - const logStreamEntries = await infraLogStream.getStreamEntries(); + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index 6eb1349210bae..1c58f8dc41eba 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -18,12 +18,14 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide await pageObjects.common.navigateToApp('infraLogs'); }, - async getLogStream() { - return await testSubjects.find('logStream'); + async navigateToTab(logsUiTab: LogsUiTab) { + await pageObjects.common.navigateToActualUrl('infraLogs', `/logs/${logsUiTab}`); }, - async getNoLogsIndicesPrompt() { - return await testSubjects.find('noLogsIndicesPrompt'); + async getLogStream() { + return await testSubjects.find('logStream'); }, }; } + +type LogsUiTab = 'log-categories' | 'log-rate' | 'settings' | 'stream'; diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 84d5a792ae6ca..aec91ba9e9034 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -46,7 +46,7 @@ import { GrokDebuggerProvider } from './grok_debugger'; import { UserMenuProvider } from './user_menu'; import { UptimeProvider } from './uptime'; import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; -import { InfraLogStreamProvider } from './infra_log_stream'; +import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; @@ -88,7 +88,7 @@ export const services = { userMenu: UserMenuProvider, uptime: UptimeProvider, infraSourceConfigurationForm: InfraSourceConfigurationFormProvider, - infraLogStream: InfraLogStreamProvider, + logsUi: LogsUiProvider, ml: MachineLearningProvider, transform: TransformProvider, }; diff --git a/x-pack/test/functional/services/logs_ui/index.ts b/x-pack/test/functional/services/logs_ui/index.ts new file mode 100644 index 0000000000000..c70a8470aafce --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { LogEntryCategoriesPageProvider } from './log_entry_categories'; +import { LogEntryRatePageProvider } from './log_entry_rate'; +import { LogStreamPageProvider } from './log_stream'; + +export function LogsUiProvider(context: FtrProviderContext) { + return { + logEntryCategoriesPage: LogEntryCategoriesPageProvider(context), + logEntryRatePage: LogEntryRatePageProvider(context), + logStreamPage: LogStreamPageProvider(context), + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts new file mode 100644 index 0000000000000..b9a400b155679 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.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 { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-categories'); + }, + + async getSetupScreen(): Promise { + return await testSubjects.find('logEntryCategoriesSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts new file mode 100644 index 0000000000000..96c69e85aa0a4 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.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 { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-rate'); + }, + + async getSetupScreen(): Promise { + return await testSubjects.find('logEntryRateSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/infra_log_stream.ts b/x-pack/test/functional/services/logs_ui/log_stream.ts similarity index 70% rename from x-pack/test/functional/services/infra_log_stream.ts rename to x-pack/test/functional/services/logs_ui/log_stream.ts index af113d3afffb4..ce37d2d5a60da 100644 --- a/x-pack/test/functional/services/infra_log_stream.ts +++ b/x-pack/test/functional/services/logs_ui/log_stream.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; -export function InfraLogStreamProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); +export function LogStreamPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); const retry = getService('retry'); + const testSubjects = getService('testSubjects'); return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('stream'); + }, + async getColumnHeaderLabels(): Promise { const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll( '~logColumnHeader' @@ -35,5 +40,9 @@ export function InfraLogStreamProvider({ getService }: FtrProviderContext) { ): Promise { return await testSubjects.findAllDescendant('~logColumn', entryElement); }, + + async getNoLogsIndicesPrompt() { + return await testSubjects.find('noLogsIndicesPrompt'); + }, }; } From 85988386b6fcc5f71c0b01fd3b6fd14767bdb93d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 3 Feb 2020 07:04:23 -0800 Subject: [PATCH 11/17] Containers (#56571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 move state containers to /common folder * refactor: 💡 remove RecursiveReadonly type in state containers * fix: 🐛 assume we are in production by default on server-side Co-authored-by: Elastic Machine --- src/plugins/kibana_utils/common/index.ts | 1 + .../create_state_container.test.ts | 0 .../create_state_container.ts | 25 +++++++++++-------- ...ate_state_container_react_helpers.test.tsx | 0 .../create_state_container_react_helpers.ts | 0 .../state_containers/index.ts | 0 .../state_containers/types.ts | 18 ++++++------- .../demos/state_containers/counter.ts | 2 +- .../demos/state_containers/todomvc.ts | 2 +- .../kibana_utils/demos/state_sync/url.ts | 2 +- src/plugins/kibana_utils/public/index.ts | 2 +- .../public/state_sync/state_sync.test.ts | 2 +- .../public/state_sync/state_sync.ts | 2 +- .../kibana_utils/public/state_sync/types.ts | 2 +- 14 files changed, 31 insertions(+), 27 deletions(-) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container.test.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container.ts (86%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container_react_helpers.test.tsx (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container_react_helpers.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/index.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/types.ts (88%) diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index bfb45b88964d8..d4aeb2c0fe4ad 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -19,4 +19,5 @@ export * from './defer'; export * from './of'; +export * from './state_containers'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts similarity index 86% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.ts index d420aec30f068..78bfc0c3e9090 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -19,7 +19,6 @@ import { BehaviorSubject } from 'rxjs'; import { skip } from 'rxjs/operators'; -import { RecursiveReadonly } from '@kbn/utility-types'; import deepFreeze from 'deep-freeze-strict'; import { PureTransitionsToTransitions, @@ -32,14 +31,18 @@ import { const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; const $$setActionType = '@@SET'; -const freeze: (value: T) => RecursiveReadonly = - process.env.NODE_ENV !== 'production' - ? (value: T): RecursiveReadonly => { - const isFreezable = value !== null && typeof value === 'object'; - if (isFreezable) return deepFreeze(value) as RecursiveReadonly; - return value as RecursiveReadonly; - } - : (value: T) => value as RecursiveReadonly; +const isProduction = + typeof window === 'object' + ? process.env.NODE_ENV === 'production' + : !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; + +const freeze: (value: T) => T = isProduction + ? (value: T) => value as T + : (value: T): T => { + const isFreezable = value !== null && typeof value === 'object'; + if (isFreezable) return deepFreeze(value) as T; + return value as T; + }; export function createStateContainer( defaultState: State @@ -66,7 +69,7 @@ export function createStateContainer< pureTransitions: PureTransitions = {} as PureTransitions, pureSelectors: PureSelectors = {} as PureSelectors ): ReduxLikeStateContainer { - const data$ = new BehaviorSubject>(freeze(defaultState)); + const data$ = new BehaviorSubject(freeze(defaultState)); const state$ = data$.pipe(skip(1)); const get = () => data$.getValue(); const container: ReduxLikeStateContainer = { @@ -101,7 +104,7 @@ export function createStateContainer< ), addMiddleware: middleware => (container.dispatch = middleware(container as any)(container.dispatch)), - subscribe: (listener: (state: RecursiveReadonly) => void) => { + subscribe: (listener: (state: State) => void) => { const subscription = state$.subscribe(listener); return () => subscription.unsubscribe(); }, diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts diff --git a/src/plugins/kibana_utils/public/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/index.ts rename to src/plugins/kibana_utils/common/state_containers/index.ts diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts similarity index 88% rename from src/plugins/kibana_utils/public/state_containers/types.ts rename to src/plugins/kibana_utils/common/state_containers/types.ts index 5f27a3d2c1dca..26a29bc470e8a 100644 --- a/src/plugins/kibana_utils/public/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; +import { Ensure } from '@kbn/utility-types'; export type BaseState = object; export interface TransitionDescription { @@ -27,7 +27,7 @@ export interface TransitionDescription = (...args: Args) => State; export type PureTransition = ( - state: RecursiveReadonly + state: State ) => Transition; export type EnsurePureTransition = Ensure>; export type PureTransitionToTransition> = ReturnType; @@ -36,9 +36,9 @@ export type PureTransitionsToTransitions = { }; export interface BaseStateContainer { - get: () => RecursiveReadonly; + get: () => State; set: (state: State) => void; - state$: Observable>; + state$: Observable; } export interface StateContainer< @@ -55,12 +55,12 @@ export interface ReduxLikeStateContainer< PureTransitions extends object = {}, PureSelectors extends object = {} > extends StateContainer { - getState: () => RecursiveReadonly; - reducer: Reducer>; - replaceReducer: (nextReducer: Reducer>) => void; + getState: () => State; + reducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; dispatch: (action: TransitionDescription) => void; - addMiddleware: (middleware: Middleware>) => void; - subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; + addMiddleware: (middleware: Middleware) => void; + subscribe: (listener: (state: State) => void) => () => void; } export type Dispatch = (action: T) => void; diff --git a/src/plugins/kibana_utils/demos/state_containers/counter.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts index 4ddf532c1506d..0484a906a60d3 100644 --- a/src/plugins/kibana_utils/demos/state_containers/counter.ts +++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer } from '../../public/state_containers'; +import { createStateContainer } from '../../common/state_containers'; interface State { count: number; diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts index e807783a56f31..0a07d721479b3 100644 --- a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts +++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer, PureTransition } from '../../public/state_containers'; +import { createStateContainer, PureTransition } from '../../common/state_containers'; export interface TodoItem { text: string; diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts index 2c426cae6733a..80c016950d224 100644 --- a/src/plugins/kibana_utils/demos/state_sync/url.ts +++ b/src/plugins/kibana_utils/demos/state_sync/url.ts @@ -18,7 +18,7 @@ */ import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc'; -import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { createKbnUrlStateStorage, syncState, diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 00c1c95028b4d..78828ad9c4b2d 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -25,7 +25,7 @@ export * from './field_wildcard'; export * from './parse'; export * from './render_complete'; export * from './resize_checker'; -export * from './state_containers'; +export * from '../common/state_containers'; export * from './storage'; export { hashedItemStore, HashedItemStore } from './storage/hashed_item_store'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 17f41483a0a21..c55c60f9b0f89 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer, createStateContainer } from '../state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { defaultState, pureTransitions, diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 28d133829e07c..ed57723f8f2b7 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -23,7 +23,7 @@ import defaultComparator from 'fast-deep-equal'; import { IStateSyncConfig } from './types'; import { IStateStorage } from './state_sync_state_storage'; import { distinctUntilChangedWithInitialValue } from '../../common'; -import { BaseState } from '../state_containers'; +import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; /** diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 3009c1d161a53..2acb466d92e92 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer } from '../state_containers/types'; +import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; export interface INullableBaseStateContainer From c86ee1b6ea001863fde399721febaf1704fd2fc0 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 3 Feb 2020 07:05:39 -0800 Subject: [PATCH 12/17] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20AppArch=20p?= =?UTF-8?q?lugins=20to=20CODEOWNERS=20(#56397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 add AppArch plugins to CODEOWNERS And sort the list alphabetically. * chore: 🤖 add @kbn/interpreter to AppArch CODEOWNERS Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2e2b20a46baed..0b0addf117f6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,15 +28,7 @@ /src/plugins/dev_tools/ @elastic/kibana-app # App Architecture -/src/plugins/data/ @elastic/kibana-app-arch -/src/plugins/embeddable/ @elastic/kibana-app-arch -/src/plugins/expressions/ @elastic/kibana-app-arch -/src/plugins/kibana_react/ @elastic/kibana-app-arch -/src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/ui_actions/ @elastic/kibana-app-arch -/src/plugins/visualizations/ @elastic/kibana-app-arch -/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/packages/kbn-interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/data/ @elastic/kibana-app-arch /src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch @@ -48,6 +40,19 @@ /src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/bfetch/ @elastic/kibana-app-arch +/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app-arch +/src/plugins/data/ @elastic/kibana-app-arch +/src/plugins/embeddable/ @elastic/kibana-app-arch +/src/plugins/expressions/ @elastic/kibana-app-arch +/src/plugins/inspector/ @elastic/kibana-app-arch +/src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-arch +/src/plugins/management/ @elastic/kibana-app-arch +/src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/ui_actions/ @elastic/kibana-app-arch +/src/plugins/visualizations/ @elastic/kibana-app-arch +/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui From 95e40e7fa3a7be1b9818c67177f3e8f4927eb9ba Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 3 Feb 2020 16:22:38 +0100 Subject: [PATCH 13/17] [Uptime] Refresh absolute date ranges for Ping Histogram (#56381) * fix abs date mismatch * fixed types * update pr * simplify params Co-authored-by: Elastic Machine --- .../connected/charts/ping_histogram.tsx | 36 +- .../connected/charts/snapshot_container.tsx | 95 +++ .../filter_group/filter_group_container.tsx | 17 +- .../public/components/connected/index.ts | 3 + .../monitor/status_bar_container.tsx | 85 +++ .../monitor/status_details_container.tsx | 64 ++ .../monitor_charts.test.tsx.snap | 2 - .../__tests__/monitor_charts.test.tsx | 2 - .../functional/__tests__/snapshot.test.tsx | 4 +- .../monitor_bar_series.test.tsx.snap | 156 +++-- .../__tests__/monitor_bar_series.test.tsx | 159 ++++- .../functional/charts/monitor_bar_series.tsx | 25 +- .../public/components/functional/index.ts | 2 - .../components/functional/monitor_charts.tsx | 26 +- .../__snapshots__/monitor_list.test.tsx.snap | 554 ++++++++++++++++++ .../monitor_list_pagination.test.tsx.snap | 4 +- .../__tests__/monitor_list.test.tsx | 25 +- .../monitor_list_pagination.test.tsx | 6 +- .../functional/monitor_list/monitor_list.tsx | 20 +- .../monitor_status.bar.test.tsx.snap | 0 .../__test__}/monitor_status.bar.test.tsx | 12 +- .../monitor_status_details/index.ts | 31 +- .../monitor_status_bar/index.ts | 44 +- .../monitor_status_bar/monitor_status_bar.tsx | 26 +- .../monitor_status_details.tsx | 27 +- .../public/components/functional/snapshot.tsx | 107 +--- .../components/functional/status_panel.tsx | 42 +- .../public/hooks/update_kuery_string.ts | 4 +- .../plugins/uptime/public/pages/monitor.tsx | 17 +- .../plugins/uptime/public/pages/overview.tsx | 11 +- 30 files changed, 1148 insertions(+), 458 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx rename x-pack/legacy/plugins/uptime/public/components/functional/{__tests__ => monitor_status_details/__test__}/__snapshots__/monitor_status.bar.test.tsx.snap (100%) rename x-pack/legacy/plugins/uptime/public/components/functional/{__tests__ => monitor_status_details/__test__}/monitor_status.bar.test.tsx (79%) diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx index a6607ca81fc18..cbdd921a36e81 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx @@ -15,34 +15,40 @@ import { getPingHistogram } from '../../../state/actions'; import { selectPingHistogram } from '../../../state/selectors'; import { withResponsiveWrapper, ResponsiveWrapperProps } from '../../higher_order'; import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; +import { useUrlParams } from '../../../hooks'; -type Props = GetPingHistogramParams & - ResponsiveWrapperProps & - PingHistogramComponentProps & - DispatchProps & { lastRefresh: number }; +type Props = ResponsiveWrapperProps & + Pick & + DispatchProps & { lastRefresh: number; monitorId?: string }; const PingHistogramContainer: React.FC = ({ data, loadData, - statusFilter, - filters, - dateStart, - dateEnd, - absoluteStartDate, - absoluteEndDate, monitorId, lastRefresh, - ...props + height, + loading, }) => { + const [getUrlParams] = useUrlParams(); + const { + absoluteDateRangeStart, + absoluteDateRangeEnd, + dateRangeStart: dateStart, + dateRangeEnd: dateEnd, + statusFilter, + filters, + } = getUrlParams(); + useEffect(() => { loadData({ monitorId, dateStart, dateEnd, statusFilter, filters }); }, [loadData, dateStart, dateEnd, monitorId, filters, statusFilter, lastRefresh]); return ( ); }; @@ -68,7 +74,7 @@ const mapDispatchToProps = (dispatch: any): DispatchProps => ({ export const PingHistogram = connect< StateProps, DispatchProps, - PingHistogramComponentProps, + Pick, AppState >( mapStateToProps, diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx new file mode 100644 index 0000000000000..6d01ebae1e100 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.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, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { fetchSnapshotCount } from '../../../state/actions'; +import { SnapshotComponent } from '../../functional/snapshot'; +import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; + +/** + * Props expected from parent components. + */ +interface OwnProps { + /** + * Height is needed, since by default charts takes height of 100% + */ + height?: string; +} + +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} + +/** + * Contains functions that will dispatch actions used + * for this component's life cycle + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. + */ +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC = ({ + count, + height, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + const [getUrlParams] = useUrlParams(); + const { dateRangeStart, dateRangeEnd, statusFilter, filters } = getUrlParams(); + + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); + return ; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx index 2d1c21d1c997d..569c6bb883cbd 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { useUrlParams } from '../../../hooks'; import { parseFiltersMap } from '../../functional/filter_group/parse_filter_map'; @@ -12,6 +12,7 @@ import { AppState } from '../../../state'; import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; import { FilterGroupComponent } from '../../functional/filter_group'; import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; +import { UptimeRefreshContext } from '../../../contexts'; interface OwnProps { esFilters?: string; @@ -37,8 +38,9 @@ export const Container: React.FC = ({ loadFilterGroup, overviewFilters, }: Props) => { - const [getUrlParams, updateUrl] = useUrlParams(); + const { lastRefresh } = useContext(UptimeRefreshContext); + const [getUrlParams, updateUrl] = useUrlParams(); const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = getUrlParams(); useEffect(() => { @@ -53,7 +55,16 @@ export const Container: React.FC = ({ statusFilter, tags: filterSelections.tags ?? [], }); - }, [dateRangeStart, dateRangeEnd, esKuery, esFilters, statusFilter, urlFilters, loadFilterGroup]); + }, [ + lastRefresh, + dateRangeStart, + dateRangeEnd, + esKuery, + esFilters, + statusFilter, + urlFilters, + loadFilterGroup, + ]); // update filters in the URL from filter group const onFilterUpdate = (filtersKuery: string) => { diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 5bb0d1ae8468f..2fd4c762cf45f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -5,6 +5,9 @@ */ export { PingHistogram } from './charts/ping_histogram'; +export { Snapshot } from './charts/snapshot_container'; export { KueryBar } from './kuerybar/kuery_bar_container'; export { OverviewPage } from './pages/overview_container'; export { FilterGroup } from './filter_group/filter_group_container'; +export { MonitorStatusDetails } from './monitor/status_details_container'; +export { MonitorStatusBar } from './monitor/status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx new file mode 100644 index 0000000000000..db6337732091a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -0,0 +1,85 @@ +/* + * 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 { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AppState } from '../../../state'; +import { selectMonitorLocations, selectMonitorStatus } from '../../../state/selectors'; +import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; +import { getMonitorStatus, getSelectedMonitor } from '../../../state/actions'; +import { useUrlParams } from '../../../hooks'; +import { Ping } from '../../../../common/graphql/types'; +import { MonitorLocations } from '../../../../common/runtime_types/monitor'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface StateProps { + monitorStatus: Ping; + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void; +} + +interface OwnProps { + monitorId: string; +} + +type Props = OwnProps & StateProps & DispatchProps; + +export const Container: React.FC = ({ + loadMonitorStatus, + monitorId, + monitorStatus, + monitorLocations, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorStatus(dateStart, dateEnd, monitorId); + }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]); + + return ( + + ); +}; + +const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({ + monitorStatus: selectMonitorStatus(state), + monitorLocations: selectMonitorLocations(state, ownProps.monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => { + dispatch( + getMonitorStatus({ + monitorId, + dateStart, + dateEnd, + }) + ); + dispatch( + getSelectedMonitor({ + monitorId, + }) + ); + }, +}); + +// @ts-ignore TODO: Investigate typescript issues here +export const MonitorStatusBar = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx new file mode 100644 index 0000000000000..6929e3bd64c4d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.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, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { selectMonitorLocations } from '../../../state/selectors'; +import { fetchMonitorLocations, MonitorLocationsPayload } from '../../../state/actions/monitor'; +import { MonitorStatusDetailsComponent } from '../../functional/monitor_status_details'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface OwnProps { + monitorId: string; +} + +interface StoreProps { + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorLocations: typeof fetchMonitorLocations; +} + +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC = ({ + loadMonitorLocations, + monitorLocations, + monitorId, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorLocations({ dateStart, dateEnd, monitorId }); + }, [loadMonitorLocations, monitorId, dateStart, dateEnd, lastRefresh]); + + return ( + + ); +}; +const mapStateToProps = (state: AppState, { monitorId }: OwnProps) => ({ + monitorLocations: selectMonitorLocations(state, monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + loadMonitorLocations: (params: MonitorLocationsPayload) => { + dispatch(fetchMonitorLocations(params)); + }, +}); + +export const MonitorStatusDetails = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap index f6846dfb1164d..9853ed5cadfc9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap @@ -180,8 +180,6 @@ exports[`MonitorCharts component renders the component without errors 1`] = ` }, } } - dateRangeEnd="2011-12-03T10:15:30+01:00" - dateRangeStart="2011-12-03T10:15:30+01:00" loading={false} mean="mean" monitorId="something" diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx index 81c60c8fbeaaa..331b5c9c0b096 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx @@ -73,8 +73,6 @@ describe('MonitorCharts component', () => { range="range" success="success" monitorId="something" - dateRangeStart="2011-12-03T10:15:30+01:00" - dateRangeEnd="2011-12-03T10:15:30+01:00" /> ) ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index d645eb21ac776..214b0394369f7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Snapshot } from '../../../../common/runtime_types'; -import { PresentationalComponent } from '../snapshot'; +import { SnapshotComponent } from '../snapshot'; describe('Snapshot component', () => { const snapshot: Snapshot = { @@ -17,7 +17,7 @@ describe('Snapshot component', () => { }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index c3b99c9785cbe..8ca73879cab8c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -1,67 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MonitorBarSeries component renders a series when there are down items 1`] = ` +exports[`MonitorBarSeries component renders if the data series is present 1`] = `
- - - - - +
+
+

+ No data to display +

+
+
+
`; + +exports[`MonitorBarSeries component shallow renders a series when there are down items 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx index 3cede0be00ef1..c3e98134e438d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx @@ -5,15 +5,16 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series'; +import { renderWithRouter } from '../../../../lib'; +import { SummaryHistogramPoint } from '../../../../../common/graphql/types'; describe('MonitorBarSeries component', () => { let props: MonitorBarSeriesProps; + let histogramSeries: SummaryHistogramPoint[]; beforeEach(() => { props = { - absoluteStartDate: 1548697920000, - absoluteEndDate: 1548700920000, dangerColor: 'A danger color', histogramSeries: [ { @@ -33,20 +34,144 @@ describe('MonitorBarSeries component', () => { }, ], }; + histogramSeries = [ + { timestamp: 1580387868000, up: 0, down: 5 }, + { timestamp: 1580387904000, up: 0, down: 20 }, + { + timestamp: 1580387940000, + up: 0, + down: 19, + }, + { + timestamp: 1580387976000, + up: 0, + down: 16, + }, + { + timestamp: 1580388012000, + up: 0, + down: 20, + }, + { + timestamp: 1580388048000, + up: 0, + down: 15, + }, + { + timestamp: 1580388084000, + up: 0, + down: 20, + }, + { + timestamp: 1580388120000, + up: 0, + down: 19, + }, + { + timestamp: 1580388156000, + up: 0, + down: 16, + }, + { + timestamp: 1580388192000, + up: 0, + down: 20, + }, + { + timestamp: 1580388228000, + up: 0, + down: 15, + }, + { + timestamp: 1580388264000, + up: 0, + down: 20, + }, + { + timestamp: 1580388300000, + up: 0, + down: 19, + }, + { + timestamp: 1580388336000, + up: 0, + down: 16, + }, + { + timestamp: 1580388372000, + up: 0, + down: 20, + }, + { + timestamp: 1580388408000, + up: 0, + down: 15, + }, + { + timestamp: 1580388444000, + up: 0, + down: 20, + }, + { + timestamp: 1580388480000, + up: 0, + down: 19, + }, + { + timestamp: 1580388516000, + up: 0, + down: 16, + }, + { + timestamp: 1580388552000, + up: 0, + down: 20, + }, + { + timestamp: 1580388588000, + up: 0, + down: 15, + }, + { + timestamp: 1580388624000, + up: 0, + down: 20, + }, + { + timestamp: 1580388660000, + up: 0, + down: 19, + }, + { + timestamp: 1580388696000, + up: 0, + down: 16, + }, + { + timestamp: 1580388732000, + up: 0, + down: 20, + }, + { + timestamp: 1580388768000, + up: 0, + down: 10, + }, + ]; }); - it('renders a series when there are down items', () => { - const component = shallowWithIntl(); + it('shallow renders a series when there are down items', () => { + const component = shallowWithIntl(renderWithRouter()); expect(component).toMatchSnapshot(); }); - it('renders null when there are no down items', () => { + it('shallow renders null when there are no down items', () => { props.histogramSeries = []; - const component = shallowWithIntl(); + const component = shallowWithIntl(renderWithRouter()); expect(component).toEqual({}); }); - it('renders nothing if the down count has no counts', () => { + it(' shallow renders nothing if the down count has no counts', () => { props.histogramSeries = [ { timestamp: 123, @@ -64,19 +189,21 @@ describe('MonitorBarSeries component', () => { up: 0, }, ]; - const component = shallowWithIntl(); + const component = shallowWithIntl(renderWithRouter()); expect(component).toEqual({}); }); - it('renders nothing if the data series is null', () => { + it('shallow renders nothing if the data series is null', () => { const component = shallowWithIntl( - + renderWithRouter() ); expect(component).toEqual({}); }); + + it('renders if the data series is present', () => { + const component = renderWithIntl( + renderWithRouter() + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index ce91bf5b1638f..2338bf0278348 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -19,16 +19,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiToolTip } from '@elastic/eui'; import { SummaryHistogramPoint } from '../../../../common/graphql/types'; import { getChartDateLabel, seriesHasDownValues } from '../../../lib/helper'; +import { useUrlParams } from '../../../hooks'; export interface MonitorBarSeriesProps { - /** - * The date/time for the start of the timespan. - */ - absoluteStartDate: number; - /** - * The date/time for the end of the timespan. - */ - absoluteEndDate: number; /** * The color to use for the display of down states. */ @@ -44,23 +37,23 @@ export interface MonitorBarSeriesProps { * so we will only render the series component if there are down counts for the selected monitor. * @param props - the values for the monitor this chart visualizes */ -export const MonitorBarSeries = ({ - absoluteStartDate, - absoluteEndDate, - dangerColor, - histogramSeries, -}: MonitorBarSeriesProps) => { +export const MonitorBarSeries = ({ dangerColor, histogramSeries }: MonitorBarSeriesProps) => { + const [getUrlParams] = useUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); + const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? (
- + ; -export const MonitorChartsComponent = ({ - data, - mean, - range, - monitorId, - dateRangeStart, - dateRangeEnd, - loading, -}: Props) => { - const [getUrlParams] = useUrlParams(); +export const MonitorChartsComponent = ({ data, mean, range, monitorId, loading }: Props) => { if (data && data.monitorChartsData) { const { monitorChartsData: { locationDurationLines }, } = data; - const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); - return ( @@ -58,15 +44,7 @@ export const MonitorChartsComponent = ({ /> - + ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index f779efca7b18a..3655db5aaff1e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -106,6 +106,560 @@ exports[`MonitorList component renders a no items message when no data is provid `; exports[`MonitorList component renders the monitor list 1`] = ` +.c1 { + padding-left: 17px; +} + +.c2 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +
+
+ Monitor status +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Status + +
+
+
+ + Name + +
+
+
+ + Url + +
+
+
+ + Downtime history + +
+
+
+ +
+
+
+ Status +
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+ 1897 Yr ago +
+
+
+
+
+
+
+ in 0/1 Location +
+
+
+
+
+
+ Name +
+ +
+
+ Url +
+
+ +
+
+
+ +
+
+ -- +
+
+
+
+
+
+ +
+
+
+ Status +
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+ 1895 Yr ago +
+
+
+
+
+
+
+ in 1/1 Location +
+
+
+
+
+
+ Name +
+ +
+
+ Url +
+
+ +
+
+
+ +
+
+ -- +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+`; + +exports[`MonitorList component shallow renders the monitor list 1`] = ` `; -exports[`MonitorList component renders the monitor list 1`] = ` +exports[`MonitorListPagination component renders the monitor list 1`] = ` { let result: MonitorSummaryResult; @@ -81,11 +82,9 @@ describe('MonitorList component', () => { }; }); - it('renders the monitor list', () => { + it('shallow renders the monitor list', () => { const component = shallowWithIntl( { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( { ); expect(component).toMatchSnapshot(); }); + + it('renders the monitor list', () => { + const component = renderWithIntl( + renderWithRouter( + + ) + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx index a172513409455..ff54e61006156 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../common/graphql/types'; import { MonitorListComponent } from '../monitor_list'; -describe('MonitorList component', () => { +describe('MonitorListPagination component', () => { let result: MonitorSummaryResult; beforeEach(() => { @@ -98,8 +98,6 @@ describe('MonitorList component', () => { it('renders the monitor list', () => { const component = shallowWithIntl( { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( { - const { - absoluteStartDate, - absoluteEndDate, - dangerColor, - data, - errors, - hasActiveFilters, - linkParameters, - loading, - } = props; + const { dangerColor, data, errors, hasActiveFilters, linkParameters, loading } = props; const [drawerIds, updateDrawerIds] = useState([]); const items = data?.monitorStates?.summaries ?? []; @@ -132,12 +121,7 @@ export const MonitorListComponent = (props: Props) => { show: false, }, render: (histogramSeries: SummaryHistogramPoint[] | null) => ( - + ), }, { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 79% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx index 545405f91d537..0a53eeb89d793 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -7,14 +7,12 @@ import moment from 'moment'; import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; -import { Ping } from '../../../../common/graphql/types'; -import { MonitorStatusBarComponent } from '../monitor_status_details/monitor_status_bar'; +import { MonitorStatusBarComponent } from '../monitor_status_bar'; +import { Ping } from '../../../../../common/graphql/types'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; let monitorLocations: any; - let dateStart: string; - let dateEnd: string; beforeEach(() => { monitorStatus = { @@ -46,9 +44,6 @@ describe('MonitorStatusBar component', () => { }, ], }; - - dateStart = moment('01-01-2010').toString(); - dateEnd = moment('10-10-2010').toString(); }); it('renders duration in ms, not us', () => { @@ -56,10 +51,7 @@ describe('MonitorStatusBar component', () => { ); expect(component).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts index 7b4e1ea353c11..385788cc825a0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts @@ -3,34 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { selectMonitorLocations } from '../../../state/selectors'; -import { fetchMonitorLocations } from '../../../state/actions/monitor'; -import { MonitorStatusDetailsComponent } from './monitor_status_details'; -const mapStateToProps = (state: AppState, { monitorId }: any) => ({ - monitorLocations: selectMonitorLocations(state, monitorId), -}); - -const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ - loadMonitorLocations: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - fetchMonitorLocations({ - monitorId, - dateStart, - dateEnd, - }) - ); - }, -}); - -export const MonitorStatusDetails = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorStatusDetailsComponent); - -export * from './monitor_status_details'; -export { MonitorStatusBar } from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; +export { MonitorStatusDetailsComponent } from './monitor_status_details'; export { StatusByLocations } from './monitor_status_bar/status_by_location'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts index 94bd7fa7f026b..0cb11587eee48 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts @@ -4,47 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { - StateProps, - DispatchProps, - MonitorStatusBarComponent, - MonitorStatusBarProps, -} from './monitor_status_bar'; -import { selectMonitorStatus, selectMonitorLocations } from '../../../../state/selectors'; -import { AppState } from '../../../../state'; -import { getMonitorStatus, getSelectedMonitor } from '../../../../state/actions'; - -const mapStateToProps = (state: AppState, ownProps: MonitorStatusBarProps) => ({ - monitorStatus: selectMonitorStatus(state), - monitorLocations: selectMonitorLocations(state, ownProps.monitorId), -}); - -const mapDispatchToProps = (dispatch: Dispatch, ownProps: MonitorStatusBarProps) => ({ - loadMonitorStatus: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - getMonitorStatus({ - monitorId, - dateStart, - dateEnd, - }) - ); - dispatch( - getSelectedMonitor({ - monitorId, - }) - ); - }, -}); - -// @ts-ignore TODO: Investigate typescript issues here -export const MonitorStatusBar = connect( - // @ts-ignore TODO: Investigate typescript issues here - mapStateToProps, - mapDispatchToProps -)(MonitorStatusBarComponent); - export { MonitorSSLCertificate } from './monitor_ssl_certificate'; -export * from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx index 2524039829add..22e4377944be1 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { EuiLink, EuiTitle, @@ -13,42 +14,23 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import React, { useEffect } from 'react'; import { MonitorSSLCertificate } from './monitor_ssl_certificate'; import * as labels from './translations'; import { StatusByLocations } from './status_by_location'; import { Ping } from '../../../../../common/graphql/types'; import { MonitorLocations } from '../../../../../common/runtime_types'; -export interface StateProps { +interface MonitorStatusBarProps { + monitorId: string; monitorStatus: Ping; monitorLocations: MonitorLocations; } -export interface DispatchProps { - loadMonitorStatus: () => void; -} - -export interface MonitorStatusBarProps { - monitorId: string; - dateStart: string; - dateEnd: string; -} - -type Props = MonitorStatusBarProps & StateProps & DispatchProps; - -export const MonitorStatusBarComponent: React.FC = ({ - dateStart, - dateEnd, +export const MonitorStatusBarComponent: React.FC = ({ monitorId, - loadMonitorStatus, monitorStatus, monitorLocations, }) => { - useEffect(() => { - loadMonitorStatus(); - }, [dateStart, dateEnd, loadMonitorStatus]); - const full = monitorStatus?.url?.full ?? ''; return ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx index 29bd8eb3a7183..7dea73da7bba0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx @@ -8,16 +8,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; import { LocationMap } from '../location_map'; -import { MonitorStatusBar } from './monitor_status_bar'; import { UptimeRefreshContext } from '../../../contexts'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorStatusBar } from '../../connected'; -interface MonitorStatusBarProps { +interface MonitorStatusDetailsProps { monitorId: string; - variables: any; - loadMonitorLocations: any; - monitorLocations: any; - dateStart: any; - dateEnd: any; + monitorLocations: MonitorLocations; } const WrapFlexItem = styled(EuiFlexItem)` @@ -28,15 +25,8 @@ const WrapFlexItem = styled(EuiFlexItem)` export const MonitorStatusDetailsComponent = ({ monitorId, - variables, - loadMonitorLocations, monitorLocations, - dateStart, - dateEnd, -}: MonitorStatusBarProps) => { - useEffect(() => { - loadMonitorLocations(monitorId); - }, [loadMonitorLocations, monitorId, dateStart, dateEnd]); +}: MonitorStatusDetailsProps) => { const { refreshApp } = useContext(UptimeRefreshContext); const [isTabActive] = useState(document.visibilityState); @@ -63,12 +53,7 @@ export const MonitorStatusDetailsComponent = ({ - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 90d716001cff9..8531cd1a3cc83 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,63 +5,28 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useEffect } from 'react'; +import React from 'react'; import { get } from 'lodash'; -import { connect } from 'react-redux'; -import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; -import { AppState } from '../../state'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -/** - * Props expected from parent components. - */ -interface OwnProps { - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; - statusFilter?: string; -} - -/** - * Props given by the Redux store based on action input. - */ -interface StoreProps { +interface SnapshotComponentProps { count: SnapshotType; - lastRefresh: number; loading: boolean; + height?: string; } /** - * Contains functions that will dispatch actions used - * for this component's life cycle - */ -interface DispatchProps { - loadSnapshotCount: typeof fetchSnapshotCount; -} - -/** - * Props used to render the Snapshot component. + * This component visualizes a KPI and histogram chart to help users quickly + * glean the status of their uptime environment. + * @param props the props required by the component */ -type Props = OwnProps & StoreProps & DispatchProps; - -type PresentationalComponentProps = Pick & - Pick; - -export const PresentationalComponent: React.FC = ({ - count, - height, - loading, -}) => ( +export const SnapshotComponent: React.FC = ({ count, height, loading }) => ( (count, 'down', 0)} total={get(count, 'total', 0)} /> @@ -73,59 +38,3 @@ export const PresentationalComponent: React.FC = ( /> ); - -/** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component - */ -export const Container: React.FC = ({ - count, - dateRangeStart, - dateRangeEnd, - filters, - height, - statusFilter, - lastRefresh, - loading, - loadSnapshotCount, -}: Props) => { - useEffect(() => { - loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); - }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); - return ; -}; - -/** - * Provides state to connected component. - * @param state the root app state - */ -const mapStateToProps = ({ - snapshot: { count, loading }, - ui: { lastRefresh }, -}: AppState): StoreProps => ({ - count, - lastRefresh, - loading, -}); - -/** - * Used for fetching snapshot counts. - * @param dispatch redux-provided action dispatcher - */ -const mapDispatchToProps = (dispatch: any) => ({ - loadSnapshotCount: ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: string, - statusFilter?: string - ): DispatchProps => { - return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); - }, -}); - -export const Snapshot = connect( - // @ts-ignore connect is expecting null | undefined for some reason - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index 03ab9fb5cf194..2c0be2aa15d6f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -4,52 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { Snapshot } from './snapshot'; -import { PingHistogram } from '../connected'; - -interface StatusPanelProps { - absoluteDateRangeStart: number; - absoluteDateRangeEnd: number; - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - statusFilter?: string; -} +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { PingHistogram, Snapshot } from '../connected'; const STATUS_CHART_HEIGHT = '160px'; -export const StatusPanel = ({ - absoluteDateRangeStart, - absoluteDateRangeEnd, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, -}: StatusPanelProps) => ( +export const StatusPanel = ({}) => ( - + - + diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts index d02a6fc2afb5d..8c9eec3abe223 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts @@ -55,9 +55,9 @@ export const useUpdateKueryString = ( const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, indexPattern); esFilters = JSON.stringify(elasticsearchQuery); - - updateEsQueryForFilterGroup(filterQueryString, indexPattern); } + updateEsQueryForFilterGroup(filterQueryString, indexPattern); + return [esFilters]; } catch (err) { return [urlFilters, err]; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 408d2584911e0..a8501ff14313a 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -12,8 +12,8 @@ import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeRefreshContext, UptimeThemeContext } from '../contexts'; import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../infra/public'; -import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; import { PageHeader } from './page_header'; +import { MonitorStatusDetails } from '../components/connected'; interface MonitorPageProps { setBreadcrumbs: UMUpdateBreadcrumbs; @@ -49,20 +49,9 @@ export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { - + - + } - + Date: Mon, 3 Feb 2020 10:12:32 -0600 Subject: [PATCH 14/17] [SIEM] Use Core HTTP Client (#54210) * Replace uses of chrome.getBasePath and fetch with core.http While core.http is coming from 'above' these functions, it doesn't make a lot of sense to pass the client through the entire stack to be able to call it at the bottom, because longer-term we'll abstract these http calls with some redux middleware. In the short term we have a mechanism to refer to core through a singleton, 'ui/new_platform', which should be around until 8.0 at least. Ideally, we'll have a more robust architecture in place by then. If not, we can reproduce the singleton module ourselves. * Fix index patterns API call core.http.fetch doesn't like a querystring in the path argument, so we move it to the query object instead. The 'type' field specifier is redundant as the type is always returned (and not as an attribute). * Refactor getIndexPatterns to use the savedObjects client We lose the updated_at field by using the client, but we weren't actually using it. I don't _think_ that the savedObjects client supports request aborts right now, but when it does we'll get that back for free. * Pass query params as object to core.http A request with query params in its path does not properly encode said params (the '?', at the very least), leading to malformed requests that result in 404s. * Remove redundant API logic This function was originally used to query both an individual rule and a list of rules, but the former functionality has been moved to fetchRuleById. * Allow throwIfNotOk to handle an undefined response This is also an error for us, and we throw as such.. * Convert new Rules APIs to use core.http These all occurred on master, this fixes them post-merge. * Refactor Signals requests expecting custom Errors These requests package up error responses in custom errors, which callers are expecting. We should refactor all of these calls to behave similarly, but for now let's just not break existing ones. * Remove default credentials specification The default is credentials: 'same-origin', and so we can omit it. * Update types in new uses of hook This savedObject type is slightly modified now that the hook is using the NP savedObjects client. * Replace explicit system header with fetch option The asSystemRequest option accomplishes the same thing without requiring us to know the implementation. With the addition of this option, setting this header explicitly causes an error. This also removes the credentials: same-origin specifier as it is the default. * Remove redundant awaits The response has previously been resolved, and so our body should be populated, here. Co-authored-by: Elastic Machine --- .../components/embeddables/__mocks__/mock.ts | 20 +- .../embeddables/embedded_map_helpers.tsx | 2 +- .../components/ml/api/anomalies_table_data.ts | 30 +-- .../components/ml/api/get_ml_capabilities.ts | 17 +- .../siem/public/components/ml_popover/api.tsx | 191 ++++++++------- .../public/components/ml_popover/types.ts | 10 - .../containers/detection_engine/rules/api.ts | 223 +++++++----------- .../detection_engine/rules/types.ts | 9 + .../detection_engine/signals/api.ts | 123 ++++------ .../siem/public/hooks/api/__mock__/api.tsx | 6 +- .../plugins/siem/public/hooks/api/api.test.ts | 4 + .../plugins/siem/public/hooks/api/api.tsx | 43 ++-- .../legacy/plugins/siem/public/hooks/types.ts | 23 +- .../siem/public/hooks/use_index_patterns.tsx | 8 +- x-pack/legacy/plugins/siem/public/plugin.tsx | 1 + 15 files changed, 299 insertions(+), 411 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index 7834bb4511dc6..19ad0d452feb1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -5,7 +5,7 @@ */ import { IndexPatternMapping } from '../types'; -import { IndexPatternSavedObject } from '../../ml_popover/types'; +import { IndexPatternSavedObject } from '../../../hooks/types'; export const mockIndexPatternIds: IndexPatternMapping[] = [ { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, @@ -425,8 +425,7 @@ export const mockLayerListMixed = [ export const mockAPMIndexPattern: IndexPatternSavedObject = { id: 'apm-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*', }, @@ -435,8 +434,7 @@ export const mockAPMIndexPattern: IndexPatternSavedObject = { export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { id: 'apm-7.*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-7.*', }, @@ -445,8 +443,7 @@ export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { export const mockFilebeatIndexPattern: IndexPatternSavedObject = { id: 'filebeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'filebeat-*', }, @@ -455,8 +452,7 @@ export const mockFilebeatIndexPattern: IndexPatternSavedObject = { export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { id: 'auditbeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'auditbeat-*', }, @@ -465,8 +461,7 @@ export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { id: 'apm-*-transaction*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*-transaction*', }, @@ -475,8 +470,7 @@ export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { export const mockGlobIndexPattern: IndexPatternSavedObject = { id: '*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: '*', }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 2d4714401f3b3..e370cbbf64a4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -21,7 +21,7 @@ import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; -import { IndexPatternSavedObject } from '../ml_popover/types'; +import { IndexPatternSavedObject } from '../../hooks/types'; /** * Creates MapEmbeddable with provided initial configuration diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts index 10b2538d1e785..cb84d9182d2e0 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import { Anomalies, InfluencerInput, CriteriaFields } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; + export interface Body { jobIds: string[]; criteriaFields: CriteriaFields[]; @@ -22,17 +22,17 @@ export interface Body { } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify(body), - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch( + '/api/ml/results/anomalies_table_data', + { + method: 'POST', + body: JSON.stringify(body), + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts index 1333951028494..dcfd7365f8e0d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { InfluencerInput, MlCapabilities } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; @@ -23,16 +23,13 @@ export interface Body { } export const getMlCapabilities = async (signal: AbortSignal): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, { + const response = await npStart.core.http.fetch('/api/ml/ml_capabilities', { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx index a04b8f4b99653..cf939d8e09b7e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { CheckRecognizerProps, CloseJobsResponse, @@ -31,21 +32,18 @@ export const checkRecognizer = async ({ indexPatternName, signal, }: CheckRecognizerProps): Promise => { - const response = await fetch( - `${chrome.getBasePath()}/api/ml/modules/recognize/${indexPatternName}`, + const response = await npStart.core.http.fetch( + `/api/ml/modules/recognize/${indexPatternName}`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -55,18 +53,18 @@ export const checkRecognizer = async ({ * @param signal to cancel request */ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/get_module/${moduleId}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch( + `/api/ml/modules/get_module/${moduleId}`, + { + method: 'GET', + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -77,7 +75,6 @@ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Pr * @param jobIdErrorFilter - if provided, filters all errors except for given jobIds * @param groups - list of groups to add to jobs being installed * @param prefix - prefix to be added to job name - * @param headers optional headers to add */ export const setupMlJob = async ({ configTemplate, @@ -86,25 +83,26 @@ export const setupMlJob = async ({ groups = ['siem'], prefix = '', }: MlSetupArgs): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - prefix, - groups, - indexPatternName, - startDatafeed: false, - useDedicatedIndex: true, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch( + `/api/ml/modules/setup/${configTemplate}`, + { + method: 'POST', + body: JSON.stringify({ + prefix, + groups, + indexPatternName, + startDatafeed: false, + useDedicatedIndex: true, + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttachedToSetup(json, jobIdErrorFilter); + return json; }; @@ -121,22 +119,23 @@ export const startDatafeeds = async ({ datafeedIds: string[]; start: number; }): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - ...(start !== 0 && { start }), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch( + '/api/ml/jobs/force_start_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + ...(start !== 0 && { start }), + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttached(json, datafeedIds); + return json; }; @@ -144,49 +143,46 @@ export const startDatafeeds = async ({ * Stops the given dataFeedIds and sets the corresponding Job's jobState to closed * * @param datafeedIds - * @param headers optional headers to add */ export const stopDatafeeds = async ({ datafeedIds, }: { datafeedIds: string[]; }): Promise<[StopDatafeedResponse | ErrorResponse, CloseJobsResponse]> => { - const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const stopDatafeedsResponse = await npStart.core.http.fetch( + '/api/ml/jobs/stop_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(stopDatafeedsResponse); - const stopDatafeedsResponseJson = await stopDatafeedsResponse.json(); + await throwIfNotOk(stopDatafeedsResponse.response); + const stopDatafeedsResponseJson = stopDatafeedsResponse.body!; const datafeedPrefix = 'datafeed-'; - const closeJobsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/close_jobs`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - jobIds: datafeedIds.map(dataFeedId => - dataFeedId.startsWith(datafeedPrefix) - ? dataFeedId.substring(datafeedPrefix.length) - : dataFeedId - ), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const closeJobsResponse = await npStart.core.http.fetch( + '/api/ml/jobs/close_jobs', + { + method: 'POST', + body: JSON.stringify({ + jobIds: datafeedIds.map(dataFeedId => + dataFeedId.startsWith(datafeedPrefix) + ? dataFeedId.substring(datafeedPrefix.length) + : dataFeedId + ), + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(closeJobsResponse); - return [stopDatafeedsResponseJson, await closeJobsResponse.json()]; + await throwIfNotOk(closeJobsResponse.response); + return [stopDatafeedsResponseJson, closeJobsResponse.body!]; }; /** @@ -198,17 +194,14 @@ export const stopDatafeeds = async ({ * @param signal to cancel request */ export const getJobsSummary = async (signal: AbortSignal): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, { + const response = await npStart.core.http.fetch('/api/ml/jobs/jobs_summary', { method: 'POST', - credentials: 'same-origin', body: JSON.stringify({}), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts index 964ae8c8242d4..f3bf78fdbb94c 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts @@ -193,16 +193,6 @@ export interface CloseJobsResponse { }; } -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} - export interface JobsFilters { filterQuery: string; showCustomJobs: boolean; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8f4abeb31c226..4f50a9bd14788 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { AddRulesProps, DeleteRulesProps, @@ -19,8 +20,9 @@ import { ImportRulesProps, ExportRulesProps, RuleError, - RuleStatus, + RuleStatusResponse, ImportRulesResponse, + PrePackagedRulesStatusResponse, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -39,19 +41,15 @@ import * as i18n from '../../../pages/detection_engine/rules/translations'; * @param signal to cancel request */ export const addRule = async ({ rule, signal }: AddRulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_RULES_URL, { method: rule.id != null ? 'PUT' : 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(rule), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -79,40 +77,36 @@ export const fetchRules = async ({ signal, }: FetchRulesProps): Promise => { const filters = [ - ...(filterOptions.filter.length !== 0 - ? [`alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] - : []), + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), ...(filterOptions.showCustomRules - ? ['alert.attributes.tags:%20%22__internal_immutable:false%22'] + ? [`alert.attributes.tags: "__internal_immutable:false"`] : []), ...(filterOptions.showElasticRules - ? ['alert.attributes.tags:%20%22__internal_immutable:true%22'] + ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags:${encodeURIComponent(t)}`) ?? []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), ]; - const queryParams = [ - `page=${pagination.page}`, - `per_page=${pagination.perPage}`, - `sort_field=${filterOptions.sortField}`, - `sort_order=${filterOptions.sortOrder}`, - ...(filters.length > 0 ? [`filter=${filters.join('%20AND%20')}`] : []), - ]; + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_find`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -123,18 +117,15 @@ export const fetchRules = async ({ * */ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { id }, + asResponse: true, signal, }); - await throwIfNotOk(response); - const rule: Rule = await response.json(); - return rule; + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -146,21 +137,17 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(ids.map(id => ({ id, enabled }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -171,21 +158,17 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + method: 'PUT', body: JSON.stringify(ids.map(id => ({ id }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -194,15 +177,10 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify( rules.map(rule => ({ ...rule, @@ -223,11 +201,12 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); + + await throwIfNotOk(response.response); return true; }; @@ -266,21 +242,19 @@ export const importRules = async ({ const formData = new FormData(); formData.append('file', fileToImport); - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_import?overwrite=${overwrite}`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_import`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'kbn-xsrf': 'true', - }, + query: { overwrite }, body: formData, + asResponse: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -304,24 +278,19 @@ export const exportRules = async ({ ? JSON.stringify({ objects: ruleIds.map(rule => ({ rule_id: rule })) }) : undefined; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_export?exclude_export_details=${excludeExportDetails}&file_name=${encodeURIComponent( - filename - )}`, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body, - signal, - } - ); + const response = await npStart.core.http.fetch(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); - await throwIfNotOk(response); - return response.blob(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -338,24 +307,19 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS_URL}?ids=${encodeURIComponent( - JSON.stringify([id]) - )}`, +}): Promise => { + const response = await npStart.core.http.fetch( + DETECTION_ENGINE_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { ids: JSON.stringify([id]) }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -365,18 +329,14 @@ export const getRuleStatusById = async ({ * */ export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_TAGS_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_TAGS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -390,25 +350,16 @@ export const getPrePackagedRulesStatus = async ({ signal, }: { signal: AbortSignal; -}): Promise<{ - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -}> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL}`, +}): Promise => { + const response = await npStart.core.http.fetch( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index b30c3b211b1b8..0aaffb7b86b28 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -197,3 +197,12 @@ export interface RuleInfoStatus { last_failure_message: string | null; last_success_message: string | null; } + +export type RuleStatusResponse = Record; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 8754d73637e7c..d0da70e646124 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -14,40 +14,37 @@ import { DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../common/constants'; import { + BasicSignals, + PostSignalError, + Privilege, QuerySignals, + SignalIndexError, SignalSearchResponse, - UpdateSignalStatusProps, SignalsIndex, - SignalIndexError, - Privilege, - PostSignalError, - BasicSignals, + UpdateSignalStatusProps, } from './types'; -import { parseJsonFromBody } from '../../../utils/api'; /** * Fetch Signals by providing a query * * @param query String to match a dsl - * @param signal AbortSignal for cancelling request */ export const fetchQuerySignals = async ({ query, signal, }: QuerySignals): Promise> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body: JSON.stringify(query), - signal, - }); - await throwIfNotOk(response); - const signals = await response.json(); - return signals; + const response = await npStart.core.http.fetch>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + asResponse: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -62,19 +59,15 @@ export const updateSignalStatus = async ({ status, signal, }: UpdateSignalStatusProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_SIGNALS_STATUS_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify({ status, ...query }), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -82,25 +75,18 @@ export const updateSignalStatus = async ({ * * @param signal AbortSignal for cancelling request */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { + try { + return await npStart.core.http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + } catch (e) { + if (e.body) { + throw new SignalIndexError(e.body); + } + throw e; } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new SignalIndexError(error); - } - return null; }; /** @@ -108,19 +94,15 @@ export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PRIVILEGES_URL}`, { +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_PRIVILEGES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -128,23 +110,16 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; - } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new PostSignalError(error); +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => { + try { + return await npStart.core.http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); + } catch (e) { + if (e.body) { + throw new PostSignalError(e.body); + } + throw e; } - return null; }; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx index 13f53cd34feb6..b12b04e8f760b 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx @@ -13,8 +13,7 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'filebeat-*', }, - updated_at: '2019-08-26T04:30:09.111Z', - version: 'WzE4LLwxXQ==', + _version: 'WzE4LLwxXQ==', }, { type: 'index-pattern', @@ -22,7 +21,6 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'auditbeat-*', }, - updated_at: '2019-08-26T04:31:12.934Z', - version: 'WzELLywxXQ==', + _version: 'WzELLywxXQ==', }, ]; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts index 95825b7d4abda..208a3b14ca283 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts @@ -13,6 +13,10 @@ describe('api', () => { }); describe('#throwIfNotOk', () => { + test('throws a network error if there is no response', async () => { + await expect(throwIfNotOk()).rejects.toThrow('Network Error'); + }); + test('does a throw if it is given response that is not ok and the body is not parsable', async () => { fetchMock.mock('http://example.com', 500); const response = await fetch('http://example.com'); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx index 8d319ffe23902..f5f32da7d8c0b 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx @@ -4,46 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import * as i18n from '../translations'; import { parseJsonFromBody, ToasterErrors } from '../../components/ml/api/throw_if_not_ok'; -import { IndexPatternResponse, IndexPatternSavedObject } from '../types'; - -const emptyIndexPattern: IndexPatternSavedObject[] = []; +import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types'; /** * Fetches Configured Index Patterns from the Kibana saved objects API * * TODO: Refactor to context provider: https://github.com/elastic/siem-team/issues/448 - * - * @param signal */ -export const getIndexPatterns = async (signal: AbortSignal): Promise => { - const response = await fetch( - `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`, - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - } - ); - await throwIfNotOk(response); - const results: IndexPatternResponse = await response.json(); +export const getIndexPatterns = async (): Promise => { + const response = await npStart.core.savedObjects.client.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); - if (results.saved_objects && Array.isArray(results.saved_objects)) { - return results.saved_objects; - } else { - return emptyIndexPattern; - } + return response.savedObjects; }; -export const throwIfNotOk = async (response: Response): Promise => { +export const throwIfNotOk = async (response?: Response): Promise => { + if (!response) { + throw new ToasterErrors([i18n.NETWORK_ERROR]); + } + if (!response.ok) { const body = await parseJsonFromBody(response); if (body != null && body.message) { diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts index 4d66d8e191235..301b8bd655333 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/types.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/types.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} +import { SimpleSavedObject } from '../../../../../../src/core/public'; -export interface IndexPatternResponse { - page: number; - per_page: number; - saved_objects: IndexPatternSavedObject[]; - total: number; -} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx index 7abe88402096c..35bed69e8617e 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx @@ -8,10 +8,10 @@ import { useEffect, useState } from 'react'; import { useStateToaster } from '../components/toasters'; import { errorToToaster } from '../components/ml/api/error_to_toaster'; -import { IndexPatternSavedObject } from '../components/ml_popover/types'; -import { getIndexPatterns } from './api/api'; import * as i18n from './translations'; +import { IndexPatternSavedObject } from './types'; +import { getIndexPatterns } from './api/api'; type Return = [boolean, IndexPatternSavedObject[]]; @@ -22,12 +22,11 @@ export const useIndexPatterns = (refreshToggle = false): Return => { useEffect(() => { let isSubscribed = true; - const abortCtrl = new AbortController(); setIsLoading(true); async function fetchIndexPatterns() { try { - const data = await getIndexPatterns(abortCtrl.signal); + const data = await getIndexPatterns(); if (isSubscribed) { setIndexPatterns(data); @@ -44,7 +43,6 @@ export const useIndexPatterns = (refreshToggle = false): Return => { fetchIndexPatterns(); return () => { isSubscribed = false; - abortCtrl.abort(); }; }, [refreshToggle]); diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index 7911b5eb9833b..74fc913d2b573 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -41,6 +41,7 @@ export type Start = ReturnType; export class Plugin implements IPlugin { public id = 'siem'; public name = 'SIEM'; + constructor( // @ts-ignore this is added to satisfy the New Platform typing constraint, // but we're not leveraging any of its functionality yet. From 4f4d3d753c0fef8df7e4ac242d5f15609af243b8 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 3 Feb 2020 11:19:41 -0500 Subject: [PATCH 15/17] [Lens] Fix bugs in Lens filters (#56441) * [Lens] Fix bug where filters were not displayed * Fix #55603 Co-authored-by: Elastic Machine --- .../lens/public/app_plugin/app.test.tsx | 24 +++++++++++++++---- .../plugins/lens/public/app_plugin/app.tsx | 7 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 80a7ceb61c324..99926c646da22 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -60,10 +60,14 @@ function createMockFilterManager() { return unsubscribe; }, }), - setFilters: (newFilters: unknown[]) => { + setFilters: jest.fn((newFilters: unknown[]) => { filters = newFilters; - subscriber(); - }, + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore @@ -189,6 +193,13 @@ describe('Lens App', () => { `); }); + it('clears app filters on load', () => { + const defaultArgs = makeDefaultArgs(); + mount(); + + expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); const instance = mount(); @@ -226,7 +237,7 @@ describe('Lens App', () => { expect(args.docStorage.load).not.toHaveBeenCalled(); }); - it('loads a document and uses query if there is a document id', async () => { + it('loads a document and uses query and filters if there is a document id', async () => { const args = makeDefaultArgs(); args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ @@ -234,6 +245,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }); @@ -245,6 +257,9 @@ describe('Lens App', () => { expect(args.docStorage.load).toHaveBeenCalledWith('1234'); expect(args.data.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(args.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', @@ -260,6 +275,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 35e45af6a3d68..6d2ebee1d88db 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -83,6 +83,10 @@ export function App({ const { lastKnownDoc } = state; useEffect(() => { + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh + data.query.filterManager.setAppFilters([]); + const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); @@ -123,13 +127,14 @@ export function App({ core.notifications ) .then(indexPatterns => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(doc.state.filters); setState(s => ({ ...s, isLoading: false, persistedDoc: doc, lastKnownDoc: doc, query: doc.state.query, - filters: doc.state.filters, indexPatternsForTopNav: indexPatterns, })); }) From 479223b0a1acf4926a18f61e2c62c458019f747d Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 3 Feb 2020 19:29:59 +0200 Subject: [PATCH 16/17] Update plugin generator to generate NP plugins (#55281) * Generate NP plugin * Added tsconfig * tsconfig * Adjust sao test * Add server side to plugin gen * Added navigation * add empty element * eslint * platform team CR * design CR improvements * text updates * temp disable plugin gen tests * eslint * Code review fixes * Add scss support - requires #53976 to be merged to work * CR fixes * comment fixes * Don't generate eslint for internal plugins by default * Update tests * reenable jest test for sao * Fix regex * review comments * code review Co-authored-by: Elastic Machine --- packages/kbn-plugin-generator/index.js | 16 ++- packages/kbn-plugin-generator/index.js.d.ts | 24 ++++ .../integration_tests/generate_plugin.test.js | 18 +-- .../kbn-plugin-generator/sao_template/sao.js | 69 +++++----- .../sao_template/sao.test.js | 127 ++++------------- .../sao_template/template/.i18nrc.json | 9 -- .../template/.kibana-plugin-helpers.json | 3 - .../sao_template/template/README.md | 31 +---- .../sao_template/template/common/index.ts | 2 + .../sao_template/template/eslintrc.js | 31 ++--- .../sao_template/template/gitignore | 6 - .../sao_template/template/index.js | 89 ------------ .../sao_template/template/kibana.json | 8 ++ .../template/package_template.json | 41 ------ .../template/public/__tests__/index.js | 7 - .../sao_template/template/public/app.js | 45 ------ .../template/public/application.tsx | 25 ++++ .../template/public/components/app.tsx | 129 ++++++++++++++++++ .../template/public/components/main/index.js | 1 - .../template/public/components/main/main.js | 97 ------------- .../sao_template/template/public/hack.js | 7 - .../template/public/{app.scss => index.scss} | 0 .../sao_template/template/public/index.ts | 16 +++ .../sao_template/template/public/plugin.ts | 42 ++++++ .../sao_template/template/public/types.ts | 11 ++ .../template/server/__tests__/index.js | 7 - .../sao_template/template/server/index.ts | 15 ++ .../sao_template/template/server/plugin.ts | 30 ++++ .../template/server/routes/example.js | 11 -- .../template/server/routes/index.ts | 17 +++ .../sao_template/template/server/types.ts | 4 + .../template/translations/zh-CN.json | 84 ------------ packages/kbn-plugin-generator/tsconfig.json | 5 + 33 files changed, 412 insertions(+), 615 deletions(-) create mode 100644 packages/kbn-plugin-generator/index.js.d.ts delete mode 100644 packages/kbn-plugin-generator/sao_template/template/.i18nrc.json delete mode 100644 packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json create mode 100644 packages/kbn-plugin-generator/sao_template/template/common/index.ts mode change 100755 => 100644 packages/kbn-plugin-generator/sao_template/template/eslintrc.js delete mode 100755 packages/kbn-plugin-generator/sao_template/template/gitignore delete mode 100755 packages/kbn-plugin-generator/sao_template/template/index.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/kibana.json delete mode 100644 packages/kbn-plugin-generator/sao_template/template/package_template.json delete mode 100755 packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js delete mode 100755 packages/kbn-plugin-generator/sao_template/template/public/app.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/application.tsx create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx delete mode 100644 packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js delete mode 100644 packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js delete mode 100755 packages/kbn-plugin-generator/sao_template/template/public/hack.js rename packages/kbn-plugin-generator/sao_template/template/public/{app.scss => index.scss} (100%) create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/index.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/plugin.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/types.ts delete mode 100755 packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/index.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/plugin.ts delete mode 100755 packages/kbn-plugin-generator/sao_template/template/server/routes/example.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/types.ts delete mode 100644 packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json create mode 100644 packages/kbn-plugin-generator/tsconfig.json diff --git a/packages/kbn-plugin-generator/index.js b/packages/kbn-plugin-generator/index.js index 90274288357b8..15adce7f01c8e 100644 --- a/packages/kbn-plugin-generator/index.js +++ b/packages/kbn-plugin-generator/index.js @@ -29,6 +29,7 @@ exports.run = function run(argv) { const options = getopts(argv, { alias: { h: 'help', + i: 'internal', }, }); @@ -40,17 +41,22 @@ exports.run = function run(argv) { if (options.help) { console.log( dedent(chalk` - {dim usage:} node scripts/generate-plugin {bold [name]} - - generate a fresh Kibana plugin in the plugins/ directory + # {dim Usage:} + node scripts/generate-plugin {bold [name]} + Generate a fresh Kibana plugin in the plugins/ directory + + # {dim Core Kibana plugins:} + node scripts/generate-plugin {bold [name]} -i + To generate a core Kibana plugin inside the src/plugins/ directory, add the -i flag. `) + '\n' ); process.exit(1); } const name = options._[0]; + const isKibanaPlugin = options.internal; const template = resolve(__dirname, './sao_template'); - const kibanaPlugins = resolve(__dirname, '../../plugins'); + const kibanaPlugins = resolve(__dirname, isKibanaPlugin ? '../../src/plugins' : '../../plugins'); const targetPath = resolve(kibanaPlugins, snakeCase(name)); sao({ @@ -58,6 +64,8 @@ exports.run = function run(argv) { targetPath: targetPath, configOptions: { name, + isKibanaPlugin, + targetPath, }, }).catch(error => { console.error(chalk`{red fatal error}!`); diff --git a/packages/kbn-plugin-generator/index.js.d.ts b/packages/kbn-plugin-generator/index.js.d.ts new file mode 100644 index 0000000000000..46f7c43fd5790 --- /dev/null +++ b/packages/kbn-plugin-generator/index.js.d.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +interface PluginGenerator { + /** + * Run plugin generator. + */ + run: (...args: any[]) => any; +} diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index aa6611f3b6738..129125c4583d5 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -61,7 +61,8 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug expect(stats.isDirectory()).toBe(true); }); - it(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { + // skipped until internationalization is re-introduced + it.skip(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { // Link to the error that happens when the blank line is not there: // https://github.com/elastic/kibana/pull/45044#issuecomment-530092627 const intlFile = `${generatedPath}/.i18nrc.json`; @@ -78,16 +79,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug }); }); - it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { - cwd: generatedPath, - env: { - DISABLE_JUNIT_REPORTER: '1', - }, - }); - }); - - it(`'yarn build' should exit 0`, async () => { + it.skip(`'yarn build' should exit 0`, async () => { await execa('yarn', ['build'], { cwd: generatedPath }); }); @@ -109,7 +101,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug '--migrations.skip=true', ], cwd: generatedPath, - wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/, + wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), }); await proc.stop('kibana'); }); @@ -120,7 +112,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug await execa('yarn', ['preinstall'], { cwd: generatedPath }); }); - it(`'yarn lint' should exit 0`, async () => { + it.skip(`'yarn lint' should exit 0`, async () => { await execa('yarn', ['lint'], { cwd: generatedPath }); }); diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js index f7401cba84358..aed4b9a02838f 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.js +++ b/packages/kbn-plugin-generator/sao_template/sao.js @@ -17,21 +17,19 @@ * under the License. */ -const { resolve, relative, dirname } = require('path'); +const { relative } = require('path'); const startCase = require('lodash.startcase'); const camelCase = require('lodash.camelcase'); const snakeCase = require('lodash.snakecase'); -const execa = require('execa'); const chalk = require('chalk'); +const execa = require('execa'); const pkg = require('../package.json'); const kibanaPkgPath = require.resolve('../../../package.json'); const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require -const KBN_DIR = dirname(kibanaPkgPath); - -module.exports = function({ name }) { +module.exports = function({ name, targetPath, isKibanaPlugin }) { return { prompts: { description: { @@ -47,41 +45,38 @@ module.exports = function({ name }) { message: 'Should an app component be generated?', default: true, }, - generateTranslations: { - type: 'confirm', - message: 'Should translation files be generated?', - default: true, - }, - generateHack: { - type: 'confirm', - message: 'Should a hack component be generated?', - default: true, - }, generateApi: { type: 'confirm', message: 'Should a server API be generated?', default: true, }, + // generateTranslations: { + // type: 'confirm', + // message: 'Should translation files be generated?', + // default: true, + // }, generateScss: { type: 'confirm', message: 'Should SCSS be used?', when: answers => answers.generateApp, default: true, }, + generateEslint: { + type: 'confirm', + message: 'Would you like to use a custom eslint file?', + default: !isKibanaPlugin, + }, }, filters: { + 'public/**/index.scss': 'generateScss', 'public/**/*': 'generateApp', - 'translations/**/*': 'generateTranslations', - '.i18nrc.json': 'generateTranslations', - 'public/hack.js': 'generateHack', 'server/**/*': 'generateApi', - 'public/app.scss': 'generateScss', - '.kibana-plugin-helpers.json': 'generateScss', + // 'translations/**/*': 'generateTranslations', + // '.i18nrc.json': 'generateTranslations', + 'eslintrc.js': 'generateEslint', }, move: { - gitignore: '.gitignore', 'eslintrc.js': '.eslintrc.js', - 'package_template.json': 'package.json', }, data: answers => Object.assign( @@ -91,34 +86,36 @@ module.exports = function({ name }) { camelCase, snakeCase, name, + isKibanaPlugin, + kbnVersion: answers.kbnVersion, + upperCamelCaseName: name.charAt(0).toUpperCase() + camelCase(name).slice(1), + hasUi: !!answers.generateApp, + hasServer: !!answers.generateApi, + hasScss: !!answers.generateScss, + relRoot: isKibanaPlugin ? '../../../..' : '../../..', }, answers ), enforceNewFolder: true, installDependencies: false, - gitInit: true, + gitInit: !isKibanaPlugin, async post({ log }) { - await execa('yarn', ['kbn', 'bootstrap'], { - cwd: KBN_DIR, - stdio: 'inherit', - }); - - const dir = relative(process.cwd(), resolve(KBN_DIR, 'plugins', snakeCase(name))); + const dir = relative(process.cwd(), targetPath); + // Apply eslint to the generated plugin try { - await execa('yarn', ['lint', '--fix'], { - cwd: dir, - all: true, - }); + await execa('yarn', ['lint:es', `./${dir}/**/*.ts*`, '--no-ignore', '--fix']); } catch (error) { - throw new Error(`Failure when running prettier on the generated output: ${error.all}`); + console.error(error); + throw new Error( + `Failure when running prettier on the generated output: ${error.all || error}` + ); } log.success(chalk`🎉 -Your plugin has been created in {bold ${dir}}. Move into that directory to run it: +Your plugin has been created in {bold ${dir}}. - {bold cd "${dir}"} {bold yarn start} `); }, diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 80149c008dad8..0dbdb7d3c097b 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -19,8 +19,6 @@ const sao = require('sao'); -const templatePkg = require('../package.json'); - const template = { fromPath: __dirname, configOptions: { @@ -32,121 +30,57 @@ function getFileContents(file) { return file.contents.toString(); } -function getConfig(file) { - const contents = getFileContents(file).replace(/\r?\n/gm, ''); - return contents.split('kibana.Plugin(')[1]; -} - describe('plugin generator sao integration', () => { test('skips files when answering no', async () => { const res = await sao.mockPrompt(template, { generateApp: false, - generateHack: false, generateApi: false, }); - expect(res.fileList).not.toContain('public/app.js'); - expect(res.fileList).not.toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).not.toContain('app:'); - expect(uiExports).not.toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); - expect(uiExports).not.toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).not.toContain('public/index.ts'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes app when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: false, - generateApi: false, - }); - - // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - expect(uiExports).not.toContain('hacks:'); - }); - - it('includes hack when answering yes', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, generateApi: false, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).toContain('public/index.ts'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('public/types.ts'); + expect(res.fileList).toContain('public/components/app.tsx'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes server api when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).toContain('server/routes/example.js'); - expect(res.fileList).toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - }); - - it('plugin config has correct name and main path', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, - generateApi: true, - }); - - const indexContents = getFileContents(res.files['index.js']); - const nameLine = indexContents.match('name: (.*)')[1]; - const mainLine = indexContents.match('main: (.*)')[1]; - - expect(nameLine).toContain('some_fancy_plugin'); - expect(mainLine).toContain('plugins/some_fancy_plugin/app'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('server/plugin.ts'); + expect(res.fileList).toContain('server/index.ts'); + expect(res.fileList).toContain('server/types.ts'); + expect(res.fileList).toContain('server/routes/index.ts'); }); - it('plugin package has correct name', async () => { + it('plugin package has correct title', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_NAME = '(.*)'")[1]; - expect(pkg.name).toBe('some_fancy_plugin'); + expect(controllerLine).toContain('Some fancy plugin'); }); it('package has version "kibana" with master', async () => { @@ -154,10 +88,10 @@ describe('plugin generator sao integration', () => { kbnVersion: 'master', }); - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.version).toBe('kibana'); + expect(pkg.version).toBe('master'); }); it('package has correct version', async () => { @@ -165,39 +99,26 @@ describe('plugin generator sao integration', () => { kbnVersion: 'v6.0.0', }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); - - expect(pkg.kibana.version).toBe('v6.0.0'); - }); - - it('package has correct templateVersion', async () => { - const res = await sao.mockPrompt(template, { - kbnVersion: 'master', - }); - - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.templateVersion).toBe(templatePkg.version); + expect(pkg.version).toBe('v6.0.0'); }); it('sample app has correct values', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const contents = getFileContents(res.files['public/app.js']); - const controllerLine = contents.match('setRootController(.*)')[1]; + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_ID = '(.*)'")[1]; expect(controllerLine).toContain('someFancyPlugin'); }); it('includes dotfiles', async () => { const res = await sao.mockPrompt(template); - expect(res.files['.gitignore']).toBeTruthy(); expect(res.files['.eslintrc.js']).toBeTruthy(); }); }); diff --git a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json b/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json deleted file mode 100644 index 1a8aea8853876..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "paths": { - "<%= camelCase(name) %>": "./" - }, - "translations": [ - "translations/zh-CN.json" - ] -} - diff --git a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json b/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json deleted file mode 100644 index 383368c7f8ce1..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "styleSheetToCompile": "public/app.scss" -} diff --git a/packages/kbn-plugin-generator/sao_template/template/README.md b/packages/kbn-plugin-generator/sao_template/template/README.md index 59c3adf2713c8..1e0139428fcbc 100755 --- a/packages/kbn-plugin-generator/sao_template/template/README.md +++ b/packages/kbn-plugin-generator/sao_template/template/README.md @@ -6,34 +6,7 @@ --- -## development +## Development -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following yarn scripts. +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. - - `yarn kbn bootstrap` - - Install dependencies and crosslink Kibana and all projects/plugins. - - > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - - `yarn build` - - Build a distributable archive of your plugin. - - - `yarn test:browser` - - Run the browser tests in a real web browser. - - - `yarn test:mocha` - - Run the server tests using mocha. - -For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks checkout the `package.json` file, or run `yarn run`. diff --git a/packages/kbn-plugin-generator/sao_template/template/common/index.ts b/packages/kbn-plugin-generator/sao_template/template/common/index.ts new file mode 100644 index 0000000000000..90ffcb70045aa --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = '<%= camelCase(name) %>'; +export const PLUGIN_NAME = '<%= name %>'; diff --git a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js old mode 100755 new mode 100644 index e1dfadc212b7e..b68d42e32e047 --- a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js +++ b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js @@ -1,24 +1,9 @@ -module.exports = { - root: true, +module.exports = { + root: true, extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - overrides: [ - { - files: ['**/public/**/*'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - forceNode: false, - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - }, - ] -}; + <%_ if (!isKibanaPlugin) { -%> + rules: { + "@kbn/eslint/require-license-header": "off" + } + <%_ } -%> +}; \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/gitignore b/packages/kbn-plugin-generator/sao_template/template/gitignore deleted file mode 100755 index db28fed19376d..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/gitignore +++ /dev/null @@ -1,6 +0,0 @@ -npm-debug.log* -node_modules -/build/ -<%_ if (generateScss) { -%> -/public/app.css -<%_ } -%> diff --git a/packages/kbn-plugin-generator/sao_template/template/index.js b/packages/kbn-plugin-generator/sao_template/template/index.js deleted file mode 100755 index 4bc3347ae6019..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/index.js +++ /dev/null @@ -1,89 +0,0 @@ -<% if (generateScss) { -%> -import { resolve } from 'path'; -import { existsSync } from 'fs'; - -<% } -%> - -<% if (generateApp) { -%> -import { i18n } from '@kbn/i18n'; -<% } -%> - -<% if (generateApi) { -%> -import exampleRoute from './server/routes/example'; - -<% } -%> -export default function (kibana) { - return new kibana.Plugin({ - require: ['elasticsearch'], - name: '<%= snakeCase(name) %>', - uiExports: { - <%_ if (generateApp) { -%> - app: { - title: '<%= startCase(name) %>', - description: '<%= description %>', - main: 'plugins/<%= snakeCase(name) %>/app', - }, - <%_ } -%> - <%_ if (generateHack) { -%> - hacks: [ - 'plugins/<%= snakeCase(name) %>/hack' - ], - <%_ } -%> - <%_ if (generateScss) { -%> - styleSheetPaths: [resolve(__dirname, 'public/app.scss'), resolve(__dirname, 'public/app.css')].find(p => existsSync(p)), - <%_ } -%> - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - <%_ if (generateApi || generateApp) { -%> - - // eslint-disable-next-line no-unused-vars - init(server, options) { - <%_ if (generateApp) { -%> - const xpackMainPlugin = server.plugins.xpack_main; - if (xpackMainPlugin) { - const featureId = '<%= snakeCase(name) %>'; - - xpackMainPlugin.registerFeature({ - id: featureId, - name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', { - defaultMessage: '<%= name %>', - }), - navLinkId: featureId, - icon: 'questionInCircle', - app: [featureId, 'kibana'], - catalogue: [], - privileges: { - all: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - read: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - }, - }); - } - <%_ } -%> - - <%_ if (generateApi) { -%> - // Add server routes and initialize the plugin here - exampleRoute(server); - <%_ } -%> - } - <%_ } -%> - }); -} diff --git a/packages/kbn-plugin-generator/sao_template/template/kibana.json b/packages/kbn-plugin-generator/sao_template/template/kibana.json new file mode 100644 index 0000000000000..f8bb07040abeb --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "<%= camelCase(name) %>", + "version": "<%= kbnVersion %>", + "server": <%= hasServer %>, + "ui": <%= hasUi %>, + "requiredPlugins": ["navigation"], + "optionalPlugins": [] +} diff --git a/packages/kbn-plugin-generator/sao_template/template/package_template.json b/packages/kbn-plugin-generator/sao_template/template/package_template.json deleted file mode 100644 index 4b6629fa90268..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/package_template.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "<%= snakeCase(name) %>", - "version": "0.0.0", - "description": "<%= description %>", - "main": "index.js", - "kibana": { - "version": "<%= (kbnVersion === 'master') ? 'kibana' : kbnVersion %>", - "templateVersion": "<%= templateVersion %>" - }, - "scripts": { - "preinstall": "node ../../preinstall_check", - "kbn": "node ../../scripts/kbn", - "es": "node ../../scripts/es", - "lint": "eslint .", - "start": "plugin-helpers start", - "test:server": "plugin-helpers test:server", - "test:browser": "plugin-helpers test:browser", - "build": "plugin-helpers build" - }, - <%_ if (generateTranslations) { _%> - "dependencies": { - "@kbn/i18n": "link:../../packages/kbn-i18n" - }, - <%_ } _%> - "devDependencies": { - "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", - "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana", - "@kbn/expect": "link:../../packages/kbn-expect", - "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jest": "^22.4.1", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.12.4" - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js deleted file mode 100755 index 9320bd7b028a8..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.js b/packages/kbn-plugin-generator/sao_template/template/public/app.js deleted file mode 100755 index 37a7c37e916a0..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/app.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; -import { render, unmountComponentAtNode } from 'react-dom'; -<%_ if (generateTranslations) { _%> -import { I18nProvider } from '@kbn/i18n/react'; -<%_ } _%> - -import { Main } from './components/main'; - -const app = uiModules.get('apps/<%= camelCase(name) %>'); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element, $http) { - const domNode = $element[0]; - - // render react to DOM - <%_ if (generateTranslations) { _%> - render( - -
- , - domNode - ); - <%_ } else { _%> - render(
, domNode); - <%_ } _%> - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('<%= camelCase(name) %>', RootController); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/application.tsx b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx new file mode 100644 index 0000000000000..8106a18a784e7 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '<%= relRoot %>/src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { <%= upperCamelCaseName %>App } from './components/app'; + + +export const renderApp = ( + { notifications, http }: CoreStart, + { navigation }: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters + ) => { + ReactDOM.render( + <<%= upperCamelCaseName %>App + basename={appBasePath} + notifications={notifications} + http={http} + navigation={navigation} + />, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); + }; + \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx new file mode 100644 index 0000000000000..7b259a9c5b99d --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx @@ -0,0 +1,129 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { + EuiButton, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { CoreStart } from '<%= relRoot %>/../src/core/public'; +import { NavigationPublicPluginStart } from '<%= relRoot %>/../src/plugins/navigation/public'; + +import { PLUGIN_ID, PLUGIN_NAME } from '../../common'; + +interface <%= upperCamelCaseName %>AppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; +} + +export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCaseName %>AppDeps) => { + // Use React hooks to manage state. + const [timestamp, setTimestamp] = useState(); + + const onClickHandler = () => { +<%_ if (generateApi) { -%> + // Use the core http service to make a response to the server API. + http.get('/api/<%= snakeCase(name) %>/example').then(res => { + setTimestamp(res.time); + // Use the core notifications service to display a success message. + notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', { + defaultMessage: 'Data updated', + })); + }); +<%_ } else { -%> + setTimestamp(new Date().toISOString()); + notifications.toasts.addSuccess(PLUGIN_NAME); +<%_ } -%> + }; + + // Render the application DOM. + // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ +

+
+
+ + +

+ +

+ +

+ +

+ + + +
+
+
+
+
+ +
+
+ ); +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js deleted file mode 100644 index 68710baa1bee8..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Main } from './main'; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js deleted file mode 100644 index 59fd667c709aa..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { - EuiPage, - EuiPageHeader, - EuiTitle, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentBody, - EuiText -} from '@elastic/eui'; -<%_ if (generateTranslations) { _%> -import { FormattedMessage } from '@kbn/i18n/react'; -<%_ } _%> - -export class Main extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { - /* - FOR EXAMPLE PURPOSES ONLY. There are much better ways to - manage state and update your UI than this. - */ - const { httpClient } = this.props; - httpClient.get('../api/<%= name %>/example').then((resp) => { - this.setState({ time: resp.data.time }); - }); - } - render() { - const { title } = this.props; - return ( - - - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - {title} Hello World! - <%_ } _%> -

-
-
- - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - Congratulations - <%_ } _%> -

-
-
- - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - You have successfully created your first Kibana Plugin! - <%_ } _%> -

-

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - The server time (via API call) is {this.state.time || 'NO API CALL YET'} - <%_ } _%> -

-
-
-
-
-
- ); - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/hack.js b/packages/kbn-plugin-generator/sao_template/template/public/hack.js deleted file mode 100755 index 775526c8e44a3..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/hack.js +++ /dev/null @@ -1,7 +0,0 @@ -import $ from 'jquery'; - -$(document.body).on('keypress', function (event) { - if (event.which === 58) { - alert('boo!'); - } -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.scss b/packages/kbn-plugin-generator/sao_template/template/public/index.scss similarity index 100% rename from packages/kbn-plugin-generator/sao_template/template/public/app.scss rename to packages/kbn-plugin-generator/sao_template/template/public/index.scss diff --git a/packages/kbn-plugin-generator/sao_template/template/public/index.ts b/packages/kbn-plugin-generator/sao_template/template/public/index.ts new file mode 100644 index 0000000000000..2999dc7264ddb --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/index.ts @@ -0,0 +1,16 @@ +<%_ if (hasScss) { -%> +import './index.scss'; +<%_ } -%> + +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new <%= upperCamelCaseName %>Plugin(); +} +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; + diff --git a/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts new file mode 100644 index 0000000000000..76f7f1a6f9908 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts @@ -0,0 +1,42 @@ +import { i18n } from '@kbn/i18n'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= relRoot %>/src/core/public'; +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart, AppPluginStartDependencies } from './types'; +import { PLUGIN_NAME } from '../common'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + + public setup(core: CoreSetup): <%= upperCamelCaseName %>PluginSetup { + // Register an application into the side navigation menu + core.application.register({ + id: '<%= camelCase(name) %>', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Render the application + return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); + }, + }); + + // Return methods that should be available to other plugins + return { + getGreeting() { + return i18n.translate('<%= camelCase(name) %>.greetingText', { + defaultMessage: 'Hello from {name}!', + values: { + name: PLUGIN_NAME, + }, + }); + }, + }; + } + + public start(core: CoreStart): <%= upperCamelCaseName %>PluginStart { + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/types.ts b/packages/kbn-plugin-generator/sao_template/template/public/types.ts new file mode 100644 index 0000000000000..2ebb0c0d1257f --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/types.ts @@ -0,0 +1,11 @@ +import { NavigationPublicPluginStart } from '<%= relRoot %>/src/plugins/navigation/public'; + +export interface <%= upperCamelCaseName %>PluginSetup { + getGreeting: () => string; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js deleted file mode 100755 index 9320bd7b028a8..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/server/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/index.ts new file mode 100644 index 0000000000000..816b8faec2a45 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/index.ts @@ -0,0 +1,15 @@ +import { PluginInitializerContext } from '<%= relRoot %>/src/core/server'; +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + + export function plugin(initializerContext: PluginInitializerContext) { + return new <%= upperCamelCaseName %>Plugin(initializerContext); +} + +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts new file mode 100644 index 0000000000000..d6a343209e39e --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts @@ -0,0 +1,30 @@ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '<%= relRoot %>/src/core/server'; + +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('<%= name %>: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('<%= name %>: Started'); + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js b/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js deleted file mode 100755 index 5a612645f48fc..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function (server) { - - server.route({ - path: '/api/<%= name %>/example', - method: 'GET', - handler() { - return { time: (new Date()).toISOString() }; - } - }); - -} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts new file mode 100644 index 0000000000000..d8bb00f0dea6c --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '<%= relRoot %>/../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/<%= snakeCase(name) %>/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/types.ts b/packages/kbn-plugin-generator/sao_template/template/server/types.ts new file mode 100644 index 0000000000000..adbc5e93f03c5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} diff --git a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json b/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json deleted file mode 100644 index 3447511c6739a..0000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "formats": { - "number": { - "currency": { - "style": "currency" - }, - "percent": { - "style": "percent" - } - }, - "date": { - "short": { - "month": "numeric", - "day": "numeric", - "year": "2-digit" - }, - "medium": { - "month": "short", - "day": "numeric", - "year": "numeric" - }, - "long": { - "month": "long", - "day": "numeric", - "year": "numeric" - }, - "full": { - "weekday": "long", - "month": "long", - "day": "numeric", - "year": "numeric" - } - }, - "time": { - "short": { - "hour": "numeric", - "minute": "numeric" - }, - "medium": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric" - }, - "long": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - }, - "full": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - } - }, - "relative": { - "years": { - "units": "year" - }, - "months": { - "units": "month" - }, - "days": { - "units": "day" - }, - "hours": { - "units": "hour" - }, - "minutes": { - "units": "minute" - }, - "seconds": { - "units": "second" - } - } - }, - "messages": { - "<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。", - "<%= camelCase(name) %>.congratulationsTitle": "恭喜!", - "<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!", - "<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}" - } -} diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json new file mode 100644 index 0000000000000..fe0f7112f1fa9 --- /dev/null +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["**/*", "index.js.d.ts"], + "exclude": ["sao_template/template/*"] +} From 4aa727560a72b3f284910dd34e78d5b9dbc81ebc Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 3 Feb 2020 19:00:40 +0100 Subject: [PATCH 17/17] fix timespan referencing to same values (#56601) (#56612) --- .../server/lib/adapters/monitor_states/search/query_context.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index 961cc94dcea19..a51931ba11630 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; @@ -97,7 +98,7 @@ export class QueryContext { // behavior. const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; - const tsStart = tsEnd.subtract(5, 'minutes'); + const tsStart = moment(tsEnd).subtract(5, 'minutes'); return { range: {