From 6398a9911df0d45ba72dafbb2d9c8398916ddba6 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 3 Feb 2020 15:55:50 -0500 Subject: [PATCH 01/22] [Monitoring] Migrate license expiration alert to Kibana alerting (#54306) * License expiration * Flip off * Only require alerting and actions if enabled * Support date formating and timezones in the alert UI messages, support ccs better * Fix status tests * Fix up front end tests * Fix linting, and switch this back * Add this back in so legacy alerts continue to work * Fix type issues * Handle CCS better * Code cleanup * Fix type issues * Flip this off, and fix test * Moved the email address config to advanced settings, but need help with test failures and typescript * Fix issue with task manager * Deprecate email_address * Use any until we can figure out this TS issue * Fix type issue * More tests * Fix mocha tests * Use mock instead of any * I'm not sure why these changed... * Provide timezone in moment usage in tests for consistency * Fix type issue * Change how we get dateFormat and timezone * Change where we calculate the dates to show in the alerts UI * Show deprecation warning based on the feature toggle * Ensure we are using UTC * PR feedback * Only add this if the feature flag is enabled * Fix tests * Ensure we only attempt to look this up if the feature flag is enabled Co-authored-by: Elastic Machine --- .../common/{constants.js => constants.ts} | 42 ++ .../legacy/plugins/monitoring/deprecations.js | 20 +- x-pack/legacy/plugins/monitoring/index.js | 12 +- .../alerts/__snapshots__/status.test.tsx.snap | 70 +++ .../__snapshots__/configuration.test.tsx.snap | 120 +++++ .../__snapshots__/step1.test.tsx.snap | 297 ++++++++++++ .../__snapshots__/step2.test.tsx.snap | 49 ++ .../__snapshots__/step3.test.tsx.snap | 95 ++++ .../configuration/configuration.test.tsx | 147 ++++++ .../alerts/configuration/configuration.tsx | 193 ++++++++ .../components/alerts/configuration/index.ts | 7 + .../alerts/configuration/step1.test.tsx | 338 +++++++++++++ .../components/alerts/configuration/step1.tsx | 334 +++++++++++++ .../alerts/configuration/step2.test.tsx | 51 ++ .../components/alerts/configuration/step2.tsx | 38 ++ .../alerts/configuration/step3.test.tsx | 48 ++ .../components/alerts/configuration/step3.tsx | 47 ++ .../components/alerts/manage_email_action.tsx | 301 ++++++++++++ .../public/components/alerts/status.test.tsx | 81 ++++ .../public/components/alerts/status.tsx | 203 ++++++++ .../cluster/overview/alerts_panel.js | 62 ++- .../components/cluster/overview/index.js | 14 +- .../plugins/monitoring/public/jest.helpers.ts | 36 ++ ...rror_handler.js => ajax_error_handler.tsx} | 7 +- .../monitoring/public/lib/form_validation.ts | 48 ++ .../monitoring/public/lib/setup_mode.test.js | 8 +- .../lib/{setup_mode.js => setup_mode.tsx} | 62 ++- .../monitoring/public/views/alerts/index.js | 2 +- .../public/views/cluster/overview/index.js | 21 +- .../server/alerts/license_expiration.test.ts | 453 ++++++++++++++++++ .../server/alerts/license_expiration.ts | 162 +++++++ .../monitoring/server/alerts/types.d.ts | 45 ++ .../lib/alerts/fetch_available_ccs.test.ts | 36 ++ .../server/lib/alerts/fetch_available_ccs.ts | 19 + .../server/lib/alerts/fetch_clusters.test.ts | 33 ++ .../server/lib/alerts/fetch_clusters.ts | 52 ++ .../fetch_default_email_address.test.ts | 17 + .../lib/alerts/fetch_default_email_address.ts | 13 + .../server/lib/alerts/fetch_licenses.test.ts | 105 ++++ .../server/lib/alerts/fetch_licenses.ts | 67 +++ .../server/lib/alerts/fetch_status.ts | 87 ++++ .../lib/alerts/get_ccs_index_pattern.test.ts | 24 + .../lib/alerts/get_ccs_index_pattern.ts | 13 + .../lib/alerts/license_expiration.lib.test.ts | 55 +++ .../lib/alerts/license_expiration.lib.ts | 58 +++ .../lib/cluster/get_clusters_from_request.js | 32 +- .../monitoring/server/lib/get_date_format.js | 9 + .../setup/collection/get_collection_status.js | 1 - .../plugins/monitoring/server/plugin.js | 39 +- .../server/routes/api/v1/alerts/alerts.js | 89 ++++ .../server/routes/api/v1/alerts/index.js | 53 +- .../routes/api/v1/alerts/legacy_alerts.js | 57 +++ .../monitoring/server/routes/api/v1/ui.js | 2 +- .../legacy/plugins/monitoring/ui_exports.js | 72 ++- x-pack/plugins/actions/common/types.ts | 7 + 55 files changed, 4223 insertions(+), 130 deletions(-) rename x-pack/legacy/plugins/monitoring/common/{constants.js => constants.ts} (85%) create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/jest.helpers.ts rename x-pack/legacy/plugins/monitoring/public/lib/{ajax_error_handler.js => ajax_error_handler.tsx} (94%) create mode 100644 x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts rename x-pack/legacy/plugins/monitoring/public/lib/{setup_mode.js => setup_mode.tsx} (76%) create mode 100644 x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js create mode 100644 x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js create mode 100644 x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js diff --git a/x-pack/legacy/plugins/monitoring/common/constants.js b/x-pack/legacy/plugins/monitoring/common/constants.ts similarity index 85% rename from x-pack/legacy/plugins/monitoring/common/constants.js rename to x-pack/legacy/plugins/monitoring/common/constants.ts index ff16b0e9c5167f9..53764f592dc1516 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.js +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -233,3 +233,45 @@ export const REPORTING_SYSTEM_ID = 'reporting'; * @type {Number} */ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; + +/** + * We want to slowly rollout the migration from watcher-based cluster alerts to + * kibana alerts and we only want to enable the kibana alerts once all + * watcher-based cluster alerts have been migrated so this flag will serve + * as the only way to see the new UI and actually run Kibana alerts. It will + * be false until all alerts have been migrated, then it will be removed + */ +export const KIBANA_ALERTING_ENABLED = false; + +/** + * The prefix for all alert types used by monitoring + */ +export const ALERT_TYPE_PREFIX = 'monitoring_'; + +/** + * This is the alert type id for the license expiration alert + */ +export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; + +/** + * A listing of all alert types + */ +export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION]; + +/** + * Matches the id for the built-in in email action type + * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + */ +export const ALERT_ACTION_TYPE_EMAIL = '.email'; + +/** + * The number of alerts that have been migrated + */ +export const NUMBER_OF_MIGRATED_ALERTS = 1; + +/** + * The advanced settings config name for the email address + */ +export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; + +export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/legacy/plugins/monitoring/deprecations.js b/x-pack/legacy/plugins/monitoring/deprecations.js index 6e35e86dd9d7184..ae8650fd3b26a64 100644 --- a/x-pack/legacy/plugins/monitoring/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/deprecations.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from './common/constants'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY, KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Re-writes deprecated user-defined config settings and logs warnings as a @@ -21,10 +21,20 @@ export const deprecations = () => { const clusterAlertsEnabled = get(settings, 'cluster_alerts.enabled'); const emailNotificationsEnabled = clusterAlertsEnabled && get(settings, 'cluster_alerts.email_notifications.enabled'); - if (emailNotificationsEnabled && !get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { - log( - `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` - ); + if (emailNotificationsEnabled) { + if (KIBANA_ALERTING_ENABLED) { + if (get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" is deprecated. Please configure the email adddress through the Stack Monitoring UI instead."` + ); + } + } else { + if (!get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` + ); + } + } } }, (settings, log) => { diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index ca595836133c2c0..ade172f527dabde 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -10,15 +10,20 @@ import { deprecations } from './deprecations'; import { getUiExports } from './ui_exports'; import { Plugin } from './server/plugin'; import { initInfraSource } from './server/lib/logs/init_infra_source'; +import { KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Invokes plugin modules to instantiate the Monitoring plugin for Kibana * @param kibana {Object} Kibana plugin instance * @return {Object} Monitoring UI Kibana plugin object */ +const deps = ['kibana', 'elasticsearch', 'xpack_main']; +if (KIBANA_ALERTING_ENABLED) { + deps.push(...['alerting', 'actions']); +} export const monitoring = kibana => new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], + require: deps, id: 'monitoring', configPrefix: 'monitoring', publicDir: resolve(__dirname, 'public'), @@ -59,6 +64,7 @@ export const monitoring = kibana => }), injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), + logger: server.newPlatform.coreContext.logger, getOSInfo: server.getOSInfo, events: { on: (...args) => server.events.on(...args), @@ -73,11 +79,13 @@ export const monitoring = kibana => xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + alerting: server.plugins.alerting, usageCollection, licensing, }; - new Plugin().setup(serverFacade, plugins); + const plugin = new Plugin(); + plugin.setup(serverFacade, plugins); }, config, deprecations, diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap new file mode 100644 index 000000000000000..4cf1f4df2eb2e60 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Status should render a flyout when clicking the link 1`] = ` + + + +

+ Monitoring alerts +

+
+ +

+ Configure an email server and email address to receive alerts. +

+
+
+ + + +
+`; + +exports[`Status should render a success message if all alerts have been migrated and in setup mode 1`] = ` + +

+ + Want to make changes? Click here. + +

+
+`; + +exports[`Status should render without setup mode 1`] = ` + + +

+ + Migrate cluster alerts to our new alerting platform. + +

+
+ +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap new file mode 100644 index 000000000000000..f044e001700c50c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Configuration shallow view should render step 1 1`] = ` + + + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="" + /> + +`; + +exports[`Configuration shallow view should render step 2 1`] = ` + + + + + +`; + +exports[`Configuration shallow view should render step 3 1`] = ` + + + Save + + +`; + +exports[`Configuration should render high level steps 1`] = ` +
+ + + + + + + + + +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap new file mode 100644 index 000000000000000..fa03769ea3d0902 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -0,0 +1,297 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step1 creating should render a create form 1`] = ` + + + + + +`; + +exports[`Step1 editing should allow for editing 1`] = ` + + +

+ Edit the action below. +

+
+ + +
+`; + +exports[`Step1 should render normally 1`] = ` + + + From: , Service: + , + "inputDisplay": + From: , Service: + , + "value": "1", + }, + Object { + "dropdownDisplay": + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + + + + + Edit + + + + + Test + + + + + Delete + + + + +`; + +exports[`Step1 testing should should a tooltip if there is no email address 1`] = ` + + + Test + + +`; + +exports[`Step1 testing should show a failed test error 1`] = ` + + + From: , Service: + , + "inputDisplay": + From: , Service: + , + "value": "1", + }, + Object { + "dropdownDisplay": + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + + + + + Edit + + + + + Test + + + + + Delete + + + + + +

+ Very detailed error message +

+
+
+`; + +exports[`Step1 testing should show a successful test 1`] = ` + + + From: , Service: + , + "inputDisplay": + From: , Service: + , + "value": "1", + }, + Object { + "dropdownDisplay": + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + + + + + Edit + + + + + Test + + + + + Delete + + + + + +

+ Looks good on our end! +

+
+
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap new file mode 100644 index 000000000000000..bac183618b49128 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step2 should render normally 1`] = ` + + + + + +`; + +exports[`Step2 should show form errors 1`] = ` + + + + + +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap new file mode 100644 index 000000000000000..ed15ae9a9cff7f6 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step3 should render normally 1`] = ` + + + Save + + +`; + +exports[`Step3 should show a disabled state 1`] = ` + + + Save + + +`; + +exports[`Step3 should show a saving state 1`] = ` + + + Save + + +`; + +exports[`Step3 should show an error 1`] = ` + + +

+ Test error +

+
+ + + Save + +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx new file mode 100644 index 000000000000000..6b7e2391e0301c6 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx @@ -0,0 +1,147 @@ +/* + * 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 { mockUseEffects } from '../../../jest.helpers'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsConfigurationProps = { + emailAddress: 'test@elastic.co', + onDone: jest.fn(), +}; + +describe('Configuration', () => { + it('should render high level steps', () => { + const component = shallow(); + expect(component.find('EuiSteps').shallow()).toMatchSnapshot(); + }); + + function getStep(component: ShallowWrapper, index: number) { + return component + .find('EuiSteps') + .shallow() + .find('EuiStep') + .at(index) + .children() + .shallow(); + } + + describe('shallow view', () => { + it('should render step 1', () => { + const component = shallow(); + const stepOne = getStep(component, 0); + expect(stepOne).toMatchSnapshot(); + }); + + it('should render step 2', () => { + const component = shallow(); + const stepTwo = getStep(component, 1); + expect(stepTwo).toMatchSnapshot(); + }); + + it('should render step 3', () => { + const component = shallow(); + const stepThree = getStep(component, 2); + expect(stepThree).toMatchSnapshot(); + }); + }); + + describe('selected action', () => { + const actionId = 'a123b'; + let component: ShallowWrapper; + beforeEach(async () => { + mockUseEffects(2); + + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: actionId, + config: {}, + }, + ], + }; + }); + + component = shallow(); + }); + + it('reflect in Step1', async () => { + const steps = component.find('EuiSteps').dive(); + expect( + steps + .find('EuiStep') + .at(0) + .prop('title') + ).toBe('Select email action'); + expect(steps.find('Step1').prop('selectedEmailActionId')).toBe(actionId); + }); + + it('should enable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(false); + }); + + it('should enable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(false); + }); + }); + + describe('edit action', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [], + }; + }); + + component = shallow(); + }); + + it('disable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(true); + }); + + it('disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); + + describe('no email address', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: 'actionId', + config: {}, + }, + ], + }; + }); + + component = shallow(); + }); + + it('should disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx new file mode 100644 index 000000000000000..0933cd22db7c9fd --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -0,0 +1,193 @@ +/* + * 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, { ReactNode } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { EuiSteps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { getMissingFieldErrors } from '../../../lib/form_validation'; +import { Step1 } from './step1'; +import { Step2 } from './step2'; +import { Step3 } from './step3'; + +export interface AlertsConfigurationProps { + emailAddress: string; + onDone: Function; +} + +export interface StepResult { + title: string; + children: ReactNode; + status: any; +} + +export interface AlertsConfigurationForm { + email: string | null; +} + +export const NEW_ACTION_ID = '__new__'; + +export const AlertsConfiguration: React.FC = ( + props: AlertsConfigurationProps +) => { + const { onDone } = props; + + const [emailActions, setEmailActions] = React.useState([]); + const [selectedEmailActionId, setSelectedEmailActionId] = React.useState(''); + const [editAction, setEditAction] = React.useState(null); + const [emailAddress, setEmailAddress] = React.useState(props.emailAddress); + const [formErrors, setFormErrors] = React.useState({ email: null }); + const [showFormErrors, setShowFormErrors] = React.useState(false); + const [isSaving, setIsSaving] = React.useState(false); + const [saveError, setSaveError] = React.useState(''); + + React.useEffect(() => { + async function fetchData() { + await fetchEmailActions(); + } + + fetchData(); + }, []); + + React.useEffect(() => { + setFormErrors(getMissingFieldErrors({ email: emailAddress }, { email: '' })); + }, [emailAddress]); + + async function fetchEmailActions() { + const kibanaActions = await kfetch({ + method: 'GET', + pathname: `/api/action/_find`, + }); + + const actions = kibanaActions.data.filter( + (action: ActionResult) => action.actionTypeId === ALERT_ACTION_TYPE_EMAIL + ); + if (actions.length > 0) { + setSelectedEmailActionId(actions[0].id); + } else { + setSelectedEmailActionId(NEW_ACTION_ID); + } + setEmailActions(actions); + } + + async function save() { + if (emailAddress.length === 0) { + setShowFormErrors(true); + return; + } + setIsSaving(true); + setShowFormErrors(false); + + try { + await kfetch({ + method: 'POST', + pathname: `/api/monitoring/v1/alerts`, + body: JSON.stringify({ selectedEmailActionId, emailAddress }), + }); + } catch (err) { + setIsSaving(false); + setSaveError( + err?.body?.message || + i18n.translate('xpack.monitoring.alerts.configuration.unknownError', { + defaultMessage: 'Something went wrong. Please consult the server logs.', + }) + ); + return; + } + + onDone(); + } + + function isStep2Disabled() { + return isStep2AndStep3Disabled(); + } + + function isStep3Disabled() { + return isStep2AndStep3Disabled() || !emailAddress || emailAddress.length === 0; + } + + function isStep2AndStep3Disabled() { + return !!editAction || !selectedEmailActionId || selectedEmailActionId === NEW_ACTION_ID; + } + + function getStep2Status() { + const isDisabled = isStep2AndStep3Disabled(); + + if (isDisabled) { + return 'disabled' as const; + } + + if (emailAddress && emailAddress.length) { + return 'complete' as const; + } + + return 'incomplete' as const; + } + + function getStep1Status() { + if (editAction) { + return 'incomplete' as const; + } + + return selectedEmailActionId ? ('complete' as const) : ('incomplete' as const); + } + + const steps = [ + { + title: emailActions.length + ? i18n.translate('xpack.monitoring.alerts.configuration.selectEmailAction', { + defaultMessage: 'Select email action', + }) + : i18n.translate('xpack.monitoring.alerts.configuration.createEmailAction', { + defaultMessage: 'Create email action', + }), + children: ( + await fetchEmailActions()} + emailActions={emailActions} + selectedEmailActionId={selectedEmailActionId} + setSelectedEmailActionId={setSelectedEmailActionId} + emailAddress={emailAddress} + editAction={editAction} + setEditAction={setEditAction} + /> + ), + status: getStep1Status(), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.setEmailAddress', { + defaultMessage: 'Set the email to receive alerts', + }), + status: getStep2Status(), + children: ( + + ), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.confirm', { + defaultMessage: 'Confirm and save', + }), + status: getStep2Status(), + children: ( + + ), + }, + ]; + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts new file mode 100644 index 000000000000000..7a96c6e324ab363 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { AlertsConfiguration } from './configuration'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx new file mode 100644 index 000000000000000..650294c29e9a50f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -0,0 +1,338 @@ +/* + * 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 { omit, pick } from 'lodash'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { GetStep1Props } from './step1'; +import { EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; + +let Step1: React.FC; +let NEW_ACTION_ID: string; + +function setModules() { + Step1 = require('./step1').Step1; + NEW_ACTION_ID = require('./configuration').NEW_ACTION_ID; +} + +describe('Step1', () => { + const emailActions = [ + { + id: '1', + actionTypeId: '1abc', + name: 'Testing', + config: {}, + }, + ]; + const selectedEmailActionId = emailActions[0].id; + const setSelectedEmailActionId = jest.fn(); + const emailAddress = 'test@test.com'; + const editAction = null; + const setEditAction = jest.fn(); + const onActionDone = jest.fn(); + + const defaultProps: GetStep1Props = { + onActionDone, + emailActions, + selectedEmailActionId, + setSelectedEmailActionId, + emailAddress, + editAction, + setEditAction, + }; + + beforeEach(() => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: () => { + return {}; + }, + })); + setModules(); + }); + }); + + it('should render normally', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + describe('creating', () => { + it('should render a create form', () => { + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + it('should render the select box if at least one action exists', () => { + const customProps = { + emailActions: [ + { + id: 'foo', + actionTypeId: '.email', + name: '', + config: {}, + }, + ], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(); + expect(component.find('EuiSuperSelect').exists()).toBe(true); + }); + + it('should send up the create to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'POST', + pathname: `/api/action`, + body: JSON.stringify({ + name: 'Email action for Stack Monitoring alerts', + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('editing', () => { + it('should allow for editing', () => { + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + it('should send up the edit to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'PUT', + pathname: `/api/action/${emailActions[0].id}`, + body: JSON.stringify({ + name: emailActions[0].name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('testing', () => { + it('should allow for testing', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: jest.fn().mockImplementation(arg => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }), + })); + setModules(); + }); + + const component = shallow(); + + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + component + .find('EuiButton') + .at(1) + .simulate('click'); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(true); + await component.update(); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + }); + + it('should show a successful test', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show a failed test error', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { message: 'Very detailed error message' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should not allow testing if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(); + expect( + component + .find('EuiButton') + .at(1) + .prop('isDisabled') + ).toBe(true); + }); + + it('should should a tooltip if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(); + expect(component.find('EuiToolTip')).toMatchSnapshot(); + }); + }); + + describe('deleting', () => { + it('should send up the delete to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + setSelectedEmailActionId: jest.fn(), + onActionDone: jest.fn(), + }; + const component = shallow(); + + await component + .find('EuiButton') + .at(2) + .simulate('click'); + await component.update(); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'DELETE', + pathname: `/api/action/${emailActions[0].id}`, + }); + + expect(customProps.setSelectedEmailActionId).toHaveBeenCalledWith(''); + expect(customProps.onActionDone).toHaveBeenCalled(); + expect( + component + .find('EuiButton') + .at(2) + .prop('isLoading') + ).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx new file mode 100644 index 000000000000000..fc051a68e29f35c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx @@ -0,0 +1,334 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { + EuiText, + EuiSpacer, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSuperSelect, + EuiToolTip, + EuiCallOut, +} from '@elastic/eui'; +import { kfetch } from 'ui/kfetch'; +import { omit, pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ManageEmailAction, EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { NEW_ACTION_ID } from './configuration'; + +export interface GetStep1Props { + onActionDone: () => Promise; + emailActions: ActionResult[]; + selectedEmailActionId: string; + setSelectedEmailActionId: (id: string) => void; + emailAddress: string; + editAction: ActionResult | null; + setEditAction: (action: ActionResult | null) => void; +} + +export const Step1: React.FC = (props: GetStep1Props) => { + const [isTesting, setIsTesting] = React.useState(false); + const [isDeleting, setIsDeleting] = React.useState(false); + const [testingStatus, setTestingStatus] = React.useState(null); + const [fullTestingError, setFullTestingError] = React.useState(''); + + async function createEmailAction(data: EmailActionData) { + if (props.editAction) { + await kfetch({ + method: 'PUT', + pathname: `/api/action/${props.editAction.id}`, + body: JSON.stringify({ + name: props.editAction.name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + props.setEditAction(null); + } else { + await kfetch({ + method: 'POST', + pathname: '/api/action', + body: JSON.stringify({ + name: i18n.translate('xpack.monitoring.alerts.configuration.emailAction.name', { + defaultMessage: 'Email action for Stack Monitoring alerts', + }), + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + } + + await props.onActionDone(); + } + + async function deleteEmailAction(id: string) { + setIsDeleting(true); + + await kfetch({ + method: 'DELETE', + pathname: `/api/action/${id}`, + }); + + if (props.editAction && props.editAction.id === id) { + props.setEditAction(null); + } + if (props.selectedEmailActionId === id) { + props.setSelectedEmailActionId(''); + } + await props.onActionDone(); + setIsDeleting(false); + setTestingStatus(null); + } + + async function testEmailAction() { + setIsTesting(true); + setTestingStatus(null); + + const params = { + subject: 'Kibana alerting test configuration', + message: `This is a test for the configured email action for Kibana alerting.`, + to: [props.emailAddress], + }; + + const result = await kfetch({ + method: 'POST', + pathname: `/api/action/${props.selectedEmailActionId}/_execute`, + body: JSON.stringify({ params }), + }); + if (result.status === 'ok') { + setTestingStatus(true); + } else { + setTestingStatus(false); + setFullTestingError(result.message); + } + setIsTesting(false); + } + + function getTestButton() { + const isTestingDisabled = !props.emailAddress || props.emailAddress.length === 0; + const testBtn = ( + + {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.buttonText', { + defaultMessage: 'Test', + })} + + ); + + if (isTestingDisabled) { + return ( + + {testBtn} + + ); + } + + return testBtn; + } + + if (props.editAction) { + return ( + + +

+ {i18n.translate('xpack.monitoring.alerts.configuration.step1.editAction', { + defaultMessage: 'Edit the action below.', + })} +

+
+ + await createEmailAction(data)} + cancel={() => props.setEditAction(null)} + isNew={false} + action={props.editAction} + /> +
+ ); + } + + const newAction = ( + + {i18n.translate('xpack.monitoring.alerts.configuration.newActionDropdownDisplay', { + defaultMessage: 'Create new email action...', + })} + + ); + + const options = [ + ...props.emailActions.map(action => { + const actionLabel = i18n.translate( + 'xpack.monitoring.alerts.configuration.selectAction.inputDisplay', + { + defaultMessage: 'From: {from}, Service: {service}', + values: { + service: action.config.service, + from: action.config.from, + }, + } + ); + + return { + value: action.id, + inputDisplay: {actionLabel}, + dropdownDisplay: {actionLabel}, + }; + }), + { + value: NEW_ACTION_ID, + inputDisplay: newAction, + dropdownDisplay: newAction, + }, + ]; + + let selectBox: React.ReactNode | null = ( + props.setSelectedEmailActionId(id)} + hasDividers + /> + ); + let createNew = null; + if (props.selectedEmailActionId === NEW_ACTION_ID) { + createNew = ( + + await createEmailAction(data)} + isNew={true} + /> + + ); + + // If there are no actions, do not show the select box as there are no choices + if (props.emailActions.length === 0) { + selectBox = null; + } else { + // Otherwise, add a spacer + selectBox = ( + + {selectBox} + + + ); + } + } + + let manageConfiguration = null; + const selectedEmailAction = props.emailActions.find( + action => action.id === props.selectedEmailActionId + ); + + if ( + props.selectedEmailActionId !== NEW_ACTION_ID && + props.selectedEmailActionId && + selectedEmailAction + ) { + let testingStatusUi = null; + if (testingStatus === true) { + testingStatusUi = ( + + + +

+ {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.success', { + defaultMessage: 'Looks good on our end!', + })} +

+
+
+ ); + } else if (testingStatus === false) { + testingStatusUi = ( + + + +

{fullTestingError}

+
+
+ ); + } + + manageConfiguration = ( + + + + + { + const editAction = + props.emailActions.find(action => action.id === props.selectedEmailActionId) || + null; + props.setEditAction(editAction); + }} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.editConfiguration.buttonText', + { + defaultMessage: 'Edit', + } + )} + + + {getTestButton()} + + deleteEmailAction(props.selectedEmailActionId)} + isLoading={isDeleting} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText', + { + defaultMessage: 'Delete', + } + )} + + + + {testingStatusUi} + + ); + } + + return ( + + {selectBox} + {manageConfiguration} + {createNew} + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx new file mode 100644 index 000000000000000..14e3cb078f9ccad --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.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 React from 'react'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step2, GetStep2Props } from './step2'; + +describe('Step2', () => { + const defaultProps: GetStep2Props = { + emailAddress: 'test@test.com', + setEmailAddress: jest.fn(), + showFormErrors: false, + formErrors: { email: null }, + isDisabled: false, + }; + + it('should render normally', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should set the email address properly', () => { + const newEmail = 'email@email.com'; + const component = shallow(); + component.find('EuiFieldText').simulate('change', { target: { value: newEmail } }); + expect(defaultProps.setEmailAddress).toHaveBeenCalledWith(newEmail); + }); + + it('should show form errors', () => { + const customProps = { + showFormErrors: true, + formErrors: { + email: 'This is required', + }, + }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should disable properly', () => { + const customProps = { + isDisabled: true, + }; + const component = shallow(); + expect(component.find('EuiFieldText').prop('disabled')).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx new file mode 100644 index 000000000000000..974dd8513d231d7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AlertsConfigurationForm } from './configuration'; + +export interface GetStep2Props { + emailAddress: string; + setEmailAddress: (email: string) => void; + showFormErrors: boolean; + formErrors: AlertsConfigurationForm; + isDisabled: boolean; +} + +export const Step2: React.FC = (props: GetStep2Props) => { + return ( + + + props.setEmailAddress(e.target.value)} + /> + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx new file mode 100644 index 000000000000000..9b1304c42a507d6 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx @@ -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 React from 'react'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step3 } from './step3'; + +describe('Step3', () => { + const defaultProps = { + isSaving: false, + isDisabled: false, + save: jest.fn(), + error: null, + }; + + it('should render normally', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should save properly', () => { + const component = shallow(); + component.find('EuiButton').simulate('click'); + expect(defaultProps.save).toHaveBeenCalledWith(); + }); + + it('should show a saving state', () => { + const customProps = { isSaving: true }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should show a disabled state', () => { + const customProps = { isDisabled: true }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should show an error', () => { + const customProps = { error: 'Test error' }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx new file mode 100644 index 000000000000000..80acb8992cbc164 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface GetStep3Props { + isSaving: boolean; + isDisabled: boolean; + save: () => void; + error: string | null; +} + +export const Step3: React.FC = (props: GetStep3Props) => { + let errorUi = null; + if (props.error) { + errorUi = ( + + +

{props.error}

+
+ +
+ ); + } + + return ( + + {errorUi} + + {i18n.translate('xpack.monitoring.alerts.configuration.save', { + defaultMessage: 'Save', + })} + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx new file mode 100644 index 000000000000000..2bd9804795cb569 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx @@ -0,0 +1,301 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiLink, + EuiSpacer, + EuiFieldNumber, + EuiFieldPassword, + EuiSwitch, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../plugins/actions/common'; +import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; +import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; + +export interface EmailActionData { + service: string; + host: string; + port?: number; + secure: boolean; + from: string; + user: string; + password: string; +} + +interface ManageActionModalProps { + createEmailAction: (handler: EmailActionData) => void; + cancel?: () => void; + isNew: boolean; + action?: ActionResult | null; +} + +const DEFAULT_DATA: EmailActionData = { + service: '', + host: '', + port: 0, + secure: false, + from: '', + user: '', + password: '', +}; + +const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', { + defaultMessage: 'Create email action', +}); +const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', { + defaultMessage: 'Save email action', +}); +const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', { + defaultMessage: 'Cancel', +}); + +const NEW_SERVICE_ID = '__new__'; + +export const ManageEmailAction: React.FC = ( + props: ManageActionModalProps +) => { + const { createEmailAction, cancel, isNew, action } = props; + + const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {}); + const [isSaving, setIsSaving] = React.useState(false); + const [showErrors, setShowErrors] = React.useState(false); + const [errors, setErrors] = React.useState( + getMissingFieldErrors(defaultData, DEFAULT_DATA) + ); + const [data, setData] = React.useState(defaultData); + const [createNewService, setCreateNewService] = React.useState(false); + const [newService, setNewService] = React.useState(''); + + React.useEffect(() => { + const missingFieldErrors = getMissingFieldErrors(data, DEFAULT_DATA); + if (!missingFieldErrors.service) { + if (data.service === NEW_SERVICE_ID && !newService) { + missingFieldErrors.service = getRequiredFieldError('service'); + } + } + setErrors(missingFieldErrors); + }, [data, newService]); + + async function saveEmailAction() { + setShowErrors(true); + if (!hasErrors(errors)) { + setShowErrors(false); + setIsSaving(true); + const mergedData = { + ...data, + service: data.service === NEW_SERVICE_ID ? newService : data.service, + }; + try { + await createEmailAction(mergedData); + } catch (err) { + setErrors({ + general: err.body.message, + }); + } + } + } + + const serviceOptions = ALERT_EMAIL_SERVICES.map(service => ({ + value: service, + inputDisplay: {service}, + dropdownDisplay: {service}, + })); + + serviceOptions.push({ + value: NEW_SERVICE_ID, + inputDisplay: ( + + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText', { + defaultMessage: 'Adding new service...', + })} + + ), + dropdownDisplay: ( + + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addNewServiceText', { + defaultMessage: 'Add new service...', + })} + + ), + }); + + let addNewServiceUi = null; + if (createNewService) { + addNewServiceUi = ( + + + setNewService(e.target.value)} + isInvalid={showErrors} + /> + + ); + } + + return ( + + + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', { + defaultMessage: 'Find out more', + })} + + } + error={errors.service} + isInvalid={showErrors && !!errors.service} + > + + { + if (id === NEW_SERVICE_ID) { + setCreateNewService(true); + setData({ ...data, service: NEW_SERVICE_ID }); + } else { + setCreateNewService(false); + setData({ ...data, service: id }); + } + }} + hasDividers + isInvalid={showErrors && !!errors.service} + /> + {addNewServiceUi} + + + + + setData({ ...data, host: e.target.value })} + isInvalid={showErrors && !!errors.host} + /> + + + + setData({ ...data, port: parseInt(e.target.value, 10) })} + isInvalid={showErrors && !!errors.port} + /> + + + + setData({ ...data, secure: e.target.checked })} + /> + + + + setData({ ...data, from: e.target.value })} + isInvalid={showErrors && !!errors.from} + /> + + + + setData({ ...data, user: e.target.value })} + isInvalid={showErrors && !!errors.user} + /> + + + + setData({ ...data, password: e.target.value })} + isInvalid={showErrors && !!errors.password} + /> + + + + + + + + {isNew ? CREATE_LABEL : SAVE_LABEL} + + + {!action || isNew ? null : ( + + {CANCEL_LABEL} + + )} + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx new file mode 100644 index 000000000000000..258a5b68db372e1 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsStatus, AlertsStatusProps } from './status'; +import { ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { getSetupModeState } from '../../lib/setup_mode'; +import { mockUseEffects } from '../../jest.helpers'; + +jest.mock('../../lib/setup_mode', () => ({ + getSetupModeState: jest.fn(), + addSetupModeCallback: jest.fn(), + toggleSetupMode: jest.fn(), +})); + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsStatusProps = { + clusterUuid: '1adsb23', + emailAddress: 'test@elastic.co', +}; + +describe('Status', () => { + beforeEach(() => { + mockUseEffects(2); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: false, + }); + + (kfetch as jest.Mock).mockImplementation(({ pathname }) => { + if (pathname === '/internal/security/api_key/privileges') { + return { areApiKeysEnabled: true }; + } + return { + data: [], + }; + }); + }); + + it('should render without setup mode', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should render a flyout when clicking the link', async () => { + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(); + component.find('EuiLink').simulate('click'); + await component.update(); + expect(component.find('EuiFlyout')).toMatchSnapshot(); + }); + + it('should render a success message if all alerts have been migrated and in setup mode', async () => { + (kfetch as jest.Mock).mockReturnValue({ + data: [ + { + alertTypeId: ALERT_TYPE_PREFIX, + }, + ], + }); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(); + await component.update(); + expect(component.find('EuiCallOut')).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx new file mode 100644 index 000000000000000..0ee0015ed39a70a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { + EuiSpacer, + EuiCallOut, + EuiTitle, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Alert } from '../../../../alerting/server/types'; +import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; +import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { AlertsConfiguration } from './configuration'; + +export interface AlertsStatusProps { + clusterUuid: string; + emailAddress: string; +} + +export const AlertsStatus: React.FC = (props: AlertsStatusProps) => { + const { emailAddress } = props; + + const [setupModeEnabled, setSetupModeEnabled] = React.useState(getSetupModeState().enabled); + const [kibanaAlerts, setKibanaAlerts] = React.useState([]); + const [showMigrationFlyout, setShowMigrationFlyout] = React.useState(false); + const [isSecurityConfigured, setIsSecurityConfigured] = React.useState(false); + + React.useEffect(() => { + async function fetchAlertsStatus() { + const alerts = await kfetch({ method: 'GET', pathname: `/api/alert/_find` }); + const monitoringAlerts = alerts.data.filter((alert: Alert) => + alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) + ); + setKibanaAlerts(monitoringAlerts); + } + + fetchAlertsStatus(); + fetchSecurityConfigured(); + }, [setupModeEnabled, showMigrationFlyout]); + + React.useEffect(() => { + if (!setupModeEnabled && showMigrationFlyout) { + setShowMigrationFlyout(false); + } + }, [setupModeEnabled, showMigrationFlyout]); + + async function fetchSecurityConfigured() { + const response = await kfetch({ pathname: '/internal/security/api_key/privileges' }); + setIsSecurityConfigured(response.areApiKeysEnabled); + } + + addSetupModeCallback(() => setSetupModeEnabled(getSetupModeState().enabled)); + + function enterSetupModeAndOpenFlyout() { + toggleSetupMode(true); + setShowMigrationFlyout(true); + } + + function getSecurityConfigurationErrorUi() { + if (isSecurityConfigured) { + return null; + } + + const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; + return ( + + + +

+ + {i18n.translate( + 'xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel', + { + defaultMessage: 'docs', + } + )} + + ), + }} + /> +

+
+
+ ); + } + + function renderContent() { + let flyout = null; + if (showMigrationFlyout) { + flyout = ( + setShowMigrationFlyout(false)} aria-labelledby="flyoutTitle"> + + +

+ {i18n.translate('xpack.monitoring.alerts.status.flyoutTitle', { + defaultMessage: 'Monitoring alerts', + })} +

+
+ +

+ {i18n.translate('xpack.monitoring.alerts.status.flyoutSubtitle', { + defaultMessage: 'Configure an email server and email address to receive alerts.', + })} +

+
+ {getSecurityConfigurationErrorUi()} +
+ + setShowMigrationFlyout(false)} + /> + +
+ ); + } + + const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS; + if (allMigrated) { + if (setupModeEnabled) { + return ( + + +

+ + {i18n.translate('xpack.monitoring.alerts.status.manage', { + defaultMessage: 'Want to make changes? Click here.', + })} + +

+
+ {flyout} +
+ ); + } + } else { + return ( + + +

+ + {i18n.translate('xpack.monitoring.alerts.status.needToMigrate', { + defaultMessage: 'Migrate cluster alerts to our new alerting platform.', + })} + +

+
+ {flyout} +
+ ); + } + } + + const content = renderContent(); + if (content) { + return ( + + {content} + + + ); + } + + return null; +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 33b26c7ec56e00d..a8001638f4399d5 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment } from 'react'; +import moment from 'moment-timezone'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; +import { + CALCULATE_DURATION_SINCE, + KIBANA_ALERTING_ENABLED, + ALERT_TYPE_LICENSE_EXPIRATION, + CALCULATE_DURATION_UNTIL, +} from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -21,6 +27,7 @@ import { EuiText, EuiSpacer, EuiCallOut, + EuiLink, } from '@elastic/eui'; export function AlertsPanel({ alerts, changeUrl }) { @@ -82,9 +89,52 @@ export function AlertsPanel({ alerts, changeUrl }) { ); } - const topAlertItems = alerts.map((item, index) => ( - - )); + const alertsList = KIBANA_ALERTING_ENABLED + ? alerts.map((alert, idx) => { + const callOutProps = mapSeverity(alert.severity); + let message = alert.message + // scan message prefix and replace relative times + // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. + .replace( + '#relative', + formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) + ) + .replace('#absolute', moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z')); + + if (!alert.isFiring) { + callOutProps.title = i18n.translate( + 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', + { + defaultMessage: '{severityIconTitle} (resolved {time} ago)', + values: { + severityIconTitle: callOutProps.title, + time: formatTimestampToDuration(alert.resolvedMS, CALCULATE_DURATION_SINCE), + }, + } + ); + callOutProps.color = 'success'; + callOutProps.iconType = 'check'; + } else { + if (alert.type === ALERT_TYPE_LICENSE_EXPIRATION) { + message = ( + + {message} +   + Please update your license + + ); + } + } + + return ( + +

{message}

+
+ ); + }) + : alerts.map((item, index) => ( + + )); return (
@@ -109,7 +159,7 @@ export function AlertsPanel({ alerts, changeUrl }) { - {topAlertItems} + {alertsList}
); diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js index cad4bbf411c347b..eee51c416d11ebe 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js @@ -10,15 +10,22 @@ import { KibanaPanel } from './kibana_panel'; import { LogstashPanel } from './logstash_panel'; import { AlertsPanel } from './alerts_panel'; import { BeatsPanel } from './beats_panel'; - import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsStatus } from '../../alerts/status'; +import { + STANDALONE_CLUSTER_CLUSTER_UUID, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; + const kibanaAlerts = KIBANA_ALERTING_ENABLED ? ( + + ) : null; + return ( @@ -30,6 +37,9 @@ export function Overview(props) { /> + + {kibanaAlerts} + {!isFromStandaloneCluster ? ( diff --git a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts new file mode 100644 index 000000000000000..46ba603d3013820 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +/** + * Suppress React 16.8 act() warnings globally. + * The react teams fix won't be out of alpha until 16.9.0. + * https://github.com/facebook/react/issues/14769#issuecomment-514589856 + */ +const consoleError = console.error; // eslint-disable-line no-console +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + if (!args[0].includes('Warning: An update to %s inside a test was not wrapped in act')) { + consoleError(...args); + } + }); +}); + +export function mockUseEffects(count = 1) { + const spy = jest.spyOn(React, 'useEffect'); + for (let i = 0; i < count; i++) { + spy.mockImplementationOnce(f => f()); + } +} + +// export function mockUseEffectForDeps(deps, count = 1) { +// const spy = jest.spyOn(React, 'useEffect'); +// for (let i = 0; i < count; i++) { +// spy.mockImplementationOnce((f, depList) => { + +// }); +// } +// } diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx similarity index 94% rename from x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js rename to x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx index 9a51a8859692648..22ce32103c208f5 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { contains } from 'lodash'; import { toastNotifications } from 'ui/notify'; +// @ts-ignore import { formatMsg } from 'ui/notify/lib'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -export function formatMonitoringError(err) { +export function formatMonitoringError(err: any) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects // then we can do better messages, such as highlighting the Cluster UUID instead of requiring it be part of the message if (err.status && err.status !== -1 && err.data) { @@ -33,10 +34,10 @@ export function formatMonitoringError(err) { return formatMsg(err); } -export function ajaxErrorHandlersProvider($injector) { +export function ajaxErrorHandlersProvider($injector: any) { const kbnUrl = $injector.get('kbnUrl'); - return err => { + return (err: any) => { if (err.status === 403) { // redirect to error message view kbnUrl.redirect('access-denied'); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts new file mode 100644 index 000000000000000..98d56f9790be475 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.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 { i18n } from '@kbn/i18n'; +import { isString, isNumber, capitalize } from 'lodash'; + +export function getRequiredFieldError(field: string): string { + return i18n.translate('xpack.monitoring.alerts.migrate.manageAction.requiredFieldError', { + defaultMessage: '{field} is a required field.', + values: { + field: capitalize(field), + }, + }); +} + +export function getMissingFieldErrors(data: any, defaultData: any) { + const errors: any = {}; + + for (const key in data) { + if (!data.hasOwnProperty(key)) { + continue; + } + + if (isString(defaultData[key])) { + if (!data[key] || data[key].length === 0) { + errors[key] = getRequiredFieldError(key); + } + } else if (isNumber(defaultData[key])) { + if (isNaN(data[key]) || data[key] === 0) { + errors[key] = getRequiredFieldError(key); + } + } + } + + return errors; +} + +export function hasErrors(errors: any) { + for (const error in errors) { + if (error.length) { + return true; + } + } + return false; +} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index aa931368b34c280..4a2b470f04c7295 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -90,7 +90,7 @@ describe('setup_mode', () => { } catch (err) { error = err; } - expect(error).toEqual( + expect(error.message).toEqual( 'Unable to interact with setup ' + 'mode because the angular injector was not previously set. This needs to be ' + 'set by calling `initSetupModeState`.' @@ -255,9 +255,9 @@ describe('setup_mode', () => { await toggleSetupMode(true); injectorModulesMock.$http.post.mockClear(); await updateSetupModeData(undefined, true); - expect( - injectorModulesMock.$http.post - ).toHaveBeenCalledWith('../api/monitoring/v1/setup/collection/cluster', { ccs: undefined }); + const url = '../api/monitoring/v1/setup/collection/cluster'; + const args = { ccs: undefined }; + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx similarity index 76% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js rename to x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx index 41aae0130761788..d805c10247b2ed6 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx @@ -6,31 +6,49 @@ import React from 'react'; import { render } from 'react-dom'; -import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { npSetup } from 'ui/new_platform'; +import { PluginsSetup } from 'ui/new_platform/new_platform'; +import { CloudSetup } from '../../../../../plugins/cloud/public'; +import { ajaxErrorHandlersProvider } from './ajax_error_handler'; +import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; + +interface PluginsSetupWithCloud extends PluginsSetup { + cloud: CloudSetup; +} -function isOnPage(hash) { +function isOnPage(hash: string) { return contains(window.location.hash, hash); } -const angularState = { +interface IAngularState { + injector: any; + scope: any; +} + +const angularState: IAngularState = { injector: null, scope: null, }; const checkAngularState = () => { if (!angularState.injector || !angularState.scope) { - throw 'Unable to interact with setup mode because the angular injector was not previously set.' + - ' This needs to be set by calling `initSetupModeState`.'; + throw new Error( + 'Unable to interact with setup mode because the angular injector was not previously set.' + + ' This needs to be set by calling `initSetupModeState`.' + ); } }; -const setupModeState = { +interface ISetupModeState { + enabled: boolean; + data: any; + callbacks: Function[]; +} +const setupModeState: ISetupModeState = { enabled: false, data: null, callbacks: [], @@ -38,7 +56,7 @@ const setupModeState = { export const getSetupModeState = () => setupModeState; -export const setNewlyDiscoveredClusterUuid = clusterUuid => { +export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { const globalState = angularState.injector.get('globalState'); const executor = angularState.injector.get('$executor'); angularState.scope.$apply(() => { @@ -48,7 +66,7 @@ export const setNewlyDiscoveredClusterUuid = clusterUuid => { executor.run(); }; -export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) => { +export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { checkAngularState(); const http = angularState.injector.get('$http'); @@ -75,19 +93,19 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) } }; -const notifySetupModeDataChange = oldData => { - setupModeState.callbacks.forEach(cb => cb(oldData)); +const notifySetupModeDataChange = (oldData?: any) => { + setupModeState.callbacks.forEach((cb: Function) => cb(oldData)); }; -export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) => { +export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); if (isCloudEnabled || !hasPermissions) { - let text = null; + let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', @@ -113,9 +131,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { - const liveClusterUuid = get(data, '_meta.liveClusterUuid'); + const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( - node => node.isPartiallyMigrated || node.isFullyMigrated + (node: any) => node.isPartiallyMigrated || node.isFullyMigrated ); if (liveClusterUuid && migratedEsNodes.length > 0) { setNewlyDiscoveredClusterUuid(liveClusterUuid); @@ -140,7 +158,7 @@ export const disableElasticsearchInternalCollection = async () => { } }; -export const toggleSetupMode = inSetupMode => { +export const toggleSetupMode = (inSetupMode: boolean) => { checkAngularState(); const globalState = angularState.injector.get('globalState'); @@ -164,7 +182,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const enabled = !globalState.inSetupMode && !isCloudEnabled; @@ -174,10 +192,14 @@ export const setSetupModeMenuItem = () => { ); }; -export const initSetupModeState = async ($scope, $injector, callback) => { +export const addSetupModeCallback = (callback: Function) => setupModeState.callbacks.push(callback); + +export const initSetupModeState = async ($scope: any, $injector: any, callback?: Function) => { angularState.scope = $scope; angularState.injector = $injector; - callback && setupModeState.callbacks.push(callback); + if (callback) { + setupModeState.callbacks.push(callback); + } const globalState = $injector.get('globalState'); if (globalState.inSetupMode) { diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js index 57a7850b6fd5304..1bfc76b7664579b 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js @@ -24,7 +24,7 @@ function getPageData($injector) { const globalState = $injector.get('globalState'); const $http = $injector.get('$http'); const Private = $injector.get('Private'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/alerts`; + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; const timeBounds = timefilter.getBounds(); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js index bec90f3230571e9..e7107860d61fa89 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; +import { isEmpty } from 'lodash'; +import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -12,7 +14,11 @@ import { MonitoringViewBaseController } from '../../'; import { Overview } from 'plugins/monitoring/components/cluster/overview'; import { I18nContext } from 'ui/i18n'; import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_ALL } from '../../../../common/constants'; +import { + CODE_PATH_ALL, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -31,6 +37,7 @@ uiRoutes.when('/overview', { const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); + const config = $injector.get('config'); super({ title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -58,7 +65,16 @@ uiRoutes.when('/overview', { $scope.$watch( () => this.data, - data => { + async data => { + if (isEmpty(data)) { + return; + } + + let emailAddress = chrome.getInjected('monitoringLegacyEmailAddress') || ''; + if (KIBANA_ALERTING_ENABLED) { + emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; + } + this.renderReact( new Promise(resolve => resolve()), + alertInstanceFactory: (id: string) => new AlertInstance(), + savedObjectsClient: {} as jest.Mocked, + }, + params: {}, + state: {}, + spaceId: '', + name: '', + tags: [], + createdBy: null, + updatedBy: null, +}; + +describe('getLicenseExpiration', () => { + const emailAddress = 'foo@foo.com'; + const server: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: (): any => ({ + get: () => new Promise(resolve => resolve(emailAddress)), + }), + }, + }, + }, + }; + const getMonitoringCluster: () => void = jest.fn(); + const logger: Logger = { + warn: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + info: jest.fn(), + get: jest.fn(), + }; + const getLogger = (): Logger => logger; + const ccrEnabled = false; + + afterEach(() => { + (logger.warn as jest.Mock).mockClear(); + }); + + it('should have the right id and actionGroups', () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); + expect(alert.actionGroups).toEqual(['default']); + }); + + it('should return the state if no license is provided', async () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const services: MockServices | AlertServices = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + const state = { foo: 1 }; + + const result = await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect(result).toEqual(state); + }); + + it('should log a warning if no email is provided', async () => { + const customServer: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: () => ({ + get: () => null, + }), + }, + }, + }, + }; + const alert = getLicenseExpiration(customServer, getMonitoringCluster, getLogger, ccrEnabled); + + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense({ + status: 'good', + type: 'basic', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + const state = {}; + + await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect((logger.warn as jest.Mock).mock.calls.length).toBe(1); + expect(logger.warn).toHaveBeenCalledWith( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + }); + + it('should fire actions if going to expire', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'NEW X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should fire actions if the user fixed their license', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(120, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state: AlertState = { + [clusterUuid]: { + expiredCheckDateMS: moment() + .subtract(1, 'day') + .valueOf(), + ui: { isFiring: true, severity: 0, message: null, resolvedMS: 0, expirationTime: 0 }, + }, + }; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(0); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'RESOLVED X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should not fire actions for trial license that expire in more than 14 days', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(15, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(undefined); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should fire actions for trial license that in 14 days or less', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(13, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts new file mode 100644 index 000000000000000..197c5c9cdcbc72f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -0,0 +1,162 @@ +/* + * 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 moment from 'moment-timezone'; +import { get } from 'lodash'; +import { Legacy } from 'kibana'; +import { Logger } from 'src/core/server'; +import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { AlertType } from '../../../alerting'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; +import { fetchDefaultEmailAddress } from '../lib/alerts/fetch_default_email_address'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { + AlertLicense, + AlertState, + AlertClusterState, + AlertClusterUiState, + LicenseExpirationAlertExecutorOptions, +} from './types'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; + +export const getLicenseExpiration = ( + server: Legacy.Server, + getMonitoringCluster: any, + getLogger: (contexts: string[]) => Logger, + ccsEnabled: boolean +): AlertType => { + async function getCallCluster(services: any): Promise { + const monitoringCluster = await getMonitoringCluster(); + if (!monitoringCluster) { + return services.callCluster; + } + + return monitoringCluster.callCluster; + } + + const logger = getLogger([ALERT_TYPE_LICENSE_EXPIRATION]); + return { + id: ALERT_TYPE_LICENSE_EXPIRATION, + name: 'Monitoring Alert - License Expiration', + actionGroups: ['default'], + async executor({ + services, + params, + state, + }: LicenseExpirationAlertExecutorOptions): Promise { + logger.debug( + `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const callCluster = await getCallCluster(services); + + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (ccsEnabled) { + const availableCcs = await fetchAvailableCcs(callCluster); + if (availableCcs.length > 0) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + } + + const clusters = await fetchClusters(callCluster, esIndexPattern); + + // Fetch licensing information from cluster_stats documents + const licenses: AlertLicense[] = await fetchLicenses(callCluster, clusters, esIndexPattern); + if (licenses.length === 0) { + logger.warn(`No license found for ${ALERT_TYPE_LICENSE_EXPIRATION}.`); + return state; + } + + const uiSettings = server.newPlatform.__internals.uiSettings.asScopedToClient( + services.savedObjectsClient + ); + const dateFormat: string = await uiSettings.get('dateFormat'); + const timezone: string = await uiSettings.get('dateFormat:tz'); + const emailAddress = await fetchDefaultEmailAddress(uiSettings); + if (!emailAddress) { + // TODO: we can do more here + logger.warn( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + return; + } + + const result: AlertState = { ...state }; + + for (const license of licenses) { + const licenseState: AlertClusterState = state[license.clusterUuid] || {}; + const $expiry = moment.utc(license.expiryDateMS); + let isExpired = false; + let severity = 0; + + if (license.status !== 'active') { + isExpired = true; + severity = 2001; + } else if (license.expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (license.type === 'trial' && i < 2) { + break; + } + + const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); + if ($fromNow.isAfter($expiry)) { + isExpired = true; + severity = 1000 * i; + break; + } + } + } + + const ui: AlertClusterUiState = get(licenseState, 'ui', { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }); + let resolved = ui.resolvedMS; + let message = ui.message; + let expiredCheckDate = licenseState.expiredCheckDateMS; + const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); + + if (isExpired) { + if (!licenseState.expiredCheckDateMS) { + logger.debug(`License will expire soon, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress); + expiredCheckDate = moment().valueOf(); + } + message = getUiMessage(license, timezone); + resolved = 0; + } else if (!isExpired && licenseState.expiredCheckDateMS) { + logger.debug(`License expiration has been resolved, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expiredCheckDate = 0; + message = getUiMessage(license, timezone, true); + resolved = moment().valueOf(); + } + + result[license.clusterUuid] = { + expiredCheckDateMS: expiredCheckDate, + ui: { + message, + expirationTime: license.expiryDateMS, + isFiring: expiredCheckDate > 0, + severity, + resolvedMS: resolved, + }, + }; + } + + return result; + }, + }; +}; diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts new file mode 100644 index 000000000000000..6346ca00dabbd27 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Moment } from 'moment'; +import { AlertExecutorOptions } from '../../../alerting'; + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + clusterName: string; +} + +export interface AlertState { + [clusterUuid: string]: AlertClusterState; +} + +export interface AlertClusterState { + expiredCheckDateMS: number | Moment; + ui: AlertClusterUiState; +} + +export interface AlertClusterUiState { + isFiring: boolean; + severity: number; + message: string | null; + resolvedMS: number; + expirationTime: number; +} + +export interface AlertCluster { + clusterUuid: string; +} + +export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions { + state: AlertState; +} + +export interface AlertParams { + dateFormat: string; + timezone: string; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts new file mode 100644 index 000000000000000..4398b2dd675ec32 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchAvailableCcs } from './fetch_available_ccs'; + +describe('fetchAvailableCcs', () => { + it('should call the `cluster.remoteInfo` api', async () => { + const callCluster = jest.fn(); + await fetchAvailableCcs(callCluster); + expect(callCluster).toHaveBeenCalledWith('cluster.remoteInfo'); + }); + + it('should return clusters that are connected', async () => { + const connectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [connectedRemote]: { + connected: true, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result).toEqual([connectedRemote]); + }); + + it('should not return clusters that are connected', async () => { + const disconnectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [disconnectedRemote]: { + connected: false, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result.length).toBe(0); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts new file mode 100644 index 000000000000000..34efaff93f34c77 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export async function fetchAvailableCcs(callCluster: any): Promise { + const availableCcs = []; + const response = await callCluster('cluster.remoteInfo'); + for (const remoteName in response) { + if (!response.hasOwnProperty(remoteName)) { + continue; + } + const remoteInfo = response[remoteName]; + if (remoteInfo.connected) { + availableCcs.push(remoteName); + } + } + return availableCcs; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts new file mode 100644 index 000000000000000..78eb9773df15fb4 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchClusters } from './fetch_clusters'; + +describe('fetchClusters', () => { + it('return a list of clusters', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + aggregations: { + clusters: { + buckets: [ + { + key: 'clusterA', + }, + ], + }, + }, + })); + const index = '.monitoring-es-*'; + const result = await fetchClusters(callCluster, index); + expect(result).toEqual([{ clusterUuid: 'clusterA' }]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const index = '.monitoring-es-*'; + await fetchClusters(callCluster, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts new file mode 100644 index 000000000000000..8ef7339618a2c08 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertCluster } from '../../alerts/types'; + +interface AggregationResult { + key: string; +} + +export async function fetchClusters(callCluster: any, index: string): Promise { + const params = { + index, + filterPath: 'aggregations.clusters.buckets', + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size: 1000, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'aggregations.clusters.buckets', []).map((bucket: AggregationResult) => ({ + clusterUuid: bucket.key, + })); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts new file mode 100644 index 000000000000000..25b09b956038a0b --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchDefaultEmailAddress } from './fetch_default_email_address'; +import { uiSettingsServiceMock } from '../../../../../../../src/core/server/mocks'; + +describe('fetchDefaultEmailAddress', () => { + it('get the email address', async () => { + const email = 'test@test.com'; + const uiSettingsClient = uiSettingsServiceMock.createClient(); + uiSettingsClient.get.mockResolvedValue(email); + const result = await fetchDefaultEmailAddress(uiSettingsClient); + expect(result).toBe(email); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts new file mode 100644 index 000000000000000..88e4199a88256a8 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'src/core/server'; +import { MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS } from '../../../common/constants'; + +export async function fetchDefaultEmailAddress( + uiSettingsClient: IUiSettingsClient +): Promise { + return await uiSettingsClient.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 000000000000000..dd6c074e68b1f9e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + it('return a list of licenses', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: clusterName, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); + + it('should give priority to the metadata name', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: 'fakeName', + cluster_uuid: clusterUuid, + cluster_settings: { + cluster: { + metadata: { + display_name: clusterName, + }, + }, + }, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 000000000000000..31a68e8aa9c3ee8 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash'; +import { AlertLicense, AlertCluster } from '../../alerts/types'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_settings.cluster.metadata.display_name', + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.cluster_name', + ], + body: { + size: 1, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map(cluster => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'hits.hits', []).map((hit: any) => { + const clusterName: string = + get(hit, '_source.cluster_settings.cluster.metadata.display_name') || + get(hit, '_source.cluster_name') || + get(hit, '_source.cluster_uuid'); + const rawLicense: any = get(hit, '_source.license', {}); + const license: AlertLicense = { + status: rawLicense.status, + type: rawLicense.type, + expiryDateMS: rawLicense.expiry_date_in_millis, + clusterUuid: get(hit, '_source.cluster_uuid'), + clusterName, + }; + return license; + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts new file mode 100644 index 000000000000000..9f7c1d5a994d27d --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -0,0 +1,87 @@ +/* + * 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 moment from 'moment'; +import { get } from 'lodash'; +import { AlertClusterState } from '../../alerts/types'; +import { ALERT_TYPES, LOGGING_TAG } from '../../../common/constants'; + +export async function fetchStatus( + callCluster: any, + start: number, + end: number, + clusterUuid: string, + server: any +): Promise { + // TODO: this shouldn't query task manager directly but rather + // use an api exposed by the alerting/actions plugin + // See https://github.com/elastic/kibana/issues/48442 + const statuses = await Promise.all( + ALERT_TYPES.map( + type => + new Promise(async (resolve, reject) => { + try { + const params = { + index: '.kibana_task_manager', + filterPath: ['hits.hits._source.task.state'], + body: { + size: 1, + sort: [{ updated_at: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + term: { + 'task.taskType': `alerting:${type}`, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + const state = get(response, 'hits.hits[0]._source.task.state', '{}'); + const clusterState: AlertClusterState = get( + JSON.parse(state), + `alertTypeState.${clusterUuid}`, + { + expiredCheckDateMS: 0, + ui: { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }, + } + ); + const isInBetween = moment(clusterState.ui.resolvedMS).isBetween(start, end); + if (clusterState.ui.isFiring || isInBetween) { + return resolve({ + type, + ...clusterState.ui, + }); + } + return resolve(false); + } catch (err) { + const reason = get(err, 'body.error.type'); + if (reason === 'index_not_found_exception') { + server.log( + ['error', LOGGING_TAG], + `Unable to fetch alerts. Alerts depends on task manager, which has not been started yet.` + ); + } else { + server.log(['error', LOGGING_TAG], err.message); + } + return resolve(false); + } + }) + ) + ); + + return statuses.filter(Boolean); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts new file mode 100644 index 000000000000000..a5eb10498616120 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { getCcsIndexPattern } from './get_ccs_index_pattern'; + +describe('getCcsIndexPattern', () => { + it('should return an index pattern including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe('.monitoring-es-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*'); + }); + + it('should return an index pattern from multiple index patterns including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*,.monitoring-kibana-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe( + '.monitoring-es-*,.monitoring-kibana-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*,Remote1:.monitoring-kibana-*,Remote2:.monitoring-kibana-*' + ); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts new file mode 100644 index 000000000000000..b562fde2a0810f3 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string { + return `${indexPattern},${indexPattern + .split(',') + .map(pattern => { + return remotes.map(remoteName => `${remoteName}:${pattern}`).join(','); + }) + .join(',')}`; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts new file mode 100644 index 000000000000000..1a2eb1e44be8431 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts @@ -0,0 +1,55 @@ +/* + * 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 moment from 'moment-timezone'; +import { executeActions, getUiMessage } from './license_expiration.lib'; + +describe('licenseExpiration lib', () => { + describe('executeActions', () => { + const clusterName = 'clusterA'; + const instance: any = { scheduleActions: jest.fn() }; + const license: any = { clusterName }; + const $expiry = moment('2020-01-20'); + const dateFormat = 'dddd, MMMM Do YYYY, h:mm:ss a'; + const emailAddress = 'test@test.com'; + + beforeEach(() => { + instance.scheduleActions.mockClear(); + }); + + it('should schedule actions when firing', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, false); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'NEW X-Pack Monitoring: License Expiration', + message: `Cluster '${clusterName}' license is going to expire on Monday, January 20th 2020, 12:00:00 am. Please update your license.`, + to: emailAddress, + }); + }); + + it('should schedule actions when resolved', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'RESOLVED X-Pack Monitoring: License Expiration', + message: `This cluster alert has been resolved: Cluster '${clusterName}' license was going to expire on Monday, January 20th 2020, 12:00:00 am.`, + to: emailAddress, + }); + }); + }); + + describe('getUiMessage', () => { + const timezone = 'Europe/London'; + const license: any = { expiryDateMS: moment.tz('2020-01-20 08:00:00', timezone).utc() }; + + it('should return a message when firing', () => { + const message = getUiMessage(license, timezone, false); + expect(message).toBe(`This cluster's license is going to expire in #relative at #absolute.`); + }); + + it('should return a message when resolved', () => { + const message = getUiMessage(license, timezone, true); + expect(message).toBe(`This cluster's license is active.`); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts new file mode 100644 index 000000000000000..8a75fc1fbbd82f2 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Moment } from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { AlertInstance } from '../../../../alerting/server/alert_instance'; +import { AlertLicense } from '../../alerts/types'; + +const RESOLVED_SUBJECT = i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', + { + defaultMessage: 'RESOLVED X-Pack Monitoring: License Expiration', + } +); + +const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.newSubject', { + defaultMessage: 'NEW X-Pack Monitoring: License Expiration', +}); + +export function executeActions( + instance: AlertInstance, + license: AlertLicense, + $expiry: Moment, + dateFormat: string, + emailAddress: string, + resolved: boolean = false +) { + if (resolved) { + instance.scheduleActions('default', { + subject: RESOLVED_SUBJECT, + message: `This cluster alert has been resolved: Cluster '${ + license.clusterName + }' license was going to expire on ${$expiry.format(dateFormat)}.`, + to: emailAddress, + }); + } else { + instance.scheduleActions('default', { + subject: NEW_SUBJECT, + message: `Cluster '${license.clusterName}' license is going to expire on ${$expiry.format( + dateFormat + )}. Please update your license.`, + to: emailAddress, + }); + } +} + +export function getUiMessage(license: AlertLicense, timezone: string, resolved: boolean = false) { + if (resolved) { + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `This cluster's license is active.`, + }); + } + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `This cluster's license is going to expire in #relative at #absolute.`, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 2b080a5c333fcfd..a5426dc04545eb4 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -16,6 +16,7 @@ import { getBeatsForClusters } from '../beats'; import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_aggregation'; import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search'; import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; +import { fetchStatus } from '../alerts/fetch_status'; import { getClustersSummary } from './get_clusters_summary'; import { CLUSTER_ALERTS_SEARCH_SIZE, @@ -27,6 +28,7 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, + KIBANA_ALERTING_ENABLED, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; @@ -99,15 +101,31 @@ export async function getClustersFromRequest( if (mlJobs !== null) { cluster.ml = { jobs: mlJobs }; } - const alerts = isInCodePath(codePaths, [CODE_PATH_ALERTS]) - ? await alertsClusterSearch(req, alertsIndex, cluster, checkLicenseForAlerts, { + + if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { + if (KIBANA_ALERTING_ENABLED) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const callCluster = (...args) => callWithRequest(req, ...args); + cluster.alerts = await fetchStatus( + callCluster, start, end, - size: CLUSTER_ALERTS_SEARCH_SIZE, - }) - : null; - if (alerts) { - cluster.alerts = alerts; + cluster.cluster_uuid, + req.server + ); + } else { + cluster.alerts = await alertsClusterSearch( + req, + alertsIndex, + cluster, + checkLicenseForAlerts, + { + start, + end, + size: CLUSTER_ALERTS_SEARCH_SIZE, + } + ); + } } cluster.logs = isInCodePath(codePaths, [CODE_PATH_LOGS]) diff --git a/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js new file mode 100644 index 000000000000000..89cbf20d9b56f79 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export async function getDateFormat(req) { + return await req.getUiSettingsService().get('dateFormat'); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 5f52e0c6a983b75..a12b48510a6ff74 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -348,7 +348,6 @@ export const getCollectionStatus = async ( }, }; } - const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index ef346e95ad0757b..50e5319a0f5263f 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -5,12 +5,17 @@ */ import { i18n } from '@kbn/i18n'; -import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants'; +import { + LOGGING_TAG, + KIBANA_MONITORING_LOGGING_TAG, + KIBANA_ALERTING_ENABLED, +} from '../common/constants'; import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; +import { getLicenseExpiration } from './alerts/license_expiration'; import { parseElasticsearchConfig } from './es_client/parse_elasticsearch_config'; export class Plugin { @@ -133,5 +138,37 @@ export class Plugin { showCgroupMetricsLogstash: config.get('monitoring.ui.container.logstash.enabled'), // Note, not currently used, but see https://github.com/elastic/x-pack-kibana/issues/1559 part 2 }; }); + + if (KIBANA_ALERTING_ENABLED && plugins.alerting) { + // this is not ready right away but we need to register alerts right away + async function getMonitoringCluster() { + const configs = config.get('xpack.monitoring.elasticsearch'); + if (configs.hosts) { + const monitoringCluster = plugins.elasticsearch.getCluster('monitoring'); + const { username, password } = configs; + const fakeRequest = { + headers: { + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, + }, + }; + return { + callCluster: (...args) => monitoringCluster.callWithRequest(fakeRequest, ...args), + }; + } + return null; + } + + function getLogger(contexts) { + return core.logger.get('plugins', LOGGING_TAG, ...contexts); + } + plugins.alerting.setup.registerType( + getLicenseExpiration( + core._hapi, + getMonitoringCluster, + getLogger, + config.get('xpack.monitoring.ccs.enabled') + ) + ); + } } } diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js new file mode 100644 index 000000000000000..f87683effe437c1 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js @@ -0,0 +1,89 @@ +/* + * 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 Joi from 'joi'; +import { isFunction } from 'lodash'; +import { + ALERT_TYPE_LICENSE_EXPIRATION, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, +} from '../../../../../common/constants'; + +async function createAlerts(req, alertsClient, { selectedEmailActionId }) { + const createdAlerts = []; + + // Create alerts + const ALERT_TYPES = { + [ALERT_TYPE_LICENSE_EXPIRATION]: { + schedule: { interval: '10s' }, + actions: [ + { + group: 'default', + id: selectedEmailActionId, + params: { + subject: '{{context.subject}}', + message: `{{context.message}}`, + to: ['{{context.to}}'], + }, + }, + ], + }, + }; + + for (const alertTypeId of Object.keys(ALERT_TYPES)) { + const existingAlert = await alertsClient.find({ + options: { + search: alertTypeId, + }, + }); + if (existingAlert.total === 1) { + await alertsClient.delete({ id: existingAlert.data[0].id }); + } + + const result = await alertsClient.create({ + data: { + enabled: true, + alertTypeId, + ...ALERT_TYPES[alertTypeId], + }, + }); + createdAlerts.push(result); + } + + return createdAlerts; +} + +async function saveEmailAddress(emailAddress, uiSettingsService) { + await uiSettingsService.set(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, emailAddress); +} + +export function createKibanaAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/alerts', + config: { + validate: { + payload: Joi.object({ + selectedEmailActionId: Joi.string().required(), + emailAddress: Joi.string().required(), + }), + }, + }, + async handler(req, headers) { + const { emailAddress, selectedEmailActionId } = req.payload; + const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; + if (!alertsClient) { + return headers.response().code(404); + } + + const [alerts, emailResponse] = await Promise.all([ + createAlerts(req, alertsClient, { ...req.params, selectedEmailActionId }), + saveEmailAddress(emailAddress, req.getUiSettingsService()), + ]); + + return { alerts, emailResponse }; + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js index cdcd776b349fc68..246cdfde97cff87 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js @@ -4,54 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; -import { checkLicense } from '../../../../cluster_alerts/check_license'; -import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; - -/* - * Cluster Alerts route. - */ -export function clusterAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/clusters/{clusterUuid}/alerts', - config: { - validate: { - params: Joi.object({ - clusterUuid: Joi.string().required(), - }), - payload: Joi.object({ - ccs: Joi.string().optional(), - timeRange: Joi.object({ - min: Joi.date().required(), - max: Joi.date().required(), - }).required(), - }), - }, - }, - handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; - const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); - const options = { - start: req.payload.timeRange.min, - end: req.payload.timeRange.max, - }; - - return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => - alertsClusterSearch( - req, - alertsIndex, - { cluster_uuid: clusterUuid, license }, - checkLicense, - options - ) - ); - }, - }); -} +export * from './legacy_alerts'; +export * from './alerts'; diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js new file mode 100644 index 000000000000000..a3049f0f3e2d2ec --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; +import { checkLicense } from '../../../../cluster_alerts/check_license'; +import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; +import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; + +/* + * Cluster Alerts route. + */ +export function legacyClusterAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/clusters/{clusterUuid}/legacy_alerts', + config: { + validate: { + params: Joi.object({ + clusterUuid: Joi.string().required(), + }), + payload: Joi.object({ + ccs: Joi.string().optional(), + timeRange: Joi.object({ + min: Joi.date().required(), + max: Joi.date().required(), + }).required(), + }), + }, + }, + handler(req) { + const config = server.config(); + const ccs = req.payload.ccs; + const clusterUuid = req.params.clusterUuid; + const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); + const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); + const options = { + start: req.payload.timeRange.min, + end: req.payload.timeRange.max, + }; + + return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => + alertsClusterSearch( + req, + alertsIndex, + { cluster_uuid: clusterUuid, license }, + checkLicense, + options + ) + ); + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js index baffbfd5f3f6f22..de0213ec84689d8 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js @@ -6,7 +6,7 @@ // all routes for the app export { checkAccessRoute } from './check_access'; -export { clusterAlertsRoute } from './alerts/'; +export * from './alerts/'; export { beatsDetailRoute, beatsListingRoute, beatsOverviewRoute } from './beats'; export { clusterRoute, clustersRoute } from './cluster'; export { diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index 9251deb673bd116..49f167b0f1b1031 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -6,6 +6,10 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; +import { + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from './common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; /** @@ -14,28 +18,48 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; * app (injectDefaultVars and hacks) * @return {Object} data per Kibana plugin uiExport schema */ -export const getUiExports = () => ({ - app: { - title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { - defaultMessage: 'Stack Monitoring', - }), - order: 9002, - description: i18n.translate('xpack.monitoring.uiExportsDescription', { - defaultMessage: 'Monitoring for Elastic Stack', - }), - icon: 'plugins/monitoring/icons/monitoring.svg', - euiIconType: 'monitoringApp', - linkToLastSubUrl: false, - main: 'plugins/monitoring/monitoring', - category: DEFAULT_APP_CATEGORIES.management, - }, - injectDefaultVars(server) { - const config = server.config(); - return { - monitoringUiEnabled: config.get('monitoring.ui.enabled'), +export const getUiExports = () => { + const uiSettingDefaults = {}; + if (KIBANA_ALERTING_ENABLED) { + uiSettingDefaults[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS] = { + name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { + defaultMessage: 'Alerting email address', + }), + value: '', + description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { + defaultMessage: `The default email address to receive alerts from Stack Monitoring`, + }), + category: ['monitoring'], }; - }, - hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], - home: ['plugins/monitoring/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), -}); + } + + return { + app: { + title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { + defaultMessage: 'Stack Monitoring', + }), + order: 9002, + description: i18n.translate('xpack.monitoring.uiExportsDescription', { + defaultMessage: 'Monitoring for Elastic Stack', + }), + icon: 'plugins/monitoring/icons/monitoring.svg', + euiIconType: 'monitoringApp', + linkToLastSubUrl: false, + main: 'plugins/monitoring/monitoring', + category: DEFAULT_APP_CATEGORIES.management, + }, + injectDefaultVars(server) { + const config = server.config(); + return { + monitoringUiEnabled: config.get('monitoring.ui.enabled'), + monitoringLegacyEmailAddress: config.get( + 'monitoring.cluster_alerts.email_notifications.email_address' + ), + }; + }, + uiSettingDefaults, + hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], + home: ['plugins/monitoring/register_feature'], + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }; +}; diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 784125b83859d4e..fbd7404a2f15eb3 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -9,3 +9,10 @@ export interface ActionType { name: string; enabled: boolean; } + +export interface ActionResult { + id: string; + actionTypeId: string; + name: string; + config: Record; +} From d5ba32ff085cbbe4f9e0478ba261a6a2d2af129c Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Feb 2020 13:11:48 -0800 Subject: [PATCH 02/22] [APM] Fixes maxTraceItems in waterfall trace and related error queries (#56111) * Addresses #55494 by combining waterfall trace and related error queries. Makes sure that maxTraceItems is only respected for Transactions and Spans. * addressed feedback back separating the combined query into 1 for transactions/spans and 1 for errors + errorCount Co-authored-by: Elastic Machine --- .../Marks/__test__/get_error_marks.test.ts | 15 +- .../Marks/get_error_marks.ts | 9 +- .../WaterfallContainer/Waterfall/index.tsx | 6 +- .../waterfall_helpers.test.ts.snap | 141 +++++++++--------- .../waterfall_helpers.test.ts | 17 ++- .../waterfall_helpers/waterfall_helpers.ts | 91 +++++++++-- .../plugins/apm/public/hooks/useWaterfall.ts | 2 +- .../errors/__snapshots__/queries.test.ts.snap | 64 -------- .../get_trace_errors_per_transaction.ts | 65 -------- .../apm/server/lib/errors/queries.test.ts | 9 -- .../traces/__snapshots__/queries.test.ts.snap | 50 +++---- .../apm/server/lib/traces/get_trace.ts | 9 +- .../apm/server/lib/traces/get_trace_items.ts | 80 +++++++++- x-pack/legacy/plugins/apm/typings/common.d.ts | 4 + 14 files changed, 267 insertions(+), 295 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts index 8fd8edd7f8a728a..b7e83073a205b13 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IWaterfallItem } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; +import { IWaterfallError } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; import { getErrorMarks } from '../get_error_marks'; describe('getErrorMarks', () => { @@ -12,13 +12,6 @@ describe('getErrorMarks', () => { it('when items are missing', () => { expect(getErrorMarks([], {})).toEqual([]); }); - it('when any error is available', () => { - const items = [ - { docType: 'span' }, - { docType: 'transaction' } - ] as IWaterfallItem[]; - expect(getErrorMarks(items, {})).toEqual([]); - }); }); it('returns error marks', () => { @@ -29,14 +22,13 @@ describe('getErrorMarks', () => { skew: 5, doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } } as unknown, - { docType: 'transaction' }, { docType: 'error', offset: 50, skew: 0, doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } } as unknown - ] as IWaterfallItem[]; + ] as IWaterfallError[]; expect( getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' }) ).toEqual([ @@ -67,14 +59,13 @@ describe('getErrorMarks', () => { skew: 5, doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } } as unknown, - { docType: 'transaction' }, { docType: 'error', offset: 50, skew: 0, doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } } as unknown - ] as IWaterfallItem[]; + ] as IWaterfallError[]; expect(getErrorMarks(items, {})).toEqual([ { type: 'errorMark', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts index f1f0163a49d1050..e2b00c13c5c1f4d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -6,7 +6,6 @@ import { isEmpty } from 'lodash'; import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/ErrorRaw'; import { - IWaterfallItem, IWaterfallError, IServiceColors } from '../Waterfall/waterfall_helpers/waterfall_helpers'; @@ -19,16 +18,14 @@ export interface ErrorMark extends Mark { } export const getErrorMarks = ( - items: IWaterfallItem[], + errorItems: IWaterfallError[], serviceColors: IServiceColors ): ErrorMark[] => { - if (isEmpty(items)) { + if (isEmpty(errorItems)) { return []; } - return (items.filter( - item => item.docType === 'error' - ) as IWaterfallError[]).map(error => ({ + return errorItems.map(error => ({ type: 'errorMark', offset: error.offset + error.skew, verticalLine: false, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index b48fc1cf7ca27ee..4f584786f2f9a5d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -80,13 +80,9 @@ export const Waterfall: React.FC = ({ const { serviceColors, duration } = waterfall; const agentMarks = getAgentMarks(waterfall.entryTransaction); - const errorMarks = getErrorMarks(waterfall.items, serviceColors); + const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors); const renderWaterfallItem = (item: IWaterfallItem) => { - if (item.docType === 'error') { - return null; - } - const errorCount = item.docType === 'transaction' ? waterfall.errorsPerTransaction[item.doc.transaction.id] diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 5b1b9be33c37539..c9b29e8692f4479 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -24,6 +24,77 @@ Object { "name": "GET /api", }, }, + "errorItems": Array [ + Object { + "doc": Object { + "agent": Object { + "name": "ruby", + "version": "2", + }, + "error": Object { + "grouping_key": "errorGroupingKey1", + "id": "error1", + "log": Object { + "message": "error message", + }, + }, + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "error", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795810000, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + ], "errorsCount": 1, "errorsPerTransaction": Object { "myTransactionId1": 2, @@ -716,75 +787,6 @@ Object { "parentId": "mySpanIdA", "skew": 0, }, - Object { - "doc": Object { - "agent": Object { - "name": "ruby", - "version": "2", - }, - "error": Object { - "grouping_key": "errorGroupingKey1", - "id": "error1", - "log": Object { - "message": "error message", - }, - }, - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "error", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795810000, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "docType": "error", - "duration": 0, - "id": "error1", - "offset": 25994, - "parent": Object { - "doc": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", - }, - }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, - "skew": 0, - }, - "parentId": "myTransactionId1", - "skew": 0, - }, ], "rootTransaction": Object { "processor": Object { @@ -848,6 +850,7 @@ Object { "name": "Api::ProductsController#index", }, }, + "errorItems": Array [], "errorsCount": 0, "errorsPerTransaction": Object { "myTransactionId1": 2, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 426842bc02f510c..6b13b93200c6174 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -12,7 +12,8 @@ import { getOrderedWaterfallItems, getWaterfall, IWaterfallItem, - IWaterfallTransaction + IWaterfallTransaction, + IWaterfallError } from './waterfall_helpers'; import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; @@ -100,7 +101,9 @@ describe('waterfall_helpers', () => { } }, timestamp: { us: 1549324795823304 } - } as unknown) as Transaction, + } as unknown) as Transaction + ]; + const errorDocs = [ ({ processor: { event: 'error' }, parent: { id: 'myTransactionId1' }, @@ -130,14 +133,15 @@ describe('waterfall_helpers', () => { }; const waterfall = getWaterfall( { - trace: { items: hits, exceedsMax: false }, + trace: { items: hits, errorDocs, exceedsMax: false }, errorsPerTransaction }, entryTransactionId ); - expect(waterfall.items.length).toBe(7); + expect(waterfall.items.length).toBe(6); expect(waterfall.items[0].id).toBe('myTransactionId1'); + expect(waterfall.errorItems.length).toBe(1); expect(waterfall.errorsCount).toEqual(1); expect(waterfall).toMatchSnapshot(); }); @@ -150,7 +154,7 @@ describe('waterfall_helpers', () => { }; const waterfall = getWaterfall( { - trace: { items: hits, exceedsMax: false }, + trace: { items: hits, errorDocs, exceedsMax: false }, errorsPerTransaction }, entryTransactionId @@ -158,6 +162,7 @@ describe('waterfall_helpers', () => { expect(waterfall.items.length).toBe(4); expect(waterfall.items[0].id).toBe('myTransactionId2'); + expect(waterfall.errorItems.length).toBe(0); expect(waterfall.errorsCount).toEqual(0); expect(waterfall).toMatchSnapshot(); }); @@ -386,7 +391,7 @@ describe('waterfall_helpers', () => { it('should return parent skew for errors', () => { const child = { docType: 'error' - } as IWaterfallItem; + } as IWaterfallError; const parent = { docType: 'transaction', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 1af6cddb3ba4a75..3b52163aa7fa407 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -24,6 +24,8 @@ interface IWaterfallGroup { [key: string]: IWaterfallItem[]; } +const ROOT_ID = 'root'; + export interface IWaterfall { entryTransaction?: Transaction; rootTransaction?: Transaction; @@ -36,6 +38,7 @@ export interface IWaterfall { errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; errorsCount: number; serviceColors: IServiceColors; + errorItems: IWaterfallError[]; } interface IWaterfallItemBase { @@ -70,10 +73,7 @@ export type IWaterfallTransaction = IWaterfallItemBase< export type IWaterfallSpan = IWaterfallItemBase; export type IWaterfallError = IWaterfallItemBase; -export type IWaterfallItem = - | IWaterfallTransaction - | IWaterfallSpan - | IWaterfallError; +export type IWaterfallItem = IWaterfallTransaction | IWaterfallSpan; function getTransactionItem(transaction: Transaction): IWaterfallTransaction { return { @@ -99,20 +99,34 @@ function getSpanItem(span: Span): IWaterfallSpan { }; } -function getErrorItem(error: APMError): IWaterfallError { - return { +function getErrorItem( + error: APMError, + items: IWaterfallItem[], + entryWaterfallTransaction?: IWaterfallTransaction +): IWaterfallError { + const entryTimestamp = entryWaterfallTransaction?.doc.timestamp.us ?? 0; + const parent = items.find( + waterfallItem => waterfallItem.id === error.parent?.id + ); + const errorItem: IWaterfallError = { docType: 'error', doc: error, id: error.error.id, - parentId: error.parent?.id, - offset: 0, + parent, + parentId: parent?.id, + offset: error.timestamp.us - entryTimestamp, skew: 0, duration: 0 }; + + return { + ...errorItem, + skew: getClockSkew(errorItem, parent) + }; } export function getClockSkew( - item: IWaterfallItem, + item: IWaterfallItem | IWaterfallError, parentItem?: IWaterfallItem ) { if (!parentItem) { @@ -218,13 +232,11 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => return getSpanItem(item as Span); case 'transaction': return getTransactionItem(item as Transaction); - case 'error': - return getErrorItem(item as APMError); } }); const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => - groupBy(waterfallItems, item => (item.parentId ? item.parentId : 'root')); + groupBy(waterfallItems, item => (item.parentId ? item.parentId : ROOT_ID)); const getEntryWaterfallTransaction = ( entryTransactionId: string, @@ -234,6 +246,48 @@ const getEntryWaterfallTransaction = ( item => item.docType === 'transaction' && item.id === entryTransactionId ) as IWaterfallTransaction; +function isInEntryTransaction( + parentIdLookup: Map, + entryTransactionId: string, + currentId: string +): boolean { + if (currentId === entryTransactionId) { + return true; + } + const parentId = parentIdLookup.get(currentId); + if (parentId) { + return isInEntryTransaction(parentIdLookup, entryTransactionId, parentId); + } + return false; +} + +function getWaterfallErrors( + errorDocs: TraceAPIResponse['trace']['errorDocs'], + items: IWaterfallItem[], + entryWaterfallTransaction?: IWaterfallTransaction +) { + const errorItems = errorDocs.map(errorDoc => + getErrorItem(errorDoc, items, entryWaterfallTransaction) + ); + if (!entryWaterfallTransaction) { + return errorItems; + } + const parentIdLookup = [...items, ...errorItems].reduce( + (map, { id, parentId }) => { + map.set(id, parentId ?? ROOT_ID); + return map; + }, + new Map() + ); + return errorItems.filter(errorItem => + isInEntryTransaction( + parentIdLookup, + entryWaterfallTransaction?.id, + errorItem.id + ) + ); +} + export function getWaterfall( { trace, errorsPerTransaction }: TraceAPIResponse, entryTransactionId?: Transaction['transaction']['id'] @@ -244,7 +298,8 @@ export function getWaterfall( items: [], errorsPerTransaction, errorsCount: sum(Object.values(errorsPerTransaction)), - serviceColors: {} + serviceColors: {}, + errorItems: [] }; } @@ -261,6 +316,11 @@ export function getWaterfall( childrenByParentId, entryWaterfallTransaction ); + const errorItems = getWaterfallErrors( + trace.errorDocs, + items, + entryWaterfallTransaction + ); const rootTransaction = getRootTransaction(childrenByParentId); const duration = getWaterfallDuration(items); @@ -274,7 +334,8 @@ export function getWaterfall( duration, items, errorsPerTransaction, - errorsCount: items.filter(item => item.docType === 'error').length, - serviceColors + errorsCount: errorItems.length, + serviceColors, + errorItems }; } diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts index f3f10c0e46d9b97..8d623a026822257 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts @@ -11,7 +11,7 @@ import { getWaterfall } from '../components/app/TransactionDetails/WaterfallWith const INITIAL_DATA = { root: undefined, - trace: { items: [], exceedsMax: false }, + trace: { items: [], exceedsMax: false, errorDocs: [] }, errorsPerTransaction: {} }; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 88d8edd17454a52..a2629366dd6d931 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -205,67 +205,3 @@ Object { "index": "myIndex", } `; - -exports[`error queries fetches trace errors 1`] = ` -Object { - "body": Object { - "aggs": Object { - "transactions": Object { - "terms": Object { - "execution_hint": "map", - "field": "transaction.id", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "trace.id": "foo", - }, - }, - Object { - "term": Object { - "processor.event": "error", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - ], - "should": Array [ - Object { - "bool": Object { - "must_not": Array [ - Object { - "exists": Object { - "field": "error.log.level", - }, - }, - ], - }, - }, - Object { - "terms": Object { - "error.log.level": Array [ - "critical", - "error", - "fatal", - ], - }, - }, - ], - }, - }, - "size": 0, - }, - "index": "myIndex", -} -`; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts deleted file mode 100644 index 6027693be518018..000000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ERROR_LOG_LEVEL, - PROCESSOR_EVENT, - TRACE_ID, - TRANSACTION_ID -} from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../helpers/range_filter'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; - -export interface ErrorsPerTransaction { - [transactionId: string]: number; -} - -const includedLogLevels = ['critical', 'error', 'fatal']; - -export async function getTraceErrorsPerTransaction( - traceId: string, - setup: Setup & SetupTimeRange -): Promise { - const { start, end, client, indices } = setup; - - const params = { - index: indices['apm_oss.errorIndices'], - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, - { range: rangeFilter(start, end) } - ], - should: [ - { bool: { must_not: [{ exists: { field: ERROR_LOG_LEVEL } }] } }, - { terms: { [ERROR_LOG_LEVEL]: includedLogLevels } } - ] - } - }, - aggs: { - transactions: { - terms: { - field: TRANSACTION_ID, - // high cardinality - execution_hint: 'map' - } - } - } - } - } as const; - - const resp = await client.search(params); - return (resp.aggregations?.transactions.buckets || []).reduce( - (acc, bucket) => ({ - ...acc, - [bucket.key]: bucket.doc_count - }), - {} - ); -} diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts index 2b1704d9424e479..f1e5d31efd4bdbd 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts @@ -6,7 +6,6 @@ import { getErrorGroup } from './get_error_group'; import { getErrorGroups } from './get_error_groups'; -import { getTraceErrorsPerTransaction } from './get_trace_errors_per_transaction'; import { SearchParamsMock, inspectSearchParams @@ -56,12 +55,4 @@ describe('error queries', () => { expect(mock.params).toMatchSnapshot(); }); - - it('fetches trace errors', async () => { - mock = await inspectSearchParams(setup => - getTraceErrorsPerTransaction('foo', setup) - ); - - expect(mock.params).toMatchSnapshot(); - }); }); diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index a2828e1d74920a8..0a9f9d38b2be700 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -3,6 +3,15 @@ exports[`trace queries fetches a trace 1`] = ` Object { "body": Object { + "aggs": Object { + "by_transaction_id": Object { + "terms": Object { + "execution_hint": "map", + "field": "transaction.id", + "size": "myIndex", + }, + }, + }, "query": Object { "bool": Object { "filter": Array [ @@ -12,12 +21,8 @@ Object { }, }, Object { - "terms": Object { - "processor.event": Array [ - "span", - "transaction", - "error", - ], + "term": Object { + "processor.event": "error", }, }, Object { @@ -30,36 +35,19 @@ Object { }, }, ], - "should": Object { - "exists": Object { - "field": "parent.id", + "must_not": Object { + "terms": Object { + "error.log.level": Array [ + "debug", + "info", + "warning", + ], }, }, }, }, "size": "myIndex", - "sort": Array [ - Object { - "_score": Object { - "order": "asc", - }, - }, - Object { - "transaction.duration.us": Object { - "order": "desc", - }, - }, - Object { - "span.duration.us": Object { - "order": "desc", - }, - }, - ], - "track_total_hits": true, }, - "index": Array [ - "myIndex", - "myIndex", - ], + "index": "myIndex", } `; diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts index e38ce56edde80e5..a1b9270e0d7b3ba 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts @@ -5,16 +5,15 @@ */ import { PromiseReturnType } from '../../../typings/common'; -import { getTraceErrorsPerTransaction } from '../errors/get_trace_errors_per_transaction'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTraceItems } from './get_trace_items'; export type TraceAPIResponse = PromiseReturnType; export async function getTrace(traceId: string, setup: Setup & SetupTimeRange) { - const [trace, errorsPerTransaction] = await Promise.all([ - getTraceItems(traceId, setup), - getTraceErrorsPerTransaction(traceId, setup) - ]); + const { errorsPerTransaction, ...trace } = await getTraceItems( + traceId, + setup + ); return { trace, diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts index 8118b6acaee3975..9d3e0d6db7f16fb 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts @@ -9,13 +9,20 @@ import { TRACE_ID, PARENT_ID, TRANSACTION_DURATION, - SPAN_DURATION + SPAN_DURATION, + TRANSACTION_ID, + ERROR_LOG_LEVEL } from '../../../common/elasticsearch_fieldnames'; import { Span } from '../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; import { APMError } from '../../../typings/es_schemas/ui/APMError'; import { rangeFilter } from '../helpers/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { PromiseValueType } from '../../../typings/common'; + +interface ErrorsPerTransaction { + [transactionId: string]: number; +} export async function getTraceItems( traceId: string, @@ -23,8 +30,36 @@ export async function getTraceItems( ) { const { start, end, client, config, indices } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; + const excludedLogLevels = ['debug', 'info', 'warning']; - const params = { + const errorResponsePromise = client.search({ + index: indices['apm_oss.errorIndices'], + body: { + size: maxTraceItems, + query: { + bool: { + filter: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [PROCESSOR_EVENT]: 'error' } }, + { range: rangeFilter(start, end) } + ], + must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } } + } + }, + aggs: { + by_transaction_id: { + terms: { + field: TRANSACTION_ID, + size: maxTraceItems, + // high cardinality + execution_hint: 'map' as const + } + } + } + } + }); + + const traceResponsePromise = client.search({ index: [ indices['apm_oss.spanIndices'], indices['apm_oss.transactionIndices'] @@ -35,7 +70,7 @@ export async function getTraceItems( bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction', 'error'] } }, + { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) } ], should: { @@ -50,12 +85,43 @@ export async function getTraceItems( ], track_total_hits: true } - }; + }); + + const [errorResponse, traceResponse]: [ + // explicit intermediary types to avoid TS "excessively deep" error + PromiseValueType, + PromiseValueType + // @ts-ignore + ] = await Promise.all([errorResponsePromise, traceResponsePromise]); + + const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const resp = await client.search(params); + const items = (traceResponse.hits.hits as Array<{ + _source: Transaction | Span; + }>).map(hit => hit._source); + + const errorFrequencies: { + errorsPerTransaction: ErrorsPerTransaction; + errorDocs: APMError[]; + } = { + errorDocs: errorResponse.hits.hits.map( + ({ _source }) => _source as APMError + ), + errorsPerTransaction: + errorResponse.aggregations?.by_transaction_id.buckets.reduce( + (acc, current) => { + return { + ...acc, + [current.key]: current.doc_count + }; + }, + {} as ErrorsPerTransaction + ) ?? {} + }; return { - items: resp.hits.hits.map(hit => hit._source), - exceedsMax: resp.hits.total.value > maxTraceItems + items, + exceedsMax, + ...errorFrequencies }; } diff --git a/x-pack/legacy/plugins/apm/typings/common.d.ts b/x-pack/legacy/plugins/apm/typings/common.d.ts index b9064980bd6577d..1e718f818246c9b 100644 --- a/x-pack/legacy/plugins/apm/typings/common.d.ts +++ b/x-pack/legacy/plugins/apm/typings/common.d.ts @@ -22,6 +22,10 @@ type AllowUnknownObjectProperties = T extends object } : T; +export type PromiseValueType = Value extends Promise + ? Value + : Value; + export type PromiseReturnType = Func extends ( ...args: any[] ) => Promise From 077d24de10a79f8a747782d54d1c574a876bed32 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Feb 2020 13:14:42 -0800 Subject: [PATCH 03/22] Closes #55502. Fixes firefox SVG error by preventing tooltip and marks (#56578) from rendering if there are no values in the Plot. Co-authored-by: Elastic Machine --- .../components/shared/charts/CustomPlot/InteractivePlot.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js index bc758c7288e96e6..69b73ad6a0c0fa5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js @@ -66,9 +66,13 @@ class InteractivePlot extends PureComponent { const tooltipPoints = this.getTooltipPoints(hoverX); const markPoints = this.getMarkPoints(hoverX); - const { x, yTickValues } = plotValues; + const { x, xTickValues, yTickValues } = plotValues; const yValueMiddle = yTickValues[1]; + if (isEmpty(xTickValues)) { + return ; + } + return ( {hoverX && ( From e08df006c3a3d49ec8612c35298d2a68be12fced Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Feb 2020 13:19:23 -0800 Subject: [PATCH 04/22] [APM] Fix initial error sort field (#56577) * Closes #52840. Changes the initial sorting field from `latestOccurrenceAt` -> `occurrenceCount` * update jest snapshots Co-authored-by: Elastic Machine --- .../__test__/__snapshots__/List.test.tsx.snap | 68 +++++++++---------- .../app/ErrorGroupOverview/List/index.tsx | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 492d28206f3dda5..a45357121354f1f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -43,7 +43,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` } initialPageSize={25} initialSortDirection="desc" - initialSortField="latestOccurrenceAt" + initialSortField="occurrenceCount" items={Array []} noItemsMessage="No errors were found" sortItems={false} @@ -190,7 +190,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` List should render empty state 1`] = ` } > "`; +exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"
"`; -exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; +exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; diff --git a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap index 18f84f41d5d9989..307c0760de7ba4f 100644 --- a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -8,7 +8,6 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index d96febee7b06d28..d320b57ee59e66a 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 170cc77ca37cc16..27a8c1fab6c8e86 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 85c76071d1e9440..51bb7240dd7c44a 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index ade93c9f500990f..9ee0e3de51d8b55 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap index b2c503806c38542..260d7de3aefd4ba 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap @@ -62,11 +62,7 @@ exports[`DetailView should render TabContent 1`] = ` `; exports[`DetailView should render tabs 1`] = ` - + - <path d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 00-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 010 8.373zM8 15A6.956 6.956 0 013.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 002.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 018 15zm-5.601-2.813a6.963 6.963 0 010-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 003 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 118 4a4 4 0 010 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 008 3a4.979 4.979 0 00-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 018 1zm0-1a8.001 8.001 0 10.003 16.002A8.001 8.001 0 008 0z" fill-rule="evenodd" diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap index 48e442ce734cf90..5f5f3a2d40f954b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap @@ -26,7 +26,6 @@ exports[`TransactionActionMenu component should match the snapshot 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M13.069 5.157L8.384 9.768a.546.546 0 01-.768 0L2.93 5.158a.552.552 0 00-.771 0 .53.53 0 000 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 000-.76.552.552 0 00-.771 0z" fill-rule="non-zero" diff --git a/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot index c1cb45123f04bc2..b394fc30c8d60f4 100644 --- a/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot @@ -26,7 +26,8 @@ exports[`Storyshots components/FontPicker default 1`] = ` className="euiScreenReaderOnly" id="generated-id" > - Select an option: , is selected + Select an option: + , is selected </span> <button aria-haspopup="true" @@ -37,9 +38,7 @@ exports[`Storyshots components/FontPicker default 1`] = ` onKeyDown={[Function]} role="option" type="button" - > - - </button> + /> <div className="euiFormControlLayoutIcons euiFormControlLayoutIcons--right" > diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap index acd68622f1af0f3..e5e13671057bd60 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with default propertie </style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div><div class=\\"root\\" style=\\"height: 48px;\\"><div class=\\"root\\"><div class=\\"slideContainer\\"><div class=\\"root\\" style=\\"height: 100px; width: 150px;\\"><div class=\\"preview\\" style=\\"height: 720px; width: 1080px;\\"><div id=\\"page-7186b301-f8a7-4c65-8b89-38d68d31cfc4\\" class=\\"root\\" style=\\"height: 720px; width: 1080px; background: rgb(119, 119, 119);\\"><div class=\\"canvasPositionable canvasInteractable\\" style=\\"width: 1082px; height: 205.37748344370857px; margin-left: -541px; margin-top: -102.68874172185429px; position: absolute;\\"><div class=\\"root\\"><div class=\\"container s2042575598\\" style=\\"overflow: hidden;\\"><style type=\\"text/css\\">.s2042575598 .canvasRenderEl h1 { font-size: 150px; text-align: center; color: #d3d3d3; } -</style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div></div></div><div class=\\"bar\\" style=\\"bottom: 0px;\\"><div class=\\"euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem title\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiLink euiLink--primary\\" href=\\"https://www.elastic.co\\" rel=\\"\\" title=\\"Powered by Elastic.co\\"><svg width=\\"32\\" height=\\"32\\" viewBox=\\"0 0 32 32\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--large euiIcon-isLoaded\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap index 8a8799207ace818..73d7599a603590a 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap @@ -65,7 +65,6 @@ exports[` can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -85,7 +84,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -110,7 +108,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -130,7 +127,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -219,7 +215,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -239,7 +234,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -264,7 +258,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -284,7 +277,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -317,7 +309,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -366,7 +357,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -381,7 +371,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -561,7 +550,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -581,7 +569,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -606,7 +593,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -626,7 +612,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -715,7 +700,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -735,7 +719,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -760,7 +743,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -780,7 +762,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -813,7 +794,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -871,7 +851,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -886,7 +865,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -927,4 +905,4 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = </div> `; -exports[`<Settings /> can navigate Toolbar Settings, closes when activated 3`] = `"<div><div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"disabled\\"><div class=\\"euiPanel euiPopover__panel euiPopover__panel--top euiPopover__panel-withTitle\\" aria-live=\\"assertive\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-describedby=\\"generated-id\\" style=\\"top: -16px; left: -22px; z-index: 2000;\\"><div class=\\"euiPopover__panelArrow euiPopover__panelArrow--top\\" style=\\"left: 10px; top: 0px;\\"></div><div><div class=\\"euiContextMenu\\" style=\\"height: 0px;\\"><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txOutLeft\\" tabindex=\\"0\\"><div class=\\"euiPopoverTitle\\"><span class=\\"euiContextMenu__itemLayout\\">Settings</span></div><div><div><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiContextMenu__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>Auto Play
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9256bee4e756b55..353dc58e6d40118 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -296,7 +296,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem } > @@ -1401,7 +1399,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -1871,7 +1868,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -2806,7 +2802,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 2dc355513ece243..c62b07a89e7a3e8 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -33,6 +33,7 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -110,6 +111,7 @@ exports[`should not show "within" relation when filter geometry is not closed 1` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -214,6 +216,7 @@ exports[`should render error message 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -294,6 +297,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap index 9a55e46b40aea3d..9d07b9c641e0f96 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap @@ -15,6 +15,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index ee76657c8d27a4c..4dd77842894c655 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -3,25 +3,17 @@ exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="uniDirectional" - grow={true} hasActiveFilters={true} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Unidirectional </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="biDirectional" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Bidirectional </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap index 4c9a27b76060cbe..8f40d0203afd43a 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap @@ -5,16 +5,12 @@ exports[`GroupsFilterPopover renders correctly against snapshot 1`] = ` anchorPosition="downCenter" button={ <EuiFilterButton - color="text" data-test-subj="groups-filter-popover-button" - grow={true} hasActiveFilters={false} - iconSide="right" iconType="arrowDown" isSelected={false} numActiveFilters={0} onClick={[Function]} - type="button" > Groups </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index fac91f75978f092..747ac63551b55ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -110,25 +110,17 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="show-elastic-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Elastic jobs </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="show-custom-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Custom jobs </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index 7e3e099bf0276ed..28481e9970a5ebd 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` <EuiOverlayMask> <EuiModal - maxWidth={true} onClose={[Function]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap index 8930dedfa0035db..6e422bc13f06be2 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -5,7 +5,6 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` <EuiModal className="spcConfirmDeleteModal" initialFocus="input[name=\\"confirmDeleteSpaceInput\\"]" - maxWidth={true} onClose={[MockFunction]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 6eed58a78421248..3a4861f4fbc9ec9 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -9,8 +9,6 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - // @ts-ignore - EuiConfirmModal, EuiFieldText, EuiFormRow, EuiModal, @@ -89,7 +87,7 @@ class ConfirmDeleteModalUI extends Component<Props, State> { // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation - const modalProps: EuiModalProps & CommonProps = { + const modalProps: Omit<EuiModalProps, 'children'> & CommonProps = { onClose: onCancel, className: 'spcConfirmDeleteModal', initialFocus: 'input[name="confirmDeleteSpaceInput"]', diff --git a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap index d7702e2f18d44bf..750afcfc44e7e37 100644 --- a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap @@ -3,7 +3,6 @@ exports[`ConfirmAlterActiveSpaceModal renders as expected 1`] = ` <EuiOverlayMask> <EuiConfirmModal - buttonColor="primary" cancelButtonText="Cancel" confirmButtonText="Update space" defaultFocusedButton="confirm" diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap index 2d3351ec1c0d20b..da9153f4a6c8d67 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap @@ -6,26 +6,18 @@ exports[`FilterBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="all" numFilters={2} onClick={[Function]} - type="button" > all </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="critical" numFilters={2} onClick={[Function]} - type="button" > critical </EuiFilterButton> diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap index b36e0c1a2bfdb09..dfc69c57cfff6bd 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap @@ -6,24 +6,16 @@ exports[`GroupByBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="message" onClick={[Function]} - type="button" > by issue </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="index" onClick={[Function]} - type="button" > by index </EuiFilterButton> diff --git a/x-pack/package.json b/x-pack/package.json index ad0be351483f616..99e2a32bf3372ff 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -176,7 +176,7 @@ "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index bb5d553b26b364a..5399d1393717906 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -84,7 +84,7 @@ It allows you to monitor the performance of thousands of applications in real ti '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html', }, }), - euiIconType: 'logoAPM', + euiIconType: 'apmApp', artifacts, onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index 42fd4417e238b11..f8bbfbc8bb33dec 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -49,7 +49,6 @@ exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.59 10.059L7.35 5.18h1.3L8.4 10.06h-.81zm.394 1.901a.61.61 0 01-.448-.186.606.606 0 01-.186-.444c0-.174.062-.323.186-.446a.614.614 0 01.448-.184c.169 0 .315.06.44.182.124.122.186.27.186.448a.6.6 0 01-.189.446.607.607 0 01-.437.184zM2 14a1 1 0 01-.878-1.479l6-11a1 1 0 011.756 0l6 11A1 1 0 0114 14H2zm0-1h12L8 2 2 13z" fillRule="evenodd" @@ -189,7 +188,6 @@ exports[`APIKeysGridPage renders permission denied if user does not have require width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap index 37db2e118861e86..a2741773f183bf2 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap @@ -39,6 +39,7 @@ exports[`<SimplePrivilegeForm> renders without crashing 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index e9f2f946e988596..8d10e27df9694eb 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -176,6 +176,7 @@ exports[`<PrivilegeSpaceForm> renders without crashing 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap index 970cbfd03954a47..4789314d9f780ba 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap @@ -67,7 +67,6 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47e11817ffa5d40..3a6ba45413de600 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "データ", "kbn.embeddable.inspectorRequestDescription": "このリクエストは Elasticsearch にクエリをかけ、検索データを取得します。", "kbn.embeddable.search.displayName": "検索", - "kbn.home.addData.addDataToKibanaDescription": "これらのソリューションで、データを作成済みのダッシュボードと監視システムへとすぐに変えることができます。", - "kbn.home.addData.addDataToKibanaTitle": "Kibana にデータを追加", "kbn.home.addData.apm.addApmButtonLabel": "APM を追加", "kbn.home.addData.apm.nameDescription": "APM は、集約内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", "kbn.home.addData.apm.nameTitle": "APM", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 86d9a69dc0900b0..f7cbaa7d72158db 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "数据", "kbn.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "kbn.embeddable.search.displayName": "搜索", - "kbn.home.addData.addDataToKibanaDescription": "使用这些解决方案可快速将您的数据转换成预建仪表板和监测系统。", - "kbn.home.addData.addDataToKibanaTitle": "将数据添加到 Kibana", "kbn.home.addData.apm.addApmButtonLabel": "添加 APM", "kbn.home.addData.apm.nameDescription": "APM 自动从您的应用程序内收集深入全面的性能指标和错误。", "kbn.home.addData.apm.nameTitle": "APM", diff --git a/yarn.lock b/yarn.lock index a3acc2ae216c5aa..4b56ec6460775b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1953,10 +1953,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@18.2.1": - version "18.2.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.2.1.tgz#6ce6d0bd1d0541052d21f2918305524d71e91678" - integrity sha512-6C5tnWJTlBB++475i0vRoCsnz4JaYznb4zMNFLc+z5GY3vA3/E3AXTjmmBwybEicCCi3h1SnpJxZsgMakiZwRA== +"@elastic/eui@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.3.0.tgz#e21c6246624f694e2ae1c7c1f1a11b612faf260a" + integrity sha512-Rkj1rTtDa6iZMUF7pxYRojku1sLXzTU0FK1D9i0XE3H//exy3VyTV6qUlbdkiKXjO7emrgQqfzKDeXT+ZYztgg== dependencies: "@types/chroma-js" "^1.4.3" "@types/lodash" "^4.14.116" From 4d3803d310f991dd0ee9f1214b7c6bda50498263 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad <frank.hassanabad@elastic.co> Date: Mon, 3 Feb 2020 16:40:29 -0700 Subject: [PATCH 11/22] [SIEM][Detection Engine] Critical blocker, fixes pre-packaged rule miscounts ## Summary * Found multiple issues with how unstable finds can occur where iterating over multiple pages of find API with saved objects might return the same results per page and omit things as you try to figure out which pre-packaged rules are installed and which ones are not. * This makes a distinct trade off of doing more JSON.parse() on the event loop by querying all the pre-packaged rules at one time. This however gives a stable and accurate count * Fixed the tags aggregation to do the same thing. * Fixes https://github.com/elastic/siem-team/issues/506 ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../get_existing_prepackaged_rules.test.ts | 191 ++++-------------- .../rules/get_existing_prepackaged_rules.ts | 55 ++--- .../lib/detection_engine/tags/read_tags.ts | 46 ++--- 3 files changed, 73 insertions(+), 219 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index dc308263baab614..8d00ddb18be6b97 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -35,32 +35,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -75,40 +50,17 @@ describe('get_existing_prepackaged_rules', () => { result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.params.immutable = true; - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -137,7 +89,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { + test('should return 2 items over 1 page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -146,11 +98,19 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 2, + }) ); + + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; @@ -160,7 +120,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([result1, result2]); }); - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -172,37 +132,17 @@ describe('get_existing_prepackaged_rules', () => { const result3 = getResult(); result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 3, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -241,80 +181,27 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ - data: [result1, result2, result3], - perPage: 3, + data: [result1], + perPage: 1, page: 1, - total: 3, + total: 2, }) ); + // second mock which will return all the data on a single page + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) + ); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rules = await getRules({ alertsClient: unsafeCast, filter: '', }); - expect(rules).toEqual([result1, result2, result3]); + expect(rules).toEqual([result1, result2]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index b7ab6a97634a800..a48957da7aa9429 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -9,7 +9,6 @@ import { AlertsClient } from '../../../../../alerting'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; -export const DEFAULT_PER_PAGE = 100; export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`; export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; @@ -33,84 +32,56 @@ export const getRulesCount = async ({ filter, perPage: 1, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); return firstRule.total; }; export const getRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, filter, }: { alertsClient: AlertsClient; - perPage?: number; filter: string; }): Promise<RuleAlertType[]> => { - const firstPrepackedRules = await findRules({ + const count = await getRulesCount({ alertsClient, filter }); + const rules = await findRules({ alertsClient, filter, - perPage, + perPage: count, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); - const totalPages = Math.ceil(firstPrepackedRules.total / firstPrepackedRules.perPage); - if (totalPages <= 1) { - if (isAlertTypes(firstPrepackedRules.data)) { - return firstPrepackedRules.data; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } - } else { - const returnPrepackagedRules = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ - alertsClient, - filter, - perPage, - page: page + 2, - }); - }) - .reduce<Promise<object[]>>(async (accum, nextPage) => { - return [...(await accum), ...(await nextPage).data]; - }, Promise.resolve(firstPrepackedRules.data)); - if (isAlertTypes(returnPrepackagedRules)) { - return returnPrepackagedRules; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } + if (isAlertTypes(rules.data)) { + return rules.data; + } else { + // If this was ever true, you have a really messed up system. + // This is keep typescript happy since we have an unknown with data + return []; } }; export const getNonPackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_NON_PREPACKED_RULES, }); }; export const getExistingPrepackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_PREPACKED_RULES, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index 0f973d816917fc6..02456732df3b4e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -9,8 +9,6 @@ import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting'; import { findRules } from '../rules/find_rules'; -const DEFAULT_PER_PAGE: number = 1000; - export interface TagType { id: string; tags: string[]; @@ -42,39 +40,37 @@ export const convertTagsToSet = (tagObjects: object[]): Set<string> => { // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<string[]> => { - const tags = await readRawTags({ alertsClient, perPage }); + const tags = await readRawTags({ alertsClient }); return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); }; export const readRawTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; perPage?: number; }): Promise<string[]> => { - const firstTags = await findRules({ alertsClient, fields: ['tags'], perPage, page: 1 }); - const firstSet = convertTagsToSet(firstTags.data); - const totalPages = Math.ceil(firstTags.total / firstTags.perPage); - if (totalPages <= 1) { - return Array.from(firstSet); - } else { - const returnTags = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ alertsClient, fields: ['tags'], perPage, page: page + 2 }); - }) - .reduce<Promise<Set<string>>>(async (accum, nextTagPage) => { - const tagArray = convertToTags((await nextTagPage).data); - return new Set([...(await accum), ...tagArray]); - }, Promise.resolve(firstSet)); - - return Array.from(returnTags); - } + // Get just one record so we can get the total count + const firstTags = await findRules({ + alertsClient, + fields: ['tags'], + perPage: 1, + page: 1, + sortField: 'createdAt', + sortOrder: 'desc', + }); + // Get all the rules to aggregate over all the tags of the rules + const rules = await findRules({ + alertsClient, + fields: ['tags'], + perPage: firstTags.total, + sortField: 'createdAt', + sortOrder: 'desc', + page: 1, + }); + const tagSet = convertTagsToSet(rules.data); + return Array.from(tagSet); }; From 653c28a8895e08665f35e8c88c8fc1413f67bff4 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian <andrew@andrewvc.com> Date: Mon, 3 Feb 2020 20:09:56 -0600 Subject: [PATCH 12/22] [Uptime] Add unit tests for QueryContext time calculation (#56671) Add Unit tests for the QueryContext class that was missing testing. This would have caught #56612 --- .../search/__tests__/query_context.test.ts | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts new file mode 100644 index 000000000000000..8924d07ac0c4dca --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { QueryContext } from '../query_context'; +import { CursorPagination } from '../..'; +import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; + +describe(QueryContext, () => { + // 10 minute range + const rangeStart = '2019-02-03T19:06:54.939Z'; + const rangeEnd = '2019-02-03T19:16:54.939Z'; + + const pagination: CursorPagination = { + cursorDirection: CursorDirection.AFTER, + sortOrder: SortOrder.DESC, + }; + + let qc: QueryContext; + beforeEach(() => (qc = new QueryContext({}, rangeStart, rangeEnd, pagination, null, 10))); + + describe('dateRangeFilter()', () => { + const expectedRange = { + range: { + '@timestamp': { + gte: rangeStart, + lte: rangeEnd, + }, + }, + }; + describe('when hasTimespan() is true', () => { + it('should create a date range filter including the timespan', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(true); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual({ + bool: { + filter: [ + expectedRange, + { + bool: { + should: [ + qc.timespanClause(), + { bool: { must_not: { exists: { field: 'monitor.timespan' } } } }, + ], + }, + }, + ], + }, + }); + }); + }); + + describe('when hasTimespan() is false', () => { + it('should only use the timestamp fields in the returned filter', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(false); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual(expectedRange); + }); + }); + }); + + describe('timespanClause()', () => { + it('should always cover the last 5m', () => { + // 5m expected range between GTE and LTE in the response + // since timespan is hardcoded to 5m + expect(qc.timespanClause()).toEqual({ + range: { + 'monitor.timespan': { + // end date minus 5m + gte: new Date(Date.parse(rangeEnd) - 5 * 60 * 1000).toISOString(), + lte: rangeEnd, + }, + }, + }); + }); + }); +}); From 0f117c9c3276c40f40a4812c128a266db6174346 Mon Sep 17 00:00:00 2001 From: Nick Partridge <nick.ryan.partridge@gmail.com> Date: Mon, 3 Feb 2020 22:05:32 -0600 Subject: [PATCH 13/22] Vislib replacement toggle (#56439) * Add new vislib replacement plugin shell * Add config to toggle new vislib replacement --- .github/CODEOWNERS | 1 + .i18nrc.json | 6 +- .sass-lint.yml | 1 + .../vis_type_vislib/public/plugin.ts | 32 ++++++-- src/legacy/core_plugins/vis_type_xy/index.ts | 56 +++++++++++++ .../core_plugins/vis_type_xy/package.json | 4 + .../core_plugins/vis_type_xy/public/index.ts | 25 ++++++ .../core_plugins/vis_type_xy/public/legacy.ts | 44 ++++++++++ .../core_plugins/vis_type_xy/public/plugin.ts | 82 +++++++++++++++++++ 9 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 src/legacy/core_plugins/vis_type_xy/index.ts create mode 100644 src/legacy/core_plugins/vis_type_xy/package.json create mode 100644 src/legacy/core_plugins/vis_type_xy/public/index.ts create mode 100644 src/legacy/core_plugins/vis_type_xy/public/legacy.ts create mode 100644 src/legacy/core_plugins/vis_type_xy/public/plugin.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0b0addf117f6fce..de7159489689e9a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app +/src/legacy/core_plugins/vis_type_xy/ @elastic/kibana-app # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon /src/plugins/home/public @elastic/kibana-app /src/plugins/home/server/*.ts @elastic/kibana-app diff --git a/.i18nrc.json b/.i18nrc.json index 1230151212f5737..7d7685b5c1ef1b0 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -22,7 +22,10 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "management": ["src/legacy/core_plugins/management", "src/plugins/management"], + "management": [ + "src/legacy/core_plugins/management", + "src/plugins/management" + ], "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", "kibana_utils": "src/plugins/kibana_utils", @@ -43,6 +46,7 @@ "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], "visTypeVega": "src/legacy/core_plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", + "visTypeXy": "src/legacy/core_plugins/vis_type_xy", "visualizations": [ "src/plugins/visualizations", "src/legacy/core_plugins/visualizations" diff --git a/.sass-lint.yml b/.sass-lint.yml index fba2c003484f6a9..9c64c1e5eea569c 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -3,6 +3,7 @@ files: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss' + - 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 9bf7ee3d594013d..056849a2926570e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -39,6 +39,7 @@ import { createGoalVisTypeDefinition, } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../../../plugins/charts/public'; +import { ConfigSchema as VisTypeXyConfigSchema } from '../../vis_type_xy'; export interface VisTypeVislibDependencies { uiSettings: IUiSettingsClient; @@ -72,11 +73,7 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> { uiSettings: core.uiSettings, charts, }; - - expressions.registerFunction(createVisTypeVislibVisFn); - expressions.registerFunction(createPieVisFn); - - [ + const vislibTypes = [ createHistogramVisTypeDefinition, createLineVisTypeDefinition, createPieVisTypeDefinition, @@ -85,7 +82,30 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> { createHorizontalBarVisTypeDefinition, createGaugeVisTypeDefinition, createGoalVisTypeDefinition, - ].forEach(vis => visualizations.types.createBaseVisualization(vis(visualizationDependencies))); + ]; + const vislibFns = [createVisTypeVislibVisFn, createPieVisFn]; + + const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as + | VisTypeXyConfigSchema['visTypeXy'] + | undefined; + + // if visTypeXy plugin is disabled it's config will be undefined + if (!visTypeXy || !visTypeXy.enabled) { + const convertedTypes: any[] = []; + const convertedFns: any[] = []; + + // Register legacy vislib types that have been converted + convertedFns.forEach(expressions.registerFunction); + convertedTypes.forEach(vis => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + // Register non-converted types + vislibFns.forEach(expressions.registerFunction); + vislibTypes.forEach(vis => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); } public start(core: CoreStart, deps: VisTypeVislibPluginStartDependencies) { diff --git a/src/legacy/core_plugins/vis_type_xy/index.ts b/src/legacy/core_plugins/vis_type_xy/index.ts new file mode 100644 index 000000000000000..975399f891503dc --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/index.ts @@ -0,0 +1,56 @@ +/* + * 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 { resolve } from 'path'; +import { Legacy } from 'kibana'; + +import { LegacyPluginApi, LegacyPluginInitializer } from '../../types'; + +export interface ConfigSchema { + visTypeXy: { + enabled: boolean; + }; +} + +const visTypeXyPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => + new Plugin({ + id: 'visTypeXy', + require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + hacks: [resolve(__dirname, 'public/legacy')], + injectDefaultVars(server): ConfigSchema { + const config = server.config(); + + return { + visTypeXy: { + enabled: config.get('visTypeXy.enabled') as boolean, + }, + }; + }, + }, + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(false), + }).default(); + }, + } as Legacy.PluginSpecOptions); + +// eslint-disable-next-line import/no-default-export +export default visTypeXyPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_xy/package.json b/src/legacy/core_plugins/vis_type_xy/package.json new file mode 100644 index 000000000000000..920f7dcb44e87c3 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/package.json @@ -0,0 +1,4 @@ +{ + "name": "visTypeXy", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/index.ts b/src/legacy/core_plugins/vis_type_xy/public/index.ts new file mode 100644 index 000000000000000..218dc8aa8a68310 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from '../../../../core/public'; +import { VisTypeXyPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts new file mode 100644 index 000000000000000..e1cee9c30804a20 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts @@ -0,0 +1,44 @@ +/* + * 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 { npSetup, npStart } from 'ui/new_platform'; +import { PluginInitializerContext } from 'kibana/public'; + +import { plugin } from '.'; +import { VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies } from './plugin'; +import { + setup as visualizationsSetup, + start as visualizationsStart, +} from '../../visualizations/public/np_ready/public/legacy'; + +const setupPlugins: Readonly<VisTypeXyPluginSetupDependencies> = { + expressions: npSetup.plugins.expressions, + visualizations: visualizationsSetup, + charts: npSetup.plugins.charts, +}; + +const startPlugins: Readonly<VisTypeXyPluginStartDependencies> = { + expressions: npStart.plugins.expressions, + visualizations: visualizationsStart, +}; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, setupPlugins); +export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts new file mode 100644 index 000000000000000..59bb64b337256ca --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts @@ -0,0 +1,82 @@ +/* + * 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 { + CoreSetup, + CoreStart, + Plugin, + IUiSettingsClient, + PluginInitializerContext, +} from 'kibana/public'; + +import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../../../plugins/charts/public'; + +export interface VisTypeXyDependencies { + uiSettings: IUiSettingsClient; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginSetupDependencies { + expressions: ReturnType<ExpressionsPublicPlugin['setup']>; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginStartDependencies { + expressions: ReturnType<ExpressionsPublicPlugin['start']>; + visualizations: VisualizationsStart; +} + +type VisTypeXyCoreSetup = CoreSetup<VisTypeXyPluginStartDependencies>; + +/** @internal */ +export class VisTypeXyPlugin implements Plugin<Promise<void>, void> { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: VisTypeXyCoreSetup, + { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies + ) { + // eslint-disable-next-line no-console + console.warn( + 'The visTypeXy plugin is enabled\n\n', + 'This may negatively alter existing vislib visualization configurations if saved.' + ); + const visualizationDependencies: Readonly<VisTypeXyDependencies> = { + uiSettings: core.uiSettings, + charts, + }; + + const visTypeDefinitions: any[] = []; + const visFunctions: any = []; + + visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); + visTypeDefinitions.forEach((vis: any) => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { + // nothing to do here + } +} From 186a82669f9c2f48080ccfac4efa515c3065bdb5 Mon Sep 17 00:00:00 2001 From: Nick Partridge <nick.ryan.partridge@gmail.com> Date: Mon, 3 Feb 2020 22:17:27 -0600 Subject: [PATCH 14/22] Kibana property config migrations (#55937) * Move defaultAppId config param into kibanaLegacy * Move disableWelcomeScreen config param into Home plugin * Update api and docs with silent option for renameFromRoot --- ...-plugin-server.configdeprecationfactory.md | 2 +- ...configdeprecationfactory.renamefromroot.md | 3 +- .../config/deprecation/deprecation_factory.ts | 20 ++++++--- src/core/server/config/deprecation/types.ts | 2 +- src/core/server/kibana_config.ts | 2 - src/core/server/mocks.ts | 2 +- .../server/plugins/plugin_context.test.ts | 2 +- src/core/server/plugins/types.ts | 2 +- src/core/server/server.api.md | 2 +- src/legacy/core_plugins/kibana/index.js | 2 - src/legacy/core_plugins/kibana/inject_vars.js | 2 - .../public/dashboard/np_ready/application.ts | 2 + .../dashboard/np_ready/dashboard_app.tsx | 2 - .../np_ready/dashboard_app_controller.tsx | 6 +-- .../public/dashboard/np_ready/legacy_app.js | 4 +- .../kibana/public/dashboard/plugin.ts | 1 + .../kibana/public/home/kibana_services.ts | 9 +++- .../public/home/np_ready/components/home.js | 2 +- .../home/np_ready/components/home.test.js | 1 + .../home/np_ready/components/home_app.js | 4 +- .../core_plugins/kibana/public/home/plugin.ts | 5 +++ .../core_plugins/kibana/public/kibana.js | 4 +- .../public/visualize/kibana_services.ts | 2 + .../public/visualize/np_ready/legacy_app.js | 2 +- .../kibana/public/visualize/plugin.ts | 1 + .../public/new_platform/__mocks__/helpers.ts | 3 ++ .../new_platform/new_platform.karma_mock.js | 12 +++++ src/plugins/home/config.ts | 26 +++++++++++ src/plugins/home/public/index.ts | 5 ++- src/plugins/home/public/plugin.test.ts | 11 +++-- src/plugins/home/public/plugin.ts | 12 +++-- src/plugins/home/server/index.ts | 13 +++++- src/plugins/home/server/plugin.ts | 2 +- src/plugins/kibana_legacy/config.ts | 26 +++++++++++ src/plugins/kibana_legacy/kibana.json | 2 +- src/plugins/kibana_legacy/public/index.ts | 5 +-- src/plugins/kibana_legacy/public/mocks.ts | 44 +++++++++++++++++++ src/plugins/kibana_legacy/public/plugin.ts | 9 +++- src/plugins/kibana_legacy/server/index.ts | 41 +++++++++++++++++ .../public/management_service.test.ts | 7 +-- test/common/config.js | 2 +- .../dashboard_mode/public/dashboard_viewer.js | 8 ++-- 42 files changed, 257 insertions(+), 57 deletions(-) create mode 100644 src/plugins/home/config.ts create mode 100644 src/plugins/kibana_legacy/config.ts create mode 100644 src/plugins/kibana_legacy/public/mocks.ts create mode 100644 src/plugins/kibana_legacy/server/index.ts diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md index c61907f3663012b..2ebee16874c8016 100644 --- a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md @@ -30,7 +30,7 @@ const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ | Method | Description | | --- | --- | | [rename(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.rename.md) | Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. | -| [renameFromRoot(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.<!-- -->This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | +| [renameFromRoot(oldKey, newKey, silent)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.<!-- -->This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | | [unused(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unused.md) | Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied. | | [unusedFromRoot(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) | Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied.<!-- -->This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead. | diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md index 269f242ec35da64..40ea891b17c9575 100644 --- a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md @@ -11,7 +11,7 @@ This should be only used when renaming properties from different configuration's <b>Signature:</b> ```typescript -renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; +renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; ``` ## Parameters @@ -20,6 +20,7 @@ renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; | --- | --- | --- | | oldKey | <code>string</code> | | | newKey | <code>string</code> | | +| silent | <code>boolean</code> | | <b>Returns:</b> diff --git a/src/core/server/config/deprecation/deprecation_factory.ts b/src/core/server/config/deprecation/deprecation_factory.ts index 6f7ed4c4e84cc6a..0b19a996243119d 100644 --- a/src/core/server/config/deprecation/deprecation_factory.ts +++ b/src/core/server/config/deprecation/deprecation_factory.ts @@ -26,7 +26,8 @@ const _rename = ( rootPath: string, log: ConfigDeprecationLogger, oldKey: string, - newKey: string + newKey: string, + silent?: boolean ) => { const fullOldPath = getPath(rootPath, oldKey); const oldValue = get(config, fullOldPath); @@ -40,11 +41,16 @@ const _rename = ( const newValue = get(config, fullNewPath); if (newValue === undefined) { set(config, fullNewPath, oldValue); - log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + + if (!silent) { + log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + } } else { - log( - `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` - ); + if (!silent) { + log( + `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` + ); + } } return config; }; @@ -67,11 +73,11 @@ const _unused = ( const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => _rename(config, rootPath, log, oldKey, newKey); -const renameFromRoot = (oldKey: string, newKey: string): ConfigDeprecation => ( +const renameFromRoot = (oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation => ( config, rootPath, log -) => _rename(config, '', log, oldKey, newKey); +) => _rename(config, '', log, oldKey, newKey, silent); const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => _unused(config, rootPath, log, unusedKey); diff --git a/src/core/server/config/deprecation/types.ts b/src/core/server/config/deprecation/types.ts index 19fba7800c919d7..dbfbad771f07494 100644 --- a/src/core/server/config/deprecation/types.ts +++ b/src/core/server/config/deprecation/types.ts @@ -102,7 +102,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; /** * Remove a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the unused key was found and deprecation applied. diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index d46960289a8d01e..17f77a6e9328f3d 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -25,9 +25,7 @@ export const config = { path: 'kibana', schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - defaultAppId: schema.string({ defaultValue: 'home' }), index: schema.string({ defaultValue: '.kibana' }), - disableWelcomeScreen: schema.boolean({ defaultValue: false }), autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), autocompleteTimeout: schema.duration({ defaultValue: 1000 }), }), diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 50ce507520d0487..7d6f09b5232c0c6 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -43,7 +43,7 @@ import { uuidServiceMock } from './uuid/uuid_service.mock'; export function pluginInitializerContextConfigMock<T>(config: T) { const globalConfig: SharedGlobalConfig = { - kibana: { defaultAppId: 'home-mocks', index: '.kibana-tests' }, + kibana: { index: '.kibana-tests' }, elasticsearch: { shardTimeout: duration('30s'), requestTimeout: duration('30s'), diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 3fcd7fbbbe1ff96..823299771544cd0 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -75,7 +75,7 @@ describe('Plugin Context', () => { .pipe(first()) .toPromise(); expect(configObject).toStrictEqual({ - kibana: { defaultAppId: 'home', index: '.kibana' }, + kibana: { index: '.kibana' }, elasticsearch: { shardTimeout: duration(30, 's'), requestTimeout: duration(30, 's'), diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a89e2f8c684e40c..9ae04787767bb62 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -214,7 +214,7 @@ export interface Plugin< export const SharedGlobalConfigKeys = { // We can add more if really needed - kibana: ['defaultAppId', 'index'] as const, + kibana: ['index'] as const, elasticsearch: ['shardTimeout', 'requestTimeout', 'pingTimeout', 'startupTimeout'] as const, path: ['data'] as const, }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e4ea06769007a1a..20d9692391a6915 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -509,7 +509,7 @@ export type ConfigDeprecation = (config: Record<string, any>, fromPath: string, // @public export interface ConfigDeprecationFactory { rename(oldKey: string, newKey: string): ConfigDeprecation; - renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; unused(unusedKey: string): ConfigDeprecation; unusedFromRoot(unusedKey: string): ConfigDeprecation; } diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e0497732e23054..8c35044b52c9e19 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -42,9 +42,7 @@ export default function(kibana) { config: function(Joi) { return Joi.object({ enabled: Joi.boolean().default(true), - defaultAppId: Joi.string().default('home'), index: Joi.string().default('.kibana'), - disableWelcomeScreen: Joi.boolean().default(false), autocompleteTerminateAfter: Joi.number() .integer() .min(1) diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index 4bf11f28732ee1a..01623341e4d38bc 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -28,8 +28,6 @@ export function injectVars(server) { ); return { - kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'), - disableWelcomeScreen: serverConfig.get('kibana.disableWelcomeScreen'), importAndExportableTypes, autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'), autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'), diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 0d461028d994a3e..f1fd93fd09b3dd5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -46,6 +46,7 @@ import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; +import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; export interface RenderDeps { core: LegacyCoreStart; @@ -62,6 +63,7 @@ export interface RenderDeps { embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; + config: KibanaLegacyStart['config']; } let angularModuleInstance: IModule | null = null; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index a48c165116304fd..0537e3f8fc456d4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -88,7 +88,6 @@ export interface DashboardAppScope extends ng.IScope { export function initDashboardAppDirective(app: any, deps: RenderDeps) { app.directive('dashboardApp', function($injector: IInjector) { const confirmModal = $injector.get<ConfirmModalFn>('confirmModal'); - const config = deps.uiSettings; return { restrict: 'E', @@ -106,7 +105,6 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $route, $scope, $routeParams, - config, confirmModal, indexPatterns: deps.npDataStart.indexPatterns, kbnUrlStateStorage, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index a8eec9c2504a7c0..624be02ac3b9dab 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -90,7 +90,6 @@ export interface DashboardAppControllerDependencies extends RenderDeps { $routeParams: any; indexPatterns: IndexPatternsContract; dashboardConfig: any; - config: any; confirmModal: ConfirmModalFn; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; @@ -109,7 +108,6 @@ export class DashboardAppController { dashboardConfig, localStorage, indexPatterns, - config, confirmModal, savedQueryService, embeddables, @@ -376,7 +374,7 @@ export class DashboardAppController { dashboardStateManager.getQuery() || { query: '', language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }, queryFilter.getFilters() ); @@ -493,7 +491,7 @@ export class DashboardAppController { { query: '', language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }, queryFilter.getGlobalFilters() ); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index abc0c789326f8a7..b0f70b7a0c68f38 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -246,10 +246,10 @@ export function initDashboardApp(app, deps) { }, }) .when(`dashboard/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }) .when(`dashboards/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }); }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index ca4b18a37504c67..227bcb53ca0df78 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -107,6 +107,7 @@ export class DashboardPlugin implements Plugin { chrome: contextCore.chrome, addBasePath: contextCore.http.basePath.prepend, uiSettings: contextCore.uiSettings, + config: kibana_legacy.config, savedQueryService: npDataStart.query.savedQueries, embeddables, dashboardCapabilities: contextCore.application.capabilities.dashboard, diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 4d9177735556df9..90fb32a88d43cb7 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -29,7 +29,12 @@ import { UiSettingsState, } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; -import { Environment, FeatureCatalogueEntry } from '../../../../../plugins/home/public'; +import { + Environment, + FeatureCatalogueEntry, + HomePublicPluginSetup, +} from '../../../../../plugins/home/public'; +import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; export interface HomeKibanaServices { indexPatternService: any; @@ -51,6 +56,8 @@ export interface HomeKibanaServices { chrome: ChromeStart; telemetryOptInProvider: any; uiSettings: IUiSettingsClient; + config: KibanaLegacySetup['config']; + homeConfig: HomePublicPluginSetup['config']; http: HttpStart; savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js index 5c32a463da11533..0c09c6c3c74fcba 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js @@ -48,7 +48,7 @@ export class Home extends Component { super(props); const isWelcomeEnabled = !( - getServices().getInjected('disableWelcomeScreen') || + getServices().homeConfig.disableWelcomeScreen || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); const currentOptInStatus = this.props.getOptInStatus(); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index 27d4f1a8b1c1fa5..d25a1f81dae5a57 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -30,6 +30,7 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ getBasePath: () => 'path', getInjected: () => '', + homeConfig: { disableWelcomeScreen: false }, }), })); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js index e49f00b949da5be..f6c91b412381cd9 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js @@ -30,7 +30,7 @@ import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../../kibana_services'; export function HomeApp({ directories }) { const { - getInjected, + config, savedObjectsClient, getBasePath, addBasePath, @@ -41,7 +41,7 @@ export function HomeApp({ directories }) { const mlEnabled = environment.ml; const apmUiEnabled = environment.apmUi; - const defaultAppId = getInjected('kbnDefaultAppId', 'discover'); + const defaultAppId = config.defaultAppId || 'discover'; const renderTutorialDirectory = props => { return ( diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 502c8f45646cf0f..aec3835dc075d93 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -27,6 +27,7 @@ import { Environment, FeatureCatalogueEntry, HomePublicPluginStart, + HomePublicPluginSetup, } from '../../../../../plugins/home/public'; export interface LegacyAngularInjectedDependencies { @@ -59,6 +60,7 @@ export interface HomePluginSetupDependencies { }; usageCollection: UsageCollectionSetup; kibana_legacy: KibanaLegacySetup; + home: HomePublicPluginSetup; } export class HomePlugin implements Plugin { @@ -69,6 +71,7 @@ export class HomePlugin implements Plugin { setup( core: CoreSetup, { + home, kibana_legacy, usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, @@ -95,6 +98,8 @@ export class HomePlugin implements Plugin { getBasePath: core.http.basePath.get, indexPatternService: this.dataStart!.indexPatterns, environment: this.environment!, + config: kibana_legacy.config, + homeConfig: home.config, ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 50f1702a2a6d0e1..f2868da947a75d3 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -20,7 +20,6 @@ // autoloading // preloading (for faster webpack builds) -import chrome from 'ui/chrome'; import routes from 'ui/routes'; import { uiModules } from 'ui/modules'; import { npSetup } from 'ui/new_platform'; @@ -64,8 +63,9 @@ localApplicationService.attachToAngular(routes); routes.enable(); +const { config } = npSetup.plugins.kibana_legacy; routes.otherwise({ - redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}`, + redirectTo: `/${config.defaultAppId || 'discover'}`, }); uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index f7fd19e8288e739..15e9c73a39effe1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -34,6 +34,7 @@ import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Chrome } from './legacy_imports'; +import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; export interface VisualizeKibanaServices { addBasePath: (url: string) => string; @@ -52,6 +53,7 @@ export interface VisualizeKibanaServices { savedVisualizations: SavedVisualizations; share: SharePluginStart; uiSettings: IUiSettingsClient; + config: KibanaLegacyStart['config']; visualizeCapabilities: any; visualizations: VisualizationsStart; usageCollection?: UsageCollectionSetup; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index d99771ccc912dd4..24055b9a2d9ed98 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -173,7 +173,7 @@ export function initVisualizeApp(app, deps) { }, }) .when(`visualize/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }); }); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 26c6691a3613f17..8e7487fee55f69a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -108,6 +108,7 @@ export class VisualizePlugin implements Plugin { share, toastNotifications: contextCore.notifications.toasts, uiSettings: contextCore.uiSettings, + config: kibana_legacy.config, visualizeCapabilities: contextCore.application.capabilities.visualize, visualizations, usageCollection, diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts index c89ae9f8b3c9b17..439ac9b5713dfa6 100644 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts @@ -27,6 +27,7 @@ import { inspectorPluginMock } from '../../../../../plugins/inspector/public/moc import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; import { managementPluginMock } from '../../../../../plugins/management/public/mocks'; import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; +import { kibanaLegacyPluginMock } from '../../../../../plugins/kibana_legacy/public/mocks'; import { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; /* eslint-enable @kbn/eslint/no-restricted-paths */ @@ -40,6 +41,7 @@ export const pluginsMock = { expressions: expressionsPluginMock.createSetupContract(), uiActions: uiActionsPluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), + kibana_legacy: kibanaLegacyPluginMock.createSetupContract(), }), createStart: () => ({ data: dataPluginMock.createStartContract(), @@ -50,6 +52,7 @@ export const pluginsMock = { expressions: expressionsPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), management: managementPluginMock.createStartContract(), + kibana_legacy: kibanaLegacyPluginMock.createStartContract(), }), }; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f98b8801d526632..c2c8b5a0fae7a77 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -119,6 +119,9 @@ export const npSetup = { kibana_legacy: { registerLegacyApp: () => {}, forwardApp: () => {}, + config: { + defaultAppId: 'home', + }, }, inspector: { registerView: () => undefined, @@ -140,6 +143,9 @@ export const npSetup = { environment: { update: sinon.fake(), }, + config: { + disableWelcomeScreen: false, + }, }, charts: { theme: { @@ -196,6 +202,9 @@ export const npStart = { kibana_legacy: { getApps: () => [], getForwards: () => [], + config: { + defaultAppId: 'home', + }, }, data: { autocomplete: { @@ -297,6 +306,9 @@ export const npStart = { environment: { get: sinon.fake(), }, + config: { + disableWelcomeScreen: false, + }, }, navigation: { ui: { diff --git a/src/plugins/home/config.ts b/src/plugins/home/config.ts new file mode 100644 index 000000000000000..149723a7ee5ae2d --- /dev/null +++ b/src/plugins/home/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + disableWelcomeScreen: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf<typeof configSchema>; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index ca05c8b5f760e7c..114d442b40943b6 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import { PluginInitializerContext } from 'kibana/public'; + export { FeatureCatalogueSetup, FeatureCatalogueStart, @@ -26,4 +28,5 @@ export { export { FeatureCatalogueEntry, FeatureCatalogueCategory, Environment } from './services'; import { HomePublicPlugin } from './plugin'; -export const plugin = () => new HomePublicPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => + new HomePublicPlugin(initializerContext); diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 34502d7d2c6cd86..fa44a110c63b70d 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -19,6 +19,9 @@ import { registryMock, environmentMock } from './plugin.test.mocks'; import { HomePublicPlugin } from './plugin'; +import { coreMock } from '../../../core/public/mocks'; + +const mockInitializerContext = coreMock.createPluginInitializerContext(); describe('HomePublicPlugin', () => { beforeEach(() => { @@ -30,13 +33,13 @@ describe('HomePublicPlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new HomePublicPlugin().setup(); + const setup = await new HomePublicPlugin(mockInitializerContext).setup(); expect(setup).toHaveProperty('featureCatalogue'); expect(setup.featureCatalogue).toHaveProperty('register'); }); test('wires up and returns environment service', async () => { - const setup = await new HomePublicPlugin().setup(); + const setup = await new HomePublicPlugin(mockInitializerContext).setup(); expect(setup).toHaveProperty('environment'); expect(setup.environment).toHaveProperty('update'); }); @@ -44,7 +47,7 @@ describe('HomePublicPlugin', () => { describe('start', () => { test('wires up and returns registry', async () => { - const service = new HomePublicPlugin(); + const service = new HomePublicPlugin(mockInitializerContext); await service.setup(); const core = { application: { capabilities: { catalogue: {} } } } as any; const start = await service.start(core); @@ -55,7 +58,7 @@ describe('HomePublicPlugin', () => { }); test('wires up and returns environment service', async () => { - const service = new HomePublicPlugin(); + const service = new HomePublicPlugin(mockInitializerContext); await service.setup(); const start = await service.start({ application: { capabilities: { catalogue: {} } }, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 39a7f23826900bd..fe68dbc3e7e490c 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -17,7 +17,8 @@ * under the License. */ -import { CoreStart, Plugin } from 'src/core/public'; +import { CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; + import { EnvironmentService, EnvironmentServiceSetup, @@ -26,19 +27,23 @@ import { FeatureCatalogueRegistrySetup, FeatureCatalogueRegistryStart, } from './services'; +import { ConfigSchema } from '../config'; export class HomePublicPlugin implements Plugin<HomePublicPluginSetup, HomePublicPluginStart> { private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); private readonly environmentService = new EnvironmentService(); - public async setup() { + constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {} + + public setup(): HomePublicPluginSetup { return { featureCatalogue: { ...this.featuresCatalogueRegistry.setup() }, environment: { ...this.environmentService.setup() }, + config: this.initializerContext.config.get(), }; } - public async start(core: CoreStart) { + public start(core: CoreStart): HomePublicPluginStart { return { featureCatalogue: { ...this.featuresCatalogueRegistry.start({ @@ -71,6 +76,7 @@ export interface HomePublicPluginSetup { * @deprecated */ environment: EnvironmentSetup; + config: ConfigSchema; } /** @public */ diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 0961c729698b9db..02f4c91a414cc95 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -20,8 +20,19 @@ export { HomeServerPluginSetup, HomeServerPluginStart } from './plugin'; export { TutorialProvider } from './services'; export { SampleDatasetProvider, SampleDataRegistrySetup } from './services'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { HomeServerPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor<ConfigSchema> = { + exposeToBrowser: { + disableWelcomeScreen: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('kibana.disableWelcomeScreen', 'home.disableWelcomeScreen'), + ], +}; export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext); diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 23c236764cddcfb..d2f2d7041024e34 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { TutorialsRegistry, TutorialsRegistrySetup, diff --git a/src/plugins/kibana_legacy/config.ts b/src/plugins/kibana_legacy/config.ts new file mode 100644 index 000000000000000..291f8813ecfb91a --- /dev/null +++ b/src/plugins/kibana_legacy/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + defaultAppId: schema.string({ defaultValue: 'home' }), +}); + +export type ConfigSchema = TypeOf<typeof configSchema>; diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index 26ee6db3ba06aec..b6d11309a4f9646 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -1,6 +1,6 @@ { "id": "kibana_legacy", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 4cb30be8917acd3..de8788808e74c6a 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -20,8 +20,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { KibanaLegacyPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new KibanaLegacyPlugin(); -} +export const plugin = (initializerContext: PluginInitializerContext) => + new KibanaLegacyPlugin(initializerContext); export * from './plugin'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts new file mode 100644 index 000000000000000..b6287dd9d9a5560 --- /dev/null +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -0,0 +1,44 @@ +/* + * 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 { KibanaLegacyPlugin } from './plugin'; + +export type Setup = jest.Mocked<ReturnType<KibanaLegacyPlugin['setup']>>; +export type Start = jest.Mocked<ReturnType<KibanaLegacyPlugin['start']>>; + +const createSetupContract = (): Setup => ({ + forwardApp: jest.fn(), + registerLegacyApp: jest.fn(), + config: { + defaultAppId: 'home', + }, +}); + +const createStartContract = (): Start => ({ + getApps: jest.fn(), + getForwards: jest.fn(), + config: { + defaultAppId: 'home', + }, +}); + +export const kibanaLegacyPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index cb95088320d7b33..b9a61a1c9b20005 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -17,7 +17,9 @@ * under the License. */ -import { App } from 'kibana/public'; +import { App, PluginInitializerContext } from 'kibana/public'; + +import { ConfigSchema } from '../config'; interface ForwardDefinition { legacyAppId: string; @@ -29,6 +31,8 @@ export class KibanaLegacyPlugin { private apps: App[] = []; private forwards: ForwardDefinition[] = []; + constructor(private readonly initializerContext: PluginInitializerContext<ConfigSchema>) {} + public setup() { return { /** @@ -77,6 +81,8 @@ export class KibanaLegacyPlugin { ) => { this.forwards.push({ legacyAppId, newAppId, ...options }); }, + + config: this.initializerContext.config.get(), }; } @@ -92,6 +98,7 @@ export class KibanaLegacyPlugin { * Just exported for wiring up with legacy platform, should not be used. */ getForwards: () => this.forwards, + config: this.initializerContext.config.get(), }; } } diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts new file mode 100644 index 000000000000000..4d0fe8364a66c58 --- /dev/null +++ b/src/plugins/kibana_legacy/server/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor<ConfigSchema> = { + exposeToBrowser: { + defaultAppId: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + // TODO: Remove deprecation once defaultAppId is deleted + renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), + ], +}; + +class Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) {} +} + +export const plugin = () => new Plugin(); diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts index 854406a10335bef..b34e76474cec23b 100644 --- a/src/plugins/management/public/management_service.test.ts +++ b/src/plugins/management/public/management_service.test.ts @@ -19,12 +19,13 @@ import { ManagementService } from './management_service'; import { coreMock } from '../../../core/public/mocks'; +import { npSetup } from '../../../legacy/ui/public/new_platform/__mocks__'; -const mockKibanaLegacy = { registerLegacyApp: () => {}, forwardApp: () => {} }; +jest.mock('ui/new_platform'); test('Provides default sections', () => { const service = new ManagementService().setup( - mockKibanaLegacy, + npSetup.plugins.kibana_legacy, () => {}, coreMock.createSetup().getStartServices ); @@ -36,7 +37,7 @@ test('Provides default sections', () => { test('Register section, enable and disable', () => { const service = new ManagementService().setup( - mockKibanaLegacy, + npSetup.plugins.kibana_legacy, () => {}, coreMock.createSetup().getStartServices ); diff --git a/test/common/config.js b/test/common/config.js index 29d4bbf10a6cee7..faf8cef02717092 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -54,7 +54,7 @@ export default function() { `--elasticsearch.hosts=${formatUrl(servers.elasticsearch)}`, `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, - `--kibana.disableWelcomeScreen=true`, + `--home.disableWelcomeScreen=true`, '--telemetry.banner=false', `--server.maxPayloadBytes=1679958`, // newsfeed mock service diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index ebae49f99472397..4215f96c8de4a48 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -55,10 +55,12 @@ chrome.setRootController('kibana', function() { uiModules.get('kibana').run(showAppRedirectNotification); -// If there is a configured kbnDefaultAppId, and it is a dashboard ID, we'll -// show that dashboard, otherwise, we'll show the default dasbhoard landing page. +/** + * If there is a configured `kibana.defaultAppId`, and it is a dashboard ID, we'll + * show that dashboard, otherwise, we'll show the default dasbhoard landing page. + */ function defaultUrl() { - const defaultAppId = chrome.getInjected('kbnDefaultAppId', ''); + const defaultAppId = npStart.plugins.kibana_legacy.config.defaultAppId || ''; const isDashboardId = defaultAppId.startsWith(dashboardAppIdPrefix()); return isDashboardId ? `/${defaultAppId}` : DashboardConstants.LANDING_PAGE_PATH; } From 0440ae50f7ad0aec051710efde2d26d158a75ecc Mon Sep 17 00:00:00 2001 From: Tyler Smalley <tyler.smalley@elastic.co> Date: Mon, 3 Feb 2020 20:54:59 -0800 Subject: [PATCH 15/22] Updates Monitoring alert Jest snapshots A UI bump caused changes the EuiSuperSelect component which were not reflected in kibana#54306. The EUI change went in after the PR went green, but then failed once it hit master. Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co> --- .../configuration/__snapshots__/configuration.test.tsx.snap | 1 + .../alerts/configuration/__snapshots__/step1.test.tsx.snap | 3 +++ 2 files changed, 4 insertions(+) diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap index f044e001700c50c..429d19fbb887ecf 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap @@ -7,6 +7,7 @@ exports[`Configuration shallow view should render step 1 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap index fa03769ea3d0902..94d951a94fe29a1 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -42,6 +42,7 @@ exports[`Step1 should render normally 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ @@ -135,6 +136,7 @@ exports[`Step1 testing should show a failed test error 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ @@ -220,6 +222,7 @@ exports[`Step1 testing should show a successful test 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ From 4bb56c80b7175a40eed275ddbe2245a95eebb393 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet <pierre.gayvallet@elastic.co> Date: Tue, 4 Feb 2020 08:03:36 +0100 Subject: [PATCH 16/22] Add `getServerInfo` API to http setup contract (#56636) * add getServerInfo http setup api * update generated doc --- ...ibana-plugin-server.httpserverinfo.host.md | 13 +++++++ .../kibana-plugin-server.httpserverinfo.md | 22 ++++++++++++ ...ibana-plugin-server.httpserverinfo.name.md | 13 +++++++ ...ibana-plugin-server.httpserverinfo.port.md | 13 +++++++ ...a-plugin-server.httpserverinfo.protocol.md | 13 +++++++ ...n-server.httpservicesetup.getserverinfo.md | 13 +++++++ .../kibana-plugin-server.httpservicesetup.md | 1 + .../core/server/kibana-plugin-server.md | 1 + src/core/server/http/http_server.test.ts | 34 +++++++++++++++++++ src/core/server/http/http_server.ts | 9 ++++- src/core/server/http/http_service.mock.ts | 7 ++++ src/core/server/http/types.ts | 17 ++++++++++ src/core/server/index.ts | 1 + src/core/server/legacy/legacy_service.ts | 1 + src/core/server/mocks.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 9 +++++ 17 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserverinfo.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md create mode 100644 docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md new file mode 100644 index 000000000000000..ee7e1e5b7c9c945 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [host](./kibana-plugin-server.httpserverinfo.host.md) + +## HttpServerInfo.host property + +The hostname of the server + +<b>Signature:</b> + +```typescript +host: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.md new file mode 100644 index 000000000000000..6dbdb11ddb66ef2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.md @@ -0,0 +1,22 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) + +## HttpServerInfo interface + + +<b>Signature:</b> + +```typescript +export interface HttpServerInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [host](./kibana-plugin-server.httpserverinfo.host.md) | <code>string</code> | The hostname of the server | +| [name](./kibana-plugin-server.httpserverinfo.name.md) | <code>string</code> | The name of the Kibana server | +| [port](./kibana-plugin-server.httpserverinfo.port.md) | <code>number</code> | The port the server is listening on | +| [protocol](./kibana-plugin-server.httpserverinfo.protocol.md) | <code>'http' | 'https' | 'socket'</code> | The protocol used by the server | + diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md new file mode 100644 index 000000000000000..8d3a45c90a34277 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [name](./kibana-plugin-server.httpserverinfo.name.md) + +## HttpServerInfo.name property + +The name of the Kibana server + +<b>Signature:</b> + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md new file mode 100644 index 000000000000000..5dd5a53830c441d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [port](./kibana-plugin-server.httpserverinfo.port.md) + +## HttpServerInfo.port property + +The port the server is listening on + +<b>Signature:</b> + +```typescript +port: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md new file mode 100644 index 000000000000000..08afb5c3f721333 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [protocol](./kibana-plugin-server.httpserverinfo.protocol.md) + +## HttpServerInfo.protocol property + +The protocol used by the server + +<b>Signature:</b> + +```typescript +protocol: 'http' | 'https' | 'socket'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md new file mode 100644 index 000000000000000..4501a7e26f75fa1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md @@ -0,0 +1,13 @@ +<!-- Do not edit this file. It is automatically generated by API Documenter. --> + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [getServerInfo](./kibana-plugin-server.httpservicesetup.getserverinfo.md) + +## HttpServiceSetup.getServerInfo property + +Provides common [information](./kibana-plugin-server.httpserverinfo.md) about the running http server. + +<b>Signature:</b> + +```typescript +getServerInfo: () => HttpServerInfo; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 2a4b0e09977c1c8..c2d53ec1eaf52ef 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -86,6 +86,7 @@ async (context, request, response) => { | [createCookieSessionStorageFactory](./kibana-plugin-server.httpservicesetup.createcookiesessionstoragefactory.md) | <code><T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>></code> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | | [createRouter](./kibana-plugin-server.httpservicesetup.createrouter.md) | <code>() => IRouter</code> | Provides ability to declare a handler function for a particular path and HTTP request method. | | [csp](./kibana-plugin-server.httpservicesetup.csp.md) | <code>ICspConfig</code> | The CSP config used for Kibana. | +| [getServerInfo](./kibana-plugin-server.httpservicesetup.getserverinfo.md) | <code>() => HttpServerInfo</code> | Provides common [information](./kibana-plugin-server.httpserverinfo.md) about the running http server. | | [isTlsEnabled](./kibana-plugin-server.httpservicesetup.istlsenabled.md) | <code>boolean</code> | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | <code>(handler: AuthenticationHandler) => void</code> | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | <code>(handler: OnPostAuthHandler) => void</code> | To define custom logic to perform for incoming requests. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index e7b13346525406b..a3abeff44c25cb5 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -64,6 +64,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- --> | [ErrorHttpResponseOptions](./kibana-plugin-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | | [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) | HTTP response parameters | +| [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to <code>hapi</code> server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index f8ef49b0f6d18be..a9fc80c86d878ea 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -62,6 +62,7 @@ beforeAll(() => { beforeEach(() => { config = { + name: 'kibana', host: '127.0.0.1', maxPayload: new ByteSizeValue(1024), port: 10002, @@ -1077,4 +1078,37 @@ describe('setup contract', () => { expect(isTlsEnabled).toBe(false); }); }); + + describe('#getServerInfo', () => { + it('returns correct information', async () => { + let { getServerInfo } = await server.setup(config); + + expect(getServerInfo()).toEqual({ + host: '127.0.0.1', + name: 'kibana', + port: 10002, + protocol: 'http', + }); + + ({ getServerInfo } = await server.setup({ + ...config, + port: 12345, + name: 'custom-name', + host: 'localhost', + })); + + expect(getServerInfo()).toEqual({ + host: 'localhost', + name: 'custom-name', + port: 12345, + protocol: 'http', + }); + }); + + it('returns correct protocol when ssl is enabled', async () => { + const { getServerInfo } = await server.setup(configWithSSL); + + expect(getServerInfo().protocol).toEqual('https'); + }); + }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index fdc272041ce3549..025ab2bf56ac2dc 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -35,7 +35,7 @@ import { import { IsAuthenticated, AuthStateStorage, GetAuthState } from './auth_state_storage'; import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; -import { HttpServiceSetup } from './types'; +import { HttpServiceSetup, HttpServerInfo } from './types'; /** @internal */ export interface HttpServerSetup { @@ -58,6 +58,7 @@ export interface HttpServerSetup { get: GetAuthState; isAuthenticated: IsAuthenticated; }; + getServerInfo: () => HttpServerInfo; } /** @internal */ @@ -122,6 +123,12 @@ export class HttpServer { isAuthenticated: this.authState.isAuthenticated, }, getAuthHeaders: this.authRequestHeaders.get, + getServerInfo: () => ({ + name: config.name, + host: config.host, + port: config.port, + protocol: this.server!.info.protocol, + }), isTlsEnabled: config.ssl.enabled, // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 2b2d98d937e8595..30032ff5da79688 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -77,12 +77,19 @@ const createSetupContractMock = () => { auth: createAuthMock(), getAuthHeaders: jest.fn(), isTlsEnabled: false, + getServerInfo: jest.fn(), }; setupContract.createCookieSessionStorageFactory.mockResolvedValue( sessionStorageMock.createFactory() ); setupContract.createRouter.mockImplementation(() => mockRouter.create()); setupContract.getAuthHeaders.mockReturnValue({ authorization: 'authorization-header' }); + setupContract.getServerInfo.mockReturnValue({ + host: 'localhost', + name: 'kibana', + port: 80, + protocol: 'http', + }); return setupContract; }; diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 01b852c26ec934e..632784410805564 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -252,6 +252,11 @@ export interface HttpServiceSetup { contextName: T, provider: RequestHandlerContextProvider<T> ) => RequestHandlerContextContainer; + + /** + * Provides common {@link HttpServerInfo | information} about the running http server. + */ + getServerInfo: () => HttpServerInfo; } /** @internal */ @@ -273,3 +278,15 @@ export interface HttpServiceStart { /** Indicates if http server is listening on a given port */ isListening: (port: number) => boolean; } + +/** @public */ +export interface HttpServerInfo { + /** The name of the Kibana server */ + name: string; + /** The hostname of the server */ + host: string; + /** The port the server is listening on */ + port: number; + /** The protocol used by the server */ + protocol: 'http' | 'https' | 'socket'; +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 91f38c9f2ddbe9d..c45acd7f0129a17 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -103,6 +103,7 @@ export { GetAuthState, HttpResponseOptions, HttpResponsePayload, + HttpServerInfo, HttpServiceSetup, HttpServiceStart, ErrorHttpResponseOptions, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index d0e0453564f9432..f9b18afadc9382e 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -292,6 +292,7 @@ export class LegacyService implements CoreService { }, csp: setupDeps.core.http.csp, isTlsEnabled: setupDeps.core.http.isTlsEnabled, + getServerInfo: setupDeps.core.http.getServerInfo, }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 7d6f09b5232c0c6..97f836f8ef37d7e 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -105,6 +105,7 @@ function createCoreSetupMock() { get: httpService.auth.get, isAuthenticated: httpService.auth.isAuthenticated, }, + getServerInfo: httpService.getServerInfo, }; httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 30e5209b2fc6a13..77300900e84f332 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -164,6 +164,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>( auth: { get: deps.http.auth.get, isAuthenticated: deps.http.auth.isAuthenticated }, csp: deps.http.csp, isTlsEnabled: deps.http.isTlsEnabled, + getServerInfo: deps.http.getServerInfo, }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 20d9692391a6915..fb27fcccc2abe17 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -736,6 +736,14 @@ export interface HttpResponseOptions { // @public export type HttpResponsePayload = undefined | string | Record<string, any> | Buffer | Stream; +// @public (undocumented) +export interface HttpServerInfo { + host: string; + name: string; + port: number; + protocol: 'http' | 'https' | 'socket'; +} + // @public export interface HttpServiceSetup { // (undocumented) @@ -747,6 +755,7 @@ export interface HttpServiceSetup { createCookieSessionStorageFactory: <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>>; createRouter: () => IRouter; csp: ICspConfig; + getServerInfo: () => HttpServerInfo; isTlsEnabled: boolean; registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; From 79ecc408df62a796f6d1ceecb912e30d430c8d53 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin <aleh.zasypkin@gmail.com> Date: Tue, 4 Feb 2020 09:18:52 +0100 Subject: [PATCH 17/22] Mention changed SAML ACS endpoint URL in breaking changes doc. (#56613) --- docs/migration/migrate_8_0.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 146d4e97b6cf4a6..a34f956ace263d9 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -74,6 +74,12 @@ specified explicitly. *Impact:* Define `xpack.security.authc.saml.realm` when using the SAML authentication provider instead. +[float] +==== `/api/security/v1/saml` endpoint is no longer supported +*Details:* The deprecated `/api/security/v1/saml` endpoint is no longer supported. + +*Impact:* Rely on `/api/security/saml/callback` endpoint when using SAML instead. This change should be reflected in Kibana `server.xsrf.whitelist` config as well as in Elasticsearch and Identity Provider SAML settings. + [float] === `optimize` directory is now in the `data` folder *Details:* Generated bundles have moved to the configured `path.data` folder. From 05edbdac78fae883fada5a5744fdf0f2d5402f41 Mon Sep 17 00:00:00 2001 From: patrykkopycinski <patryk.kopycinski@elastic.co> Date: Tue, 4 Feb 2020 11:22:40 +0100 Subject: [PATCH 18/22] [SIEM] Add eslint-plugin-react-perf (#55960) Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- .eslintrc.js | 14 ++++++++++++-- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 97a35d8b50e5650..199f3743fd621e1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -643,6 +643,18 @@ module.exports = { // '@typescript-eslint/unbound-method': 'error', }, }, + // { + // // will introduced after the other warns are fixed + // // typescript and javascript for front end react performance + // files: ['x-pack/legacy/plugins/siem/public/**/!(*.test).{js,ts,tsx}'], + // plugins: ['react-perf'], + // rules: { + // // 'react-perf/jsx-no-new-object-as-prop': 'error', + // // 'react-perf/jsx-no-new-array-as-prop': 'error', + // // 'react-perf/jsx-no-new-function-as-prop': 'error', + // // 'react/jsx-no-bind': 'error', + // }, + // }, { // typescript and javascript for front and back end files: ['x-pack/legacy/plugins/siem/**/*.{js,ts,tsx}'], @@ -747,8 +759,6 @@ module.exports = { // will introduced after the other warns are fixed // 'react/sort-comp': 'error', 'react/void-dom-elements-no-children': 'error', - // will introduced after the other warns are fixed - // 'react/jsx-no-bind': 'error', 'react/jsx-no-comment-textnodes': 'error', 'react/jsx-no-literals': 'error', 'react/jsx-no-target-blank': 'error', diff --git a/package.json b/package.json index ff6d32bfc39e515..f9a3bfd99b10989 100644 --- a/package.json +++ b/package.json @@ -414,6 +414,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.17.0", "eslint-plugin-react-hooks": "^2.3.0", + "eslint-plugin-react-perf": "^3.2.3", "exit-hook": "^2.2.0", "faker": "1.1.0", "fetch-mock": "^7.3.9", diff --git a/yarn.lock b/yarn.lock index 4b56ec6460775b1..caa5587a0bbd03e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12591,6 +12591,11 @@ eslint-plugin-react-hooks@^2.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a" integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw== +eslint-plugin-react-perf@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-perf/-/eslint-plugin-react-perf-3.2.3.tgz#e28d42d3a1f7ec3c8976a94735d8e17e7d652a45" + integrity sha512-bMiPt7uywwS1Ly25n752NE3Ei0XBZ3igplTkZ8GPJKyZVVUd3cHgzILGeQW2HIeAkzQ9zwk9HM6EcYDipdFk3Q== + eslint-plugin-react@^7.17.0: version "7.17.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz#a31b3e134b76046abe3cd278e7482bd35a1d12d7" From a36ec324a69288c1f4df9204a5c07b0f86be11b9 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov <restrry@gmail.com> Date: Tue, 4 Feb 2020 12:38:15 +0100 Subject: [PATCH 19/22] Start consuming np logging config (#56480) * pass config to the new platform * add default appenders * remove receiveAllAppenders flag it breaks legacy-appender compatibility with legacy flags: silent, quiet, verbose * add integration tests * use console mocks to simplify test setup * update tests * improve names * validate that default appender always presents on root level required during migration period to make sure logs are sent to the LP logging system * do not check condition in the loop * fix integration tests --- ...gacy_object_to_config_adapter.test.ts.snap | 2 + .../config/legacy_object_to_config_adapter.ts | 15 +- .../legacy/integration_tests/logging.test.ts | 239 ++++++++++++++++++ .../__snapshots__/logging_config.test.ts.snap | 2 + .../server/logging/appenders/appenders.ts | 6 - .../logging/integration_tests/logging.test.ts | 114 +++++++++ .../server/logging/integration_tests/utils.ts | 68 +++++ src/core/server/logging/logger.test.ts | 82 ------ src/core/server/logging/logger.ts | 8 +- .../server/logging/logging_config.test.ts | 22 +- src/core/server/logging/logging_config.ts | 39 ++- src/legacy/server/config/schema.js | 4 + 12 files changed, 495 insertions(+), 106 deletions(-) create mode 100644 src/core/server/legacy/integration_tests/logging.test.ts create mode 100644 src/core/server/logging/integration_tests/logging.test.ts create mode 100644 src/core/server/logging/integration_tests/utils.ts diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 74ecaa9f09c0e10..3b16bed92df9764 100644 --- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -72,6 +72,7 @@ Object { }, }, }, + "loggers": undefined, "root": Object { "level": "off", }, @@ -90,6 +91,7 @@ Object { }, }, }, + "loggers": undefined, "root": Object { "level": "all", }, diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index 30bb150e6c15a85..3e496648c3af903 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -19,12 +19,13 @@ import { ConfigPath } from '../../config'; import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter'; +import { LoggingConfigType } from '../../logging/logging_config'; import { LegacyVars } from '../types'; /** * Represents logging config supported by the legacy platform. */ -interface LegacyLoggingConfig { +export interface LegacyLoggingConfig { silent?: boolean; verbose?: boolean; quiet?: boolean; @@ -33,18 +34,24 @@ interface LegacyLoggingConfig { events?: Record<string, string>; } +type MixedLoggingConfig = LegacyLoggingConfig & Partial<LoggingConfigType>; + /** * Represents adapter between config provided by legacy platform and `Config` * supported by the current platform. * @internal */ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { - private static transformLogging(configValue: LegacyLoggingConfig = {}) { + private static transformLogging(configValue: MixedLoggingConfig = {}) { + const { appenders, root, loggers, ...legacyLoggingConfig } = configValue; + const loggingConfig = { appenders: { - default: { kind: 'legacy-appender', legacyLoggingConfig: configValue }, + ...appenders, + default: { kind: 'legacy-appender', legacyLoggingConfig }, }, - root: { level: 'info' }, + root: { level: 'info', ...root }, + loggers, }; if (configValue.silent) { diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts new file mode 100644 index 000000000000000..66234f677903f80 --- /dev/null +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -0,0 +1,239 @@ +/* + * 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 * as kbnTestServer from '../../../../test_utils/kbn_server'; + +import { + getPlatformLogsFromMock, + getLegacyPlatformLogsFromMock, +} from '../../logging/integration_tests/utils'; + +import { LegacyLoggingConfig } from '../config/legacy_object_to_config_adapter'; + +function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) { + return kbnTestServer.createRoot({ + migrations: { skip: true }, // otherwise stuck in polling ES + logging: { + // legacy platform config + silent: false, + json: false, + ...legacyLoggingConfig, + events: { + log: ['test-file-legacy'], + }, + // platform config + appenders: { + 'test-console': { + kind: 'console', + layout: { + highlight: false, + kind: 'pattern', + }, + }, + }, + loggers: [ + { + context: 'test-file', + appenders: ['test-console'], + level: 'info', + }, + ], + }, + }); +} + +describe('logging service', () => { + let mockConsoleLog: jest.SpyInstance; + let mockStdout: jest.SpyInstance; + + beforeAll(async () => { + mockConsoleLog = jest.spyOn(global.console, 'log'); + mockStdout = jest.spyOn(global.process.stdout, 'write'); + }); + + afterAll(async () => { + mockConsoleLog.mockRestore(); + mockStdout.mockRestore(); + }); + + describe('compatibility', () => { + describe('uses configured loggers', () => { + let root: ReturnType<typeof createRoot>; + beforeAll(async () => { + root = createRoot(); + + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => { + await root.shutdown(); + }); + + beforeEach(() => { + mockConsoleLog.mockClear(); + mockStdout.mockClear(); + }); + + it('when context matches', async () => { + root.logger.get('test-file').info('handled by NP'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + const loggedString = getPlatformLogsFromMock(mockConsoleLog); + expect(loggedString).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] handled by NP", + ] + `); + }); + + it('falls back to the root legacy logger otherwise', async () => { + root.logger.get('test-file-legacy').info('handled by LP'); + + expect(mockStdout).toHaveBeenCalledTimes(1); + + const loggedString = getLegacyPlatformLogsFromMock(mockStdout); + expect(loggedString).toMatchInlineSnapshot(` + Array [ + " log [xx:xx:xx.xxx] [info][test-file-legacy] handled by LP + ", + ] + `); + }); + }); + + describe('logging config respects legacy logging settings', () => { + let root: ReturnType<typeof createRoot>; + + afterEach(async () => { + mockConsoleLog.mockClear(); + mockStdout.mockClear(); + await root.shutdown(); + }); + + it('"silent": true', async () => { + root = createRoot({ silent: true }); + + await root.setup(); + await root.start(); + + const platformLogger = root.logger.get('test-file'); + platformLogger.info('info'); + platformLogger.warn('warn'); + platformLogger.error('error'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + + expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + ] + `); + + mockStdout.mockClear(); + + const legacyPlatformLogger = root.logger.get('test-file-legacy'); + legacyPlatformLogger.info('info'); + legacyPlatformLogger.warn('warn'); + legacyPlatformLogger.error('error'); + + expect(mockStdout).toHaveBeenCalledTimes(0); + }); + + it('"quiet": true', async () => { + root = createRoot({ quiet: true }); + + await root.setup(); + await root.start(); + + const platformLogger = root.logger.get('test-file'); + platformLogger.info('info'); + platformLogger.warn('warn'); + platformLogger.error('error'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + + expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + ] + `); + + mockStdout.mockClear(); + + const legacyPlatformLogger = root.logger.get('test-file-legacy'); + legacyPlatformLogger.info('info'); + legacyPlatformLogger.warn('warn'); + legacyPlatformLogger.error('error'); + + expect(mockStdout).toHaveBeenCalledTimes(1); + expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(` + Array [ + " log [xx:xx:xx.xxx] [error][test-file-legacy] error + ", + ] + `); + }); + + it('"verbose": true', async () => { + root = createRoot({ verbose: true }); + + await root.setup(); + await root.start(); + + const platformLogger = root.logger.get('test-file'); + platformLogger.info('info'); + platformLogger.warn('warn'); + platformLogger.error('error'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + + expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + ] + `); + + mockStdout.mockClear(); + + const legacyPlatformLogger = root.logger.get('test-file-legacy'); + legacyPlatformLogger.info('info'); + legacyPlatformLogger.warn('warn'); + legacyPlatformLogger.error('error'); + + expect(mockStdout).toHaveBeenCalledTimes(3); + expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(` + Array [ + " log [xx:xx:xx.xxx] [info][test-file-legacy] info + ", + " log [xx:xx:xx.xxx] [warning][test-file-legacy] warn + ", + " log [xx:xx:xx.xxx] [error][test-file-legacy] error + ", + ] + `); + }); + }); + }); +}); diff --git a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap index 10509b20e89423e..fe1407563a63518 100644 --- a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap @@ -13,6 +13,8 @@ Object { } `; +exports[`\`schema\` throws if \`root\` logger does not have "default" appender configured. 1`] = `"[root]: \\"default\\" appender required for migration period till the next major release"`; + exports[`\`schema\` throws if \`root\` logger does not have appenders configured. 1`] = `"[root.appenders]: array size is [0], but cannot be smaller than [1]"`; exports[`fails if loggers use unknown appenders. 1`] = `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`; diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 871acb8c465ca8d..3aa86495e4d8255 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -42,12 +42,6 @@ export type AppenderConfigType = TypeOf<typeof appendersSchema>; */ export interface Appender { append(record: LogRecord): void; - - /** - * Used to signal to `Logger` that log level filtering should be ignored for this appender. Defaults to `false`. - * @deprecated Should be removed once the `LegacyAppender` is removed. - */ - receiveAllLevels?: boolean; } /** diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts new file mode 100644 index 000000000000000..7142f91300f124a --- /dev/null +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -0,0 +1,114 @@ +/* + * 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 * as kbnTestServer from '../../../../test_utils/kbn_server'; + +function createRoot() { + return kbnTestServer.createRoot({ + logging: { + silent: true, // set "true" in kbnTestServer + appenders: { + 'test-console': { + kind: 'console', + layout: { + highlight: false, + kind: 'pattern', + pattern: '{level}|{context}|{message}', + }, + }, + }, + loggers: [ + { + context: 'parent', + appenders: ['test-console'], + level: 'warn', + }, + { + context: 'parent.child', + appenders: ['test-console'], + level: 'error', + }, + ], + }, + }); +} + +describe('logging service', () => { + describe('logs according to context hierarchy', () => { + let root: ReturnType<typeof createRoot>; + let mockConsoleLog: jest.SpyInstance; + beforeAll(async () => { + mockConsoleLog = jest.spyOn(global.console, 'log'); + root = createRoot(); + + await root.setup(); + }, 30000); + + beforeEach(() => { + mockConsoleLog.mockClear(); + }); + + afterAll(async () => { + mockConsoleLog.mockRestore(); + await root.shutdown(); + }); + + it('uses the most specific context', () => { + const logger = root.logger.get('parent.child'); + + logger.error('error from "parent.child" context'); + logger.warn('warning from "parent.child" context'); + logger.info('info from "parent.child" context'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'ERROR|parent.child|error from "parent.child" context' + ); + }); + + it('uses parent context', () => { + const logger = root.logger.get('parent.another-child'); + + logger.error('error from "parent.another-child" context'); + logger.warn('warning from "parent.another-child" context'); + logger.info('info from "parent.another-child" context'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'ERROR|parent.another-child|error from "parent.another-child" context' + ); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 2, + 'WARN |parent.another-child|warning from "parent.another-child" context' + ); + }); + + it('falls back to the root settings', () => { + const logger = root.logger.get('fallback'); + + logger.error('error from "fallback" context'); + logger.warn('warning from fallback" context'); + logger.info('info from "fallback" context'); + + // output muted by silent: true + expect(mockConsoleLog).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/src/core/server/logging/integration_tests/utils.ts b/src/core/server/logging/integration_tests/utils.ts new file mode 100644 index 000000000000000..81a76ce76ad733e --- /dev/null +++ b/src/core/server/logging/integration_tests/utils.ts @@ -0,0 +1,68 @@ +/* + * 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 Fs from 'fs'; +import Util from 'util'; +const readFile = Util.promisify(Fs.readFile); + +function replaceAllNumbers(input: string) { + return input.replace(/\d/g, 'x'); +} + +function replaceTimestamp(input: string) { + return input.replace(/\[(.*?)\]/, (full, key) => `[${replaceAllNumbers(key)}]`); +} + +function stripColors(input: string) { + return input.replace(/\u001b[^m]+m/g, ''); +} + +function normalizePlatformLogging(input: string) { + return replaceTimestamp(input); +} + +function normalizeLegacyPlatformLogging(input: string) { + return replaceTimestamp(stripColors(input)); +} + +export function getPlatformLogsFromMock(logMock: jest.SpyInstance<string, string[]>) { + return logMock.mock.calls.map(([message]) => message).map(normalizePlatformLogging); +} + +export function getLegacyPlatformLogsFromMock(stdoutMock: jest.SpyInstance<string, Buffer[]>) { + return stdoutMock.mock.calls + .map(([message]) => message) + .map(String) + .map(normalizeLegacyPlatformLogging); +} + +export async function getPlatformLogsFromFile(path: string) { + const fileContent = await readFile(path, 'utf-8'); + return fileContent + .split('\n') + .map(s => normalizePlatformLogging(s)) + .join('\n'); +} + +export async function getLegacyPlatformLogsFromFile(path: string) { + const fileContent = await readFile(path, 'utf-8'); + return fileContent + .split('\n') + .map(s => normalizeLegacyPlatformLogging(s)) + .join('\n'); +} diff --git a/src/core/server/logging/logger.test.ts b/src/core/server/logging/logger.test.ts index eeebb8ad5a0fa7e..026e24fc5df543d 100644 --- a/src/core/server/logging/logger.test.ts +++ b/src/core/server/logging/logger.test.ts @@ -410,85 +410,3 @@ test('passes log record to appenders only if log level is supported.', () => { }); } }); - -test('passes log record to appender with receiveAllLevels: true, regardless if log level is supported', () => { - const receiveAllAppender = { append: jest.fn(), receiveAllLevels: true }; - const warnLogger = new BaseLogger(context, LogLevel.Warn, [receiveAllAppender], factory); - - warnLogger.trace('trace-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(1); - expect(receiveAllAppender.append.mock.calls[0][0]).toMatchObject({ - level: LogLevel.Trace, - message: 'trace-message', - }); - - warnLogger.debug('debug-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(2); - expect(receiveAllAppender.append.mock.calls[1][0]).toMatchObject({ - level: LogLevel.Debug, - message: 'debug-message', - }); - - warnLogger.info('info-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(3); - expect(receiveAllAppender.append.mock.calls[2][0]).toMatchObject({ - level: LogLevel.Info, - message: 'info-message', - }); - - warnLogger.warn('warn-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(4); - expect(receiveAllAppender.append.mock.calls[3][0]).toMatchObject({ - level: LogLevel.Warn, - message: 'warn-message', - }); - - warnLogger.error('error-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(5); - expect(receiveAllAppender.append.mock.calls[4][0]).toMatchObject({ - level: LogLevel.Error, - message: 'error-message', - }); - - warnLogger.fatal('fatal-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(6); - expect(receiveAllAppender.append.mock.calls[5][0]).toMatchObject({ - level: LogLevel.Fatal, - message: 'fatal-message', - }); -}); - -test('passes log record to appender with receiveAllLevels: false, only if log level is supported', () => { - const notReceiveAllAppender = { append: jest.fn(), receiveAllLevels: false }; - const warnLogger = new BaseLogger(context, LogLevel.Warn, [notReceiveAllAppender], factory); - - warnLogger.trace('trace-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(0); - - warnLogger.debug('debug-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(0); - - warnLogger.info('info-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(0); - - warnLogger.warn('warn-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(1); - expect(notReceiveAllAppender.append.mock.calls[0][0]).toMatchObject({ - level: LogLevel.Warn, - message: 'warn-message', - }); - - warnLogger.error('error-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(2); - expect(notReceiveAllAppender.append.mock.calls[1][0]).toMatchObject({ - level: LogLevel.Error, - message: 'error-message', - }); - - warnLogger.fatal('fatal-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(3); - expect(notReceiveAllAppender.append.mock.calls[2][0]).toMatchObject({ - level: LogLevel.Fatal, - message: 'fatal-message', - }); -}); diff --git a/src/core/server/logging/logger.ts b/src/core/server/logging/logger.ts index ab6906ff5d68478..ac79c1916c07ba8 100644 --- a/src/core/server/logging/logger.ts +++ b/src/core/server/logging/logger.ts @@ -136,12 +136,12 @@ export class BaseLogger implements Logger { } public log(record: LogRecord) { - const supportedLevel = this.level.supports(record.level); + if (!this.level.supports(record.level)) { + return; + } for (const appender of this.appenders) { - if (supportedLevel || appender.receiveAllLevels) { - appender.append(record); - } + appender.append(record); } } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index 8eb79ac46e499a3..b3631abb9ff002b 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -33,6 +33,16 @@ test('`schema` throws if `root` logger does not have appenders configured.', () ).toThrowErrorMatchingSnapshot(); }); +test('`schema` throws if `root` logger does not have "default" appender configured.', () => { + expect(() => + config.schema.validate({ + root: { + appenders: ['console'], + }, + }) + ).toThrowErrorMatchingSnapshot(); +}); + test('`getParentLoggerContext()` returns correct parent context name.', () => { expect(LoggingConfig.getParentLoggerContext('a.b.c')).toEqual('a.b'); expect(LoggingConfig.getParentLoggerContext('a.b')).toEqual('a'); @@ -46,15 +56,23 @@ test('`getLoggerContext()` returns correct joined context name.', () => { expect(LoggingConfig.getLoggerContext([])).toEqual('root'); }); -test('correctly fills in default `appenders` config.', () => { +test('correctly fills in default config.', () => { const configValue = new LoggingConfig(config.schema.validate({})); - expect(configValue.appenders.size).toBe(1); + expect(configValue.appenders.size).toBe(3); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', layout: { kind: 'pattern', highlight: true }, }); + expect(configValue.appenders.get('console')).toEqual({ + kind: 'console', + layout: { kind: 'pattern', highlight: true }, + }); + expect(configValue.appenders.get('file')).toEqual({ + kind: 'file', + layout: { kind: 'pattern', highlight: false }, + }); }); test('correctly fills in custom `appenders` config.', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 84d707a3247e672..f1fbf787737b4a6 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -72,13 +72,22 @@ export const config = { loggers: schema.arrayOf(createLoggerSchema, { defaultValue: [], }), - root: schema.object({ - appenders: schema.arrayOf(schema.string(), { - defaultValue: [DEFAULT_APPENDER_NAME], - minSize: 1, - }), - level: createLevelSchema, - }), + root: schema.object( + { + appenders: schema.arrayOf(schema.string(), { + defaultValue: [DEFAULT_APPENDER_NAME], + minSize: 1, + }), + level: createLevelSchema, + }, + { + validate(rawConfig) { + if (!rawConfig.appenders.includes(DEFAULT_APPENDER_NAME)) { + return `"${DEFAULT_APPENDER_NAME}" appender required for migration period till the next major release`; + } + }, + } + ), }), }; @@ -118,12 +127,26 @@ export class LoggingConfig { */ public readonly appenders: Map<string, AppenderConfigType> = new Map([ [ - DEFAULT_APPENDER_NAME, + 'default', + { + kind: 'console', + layout: { kind: 'pattern', highlight: true }, + } as AppenderConfigType, + ], + [ + 'console', { kind: 'console', layout: { kind: 'pattern', highlight: true }, } as AppenderConfigType, ], + [ + 'file', + { + kind: 'file', + layout: { kind: 'pattern', highlight: false }, + } as AppenderConfigType, + ], ]); /** diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index f2a14df1d1eb380..a24ffcbaaa49f0b 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -103,6 +103,10 @@ export default () => logging: Joi.object() .keys({ + appenders: HANDLED_IN_NEW_PLATFORM, + loggers: HANDLED_IN_NEW_PLATFORM, + root: HANDLED_IN_NEW_PLATFORM, + silent: Joi.boolean().default(false), quiet: Joi.boolean().when('silent', { From 55e0e8d746ba360f5f2282187a9f2479badded6c Mon Sep 17 00:00:00 2001 From: patrykkopycinski <patryk.kopycinski@elastic.co> Date: Tue, 4 Feb 2020 13:00:08 +0100 Subject: [PATCH 20/22] [SIEM] Enable flow_target_select_connected unit tests (#55618) Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> --- .../index.test.tsx | 22 ++++++++++++++----- .../flow_target_select_connected/index.tsx | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx index 006587d8fc294bc..e71be5a51e50536 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx @@ -6,27 +6,37 @@ import { mount } from 'enzyme'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { TestProviders } from '../../../../mock'; -import { FlowTargetSelectConnected } from './index'; +import { FlowTargetSelectConnectedComponent } from './index'; import { FlowTarget } from '../../../../graphql/types'; -describe.skip('Flow Target Select Connected', () => { +describe('Flow Target Select Connected', () => { test('renders correctly against snapshot flowTarget source', () => { const wrapper = mount( <TestProviders> - <FlowTargetSelectConnected flowTarget={FlowTarget.source} /> + <MemoryRouter> + <FlowTargetSelectConnectedComponent flowTarget={FlowTarget.source} /> + </MemoryRouter> </TestProviders> ); - expect(wrapper.find('FlowTargetSelectConnected')).toMatchSnapshot(); + expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( + FlowTarget.source + ); }); test('renders correctly against snapshot flowTarget destination', () => { const wrapper = mount( <TestProviders> - <FlowTargetSelectConnected flowTarget={FlowTarget.destination} /> + <MemoryRouter> + <FlowTargetSelectConnectedComponent flowTarget={FlowTarget.destination} /> + </MemoryRouter> </TestProviders> ); - expect(wrapper.find('FlowTargetSelectConnected')).toMatchSnapshot(); + + expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( + FlowTarget.destination + ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx index 1b87c36902159ba..2651c31e0a2c9e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx @@ -36,7 +36,7 @@ const getUpdatedFlowTargetPath = ( return `${newPathame}${location.search}`; }; -const FlowTargetSelectConnectedComponent: React.FC<Props> = ({ flowTarget }) => { +export const FlowTargetSelectConnectedComponent: React.FC<Props> = ({ flowTarget }) => { const history = useHistory(); const location = useLocation(); From 661bb6b438138889f45270517cc8779fb20f2cab Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm <matthias.wilhelm@elastic.co> Date: Tue, 4 Feb 2020 13:08:52 +0100 Subject: [PATCH 21/22] [Discover] Migrate get_sort.js test from mocha to TypeScript (#56011) * Migrate get_sort.js test from mocha to jest and convert to TypeScript * Add jest test --- .../__tests__/doc_table/lib/get_sort.js | 104 ------------------ .../discover/np_ready/angular/discover.js | 11 +- .../angular/doc_table/lib/get_sort.d.ts | 27 ----- .../angular/doc_table/lib/get_sort.js | 62 ----------- .../angular/doc_table/lib/get_sort.test.ts | 91 +++++++++++++++ .../angular/doc_table/lib/get_sort.ts | 71 ++++++++++++ .../lib/get_sort_for_search_source.ts | 10 +- .../data/public/search/search_source/types.ts | 7 +- 8 files changed, 177 insertions(+), 206 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.d.ts delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.js create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js deleted file mode 100644 index d5485bca33cf503..000000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js +++ /dev/null @@ -1,104 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { getSort } from '../../../np_ready/angular/doc_table/lib/get_sort'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -let indexPattern; - -describe('docTable', function() { - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - describe('getSort function', function() { - it('should be a function', function() { - expect(getSort).to.be.a(Function); - }); - - it('should return an array of objects', function() { - expect(getSort([['bytes', 'desc']], indexPattern)).to.eql([{ bytes: 'desc' }]); - - delete indexPattern.timeFieldName; - expect(getSort([['bytes', 'desc']], indexPattern)).to.eql([{ bytes: 'desc' }]); - }); - - it('should passthrough arrays of objects', () => { - expect(getSort([{ bytes: 'desc' }], indexPattern)).to.eql([{ bytes: 'desc' }]); - }); - - it('should return an empty array when passed an unsortable field', function() { - expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql([]); - expect(getSort(['lol_nope', 'asc'], indexPattern)).to.eql([]); - - delete indexPattern.timeFieldName; - expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql([]); - }); - - it('should return an empty array ', function() { - expect(getSort([], indexPattern)).to.eql([]); - expect(getSort(['foo'], indexPattern)).to.eql([]); - expect(getSort({ foo: 'bar' }, indexPattern)).to.eql([]); - }); - - it('should return an empty array on non-time patterns', function() { - delete indexPattern.timeFieldName; - - expect(getSort([], indexPattern)).to.eql([]); - expect(getSort(['foo'], indexPattern)).to.eql([]); - expect(getSort({ foo: 'bar' }, indexPattern)).to.eql([]); - }); - }); - - describe('getSort.array function', function() { - it('should have an array method', function() { - expect(getSort.array).to.be.a(Function); - }); - - it('should return an array of arrays for sortable fields', function() { - expect(getSort.array([['bytes', 'desc']], indexPattern)).to.eql([['bytes', 'desc']]); - }); - - it('should return an array of arrays from an array of elasticsearch sort objects', function() { - expect(getSort.array([{ bytes: 'desc' }], indexPattern)).to.eql([['bytes', 'desc']]); - }); - - it('should sort by an empty array when an unsortable field is given', function() { - expect(getSort.array([{ 'non-sortable': 'asc' }], indexPattern)).to.eql([]); - expect(getSort.array([{ lol_nope: 'asc' }], indexPattern)).to.eql([]); - - delete indexPattern.timeFieldName; - expect(getSort.array([{ 'non-sortable': 'asc' }], indexPattern)).to.eql([]); - }); - - it('should return an empty array when passed an empty sort array', () => { - expect(getSort.array([], indexPattern)).to.eql([]); - - delete indexPattern.timeFieldName; - expect(getSort.array([], indexPattern)).to.eql([]); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 5e99cab1b32975f..2f73af2ab77e477 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -28,7 +28,7 @@ import '../components/field_chooser/field_chooser'; import { RequestAdapter } from '../../../../../../../plugins/inspector/public'; // doc table import './doc_table'; -import { getSort } from './doc_table/lib/get_sort'; +import { getSortArray } from './doc_table/lib/get_sort'; import { getSortForSearchSource } from './doc_table/lib/get_sort_for_search_source'; import * as columnActions from './doc_table/actions/columns'; @@ -525,7 +525,7 @@ function discoverController( language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), }, - sort: getSort.array(savedSearch.sort, $scope.indexPattern), + sort: getSortArray(savedSearch.sort, $scope.indexPattern), columns: savedSearch.columns.length > 0 ? savedSearch.columns : config.get('defaultColumns').slice(), index: $scope.indexPattern.id, @@ -537,7 +537,7 @@ function discoverController( } $state.index = $scope.indexPattern.id; - $state.sort = getSort.array($state.sort, $scope.indexPattern); + $state.sort = getSortArray($state.sort, $scope.indexPattern); $scope.getBucketIntervalToolTipText = () => { return i18n.translate('kbn.discover.bucketIntervalTooltip', { @@ -619,10 +619,7 @@ function discoverController( if (!sort) return; // get the current sort from searchSource as array of arrays - const currentSort = getSort.array( - $scope.searchSource.getField('sort'), - $scope.indexPattern - ); + const currentSort = getSortArray($scope.searchSource.getField('sort'), $scope.indexPattern); // if the searchSource doesn't know, tell it so if (!angular.equals(sort, currentSort)) $scope.fetch(); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.d.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.d.ts deleted file mode 100644 index 0bf8a93a883673a..000000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.d.ts +++ /dev/null @@ -1,27 +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 { IIndexPattern } from '../../../../../../../../../plugins/data/public'; -import { SortOrder } from '../components/table_header/helpers'; - -export function getSort( - sort?: SortOrder[], - indexPattern?: IIndexPattern, - defaultSortOrder?: SortOrder -): any; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.js deleted file mode 100644 index ce32fdaeda23755..000000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.js +++ /dev/null @@ -1,62 +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 _ from 'lodash'; - -export function isSortable(field, indexPattern) { - return indexPattern.fields.getByName(field) && indexPattern.fields.getByName(field).sortable; -} - -function createSortObject(sortPair, indexPattern) { - if (Array.isArray(sortPair) && sortPair.length === 2 && isSortable(sortPair[0], indexPattern)) { - const [field, direction] = sortPair; - return { [field]: direction }; - } else if (_.isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], indexPattern)) { - return sortPair; - } else { - return undefined; - } -} - -/** - * Take a sorting array and make it into an object - * @param {array} sort two dimensional array [[fieldToSort, directionToSort]] - * or an array of objects [{fieldToSort: directionToSort}] - * @param {object} indexPattern used for determining default sort - * @returns {object} a sort object suitable for returning to elasticsearch - */ -export function getSort(sort, indexPattern) { - let sortObjects; - if (Array.isArray(sort)) { - sortObjects = _.compact(sort.map(sortPair => createSortObject(sortPair, indexPattern))); - } - - if (!_.isEmpty(sortObjects)) { - return sortObjects; - } - return []; -} - -getSort.array = function(sort, indexPattern) { - return getSort(sort, indexPattern).map(sortPair => - _(sortPair) - .pairs() - .pop() - ); -}; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.test.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.test.ts new file mode 100644 index 000000000000000..c9cbad245f5e41f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.test.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 { getSort, getSortArray } from './get_sort'; +// @ts-ignore +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../kibana_services'; + +describe('docTable', function() { + let indexPattern: IndexPattern; + + beforeEach(() => { + indexPattern = FixturesStubbedLogstashIndexPatternProvider() as IndexPattern; + }); + + describe('getSort function', function() { + test('should be a function', function() { + expect(typeof getSort === 'function').toBeTruthy(); + }); + + test('should return an array of objects', function() { + expect(getSort([['bytes', 'desc']], indexPattern)).toEqual([{ bytes: 'desc' }]); + + delete indexPattern.timeFieldName; + expect(getSort([['bytes', 'desc']], indexPattern)).toEqual([{ bytes: 'desc' }]); + }); + + test('should passthrough arrays of objects', () => { + expect(getSort([{ bytes: 'desc' }], indexPattern)).toEqual([{ bytes: 'desc' }]); + }); + + test('should return an empty array when passed an unsortable field', function() { + expect(getSort([['non-sortable', 'asc']], indexPattern)).toEqual([]); + expect(getSort([['lol_nope', 'asc']], indexPattern)).toEqual([]); + + delete indexPattern.timeFieldName; + expect(getSort([['non-sortable', 'asc']], indexPattern)).toEqual([]); + }); + + test('should return an empty array ', function() { + expect(getSort([], indexPattern)).toEqual([]); + expect(getSort([['foo', 'bar']], indexPattern)).toEqual([]); + expect(getSort([{ foo: 'bar' }], indexPattern)).toEqual([]); + }); + }); + + describe('getSortArray function', function() { + test('should have an array method', function() { + expect(getSortArray).toBeInstanceOf(Function); + }); + + test('should return an array of arrays for sortable fields', function() { + expect(getSortArray([['bytes', 'desc']], indexPattern)).toEqual([['bytes', 'desc']]); + }); + + test('should return an array of arrays from an array of elasticsearch sort objects', function() { + expect(getSortArray([{ bytes: 'desc' }], indexPattern)).toEqual([['bytes', 'desc']]); + }); + + test('should sort by an empty array when an unsortable field is given', function() { + expect(getSortArray([{ 'non-sortable': 'asc' }], indexPattern)).toEqual([]); + expect(getSortArray([{ lol_nope: 'asc' }], indexPattern)).toEqual([]); + + delete indexPattern.timeFieldName; + expect(getSortArray([{ 'non-sortable': 'asc' }], indexPattern)).toEqual([]); + }); + + test('should return an empty array when passed an empty sort array', () => { + expect(getSortArray([], indexPattern)).toEqual([]); + + delete indexPattern.timeFieldName; + expect(getSortArray([], indexPattern)).toEqual([]); + }); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.ts new file mode 100644 index 000000000000000..a8dbaa50e5aa878 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort.ts @@ -0,0 +1,71 @@ +/* + * 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 _ from 'lodash'; +import { IndexPattern } from '../../../../../../../../../plugins/data/public'; + +export type SortPairObj = Record<string, string>; +export type SortPairArr = [string, string]; +export type SortPair = SortPairArr | SortPairObj; +export type SortInput = SortPair | SortPair[]; + +export function isSortable(fieldName: string, indexPattern: IndexPattern) { + const field = indexPattern.getFieldByName(fieldName); + return field && field.sortable; +} + +function createSortObject( + sortPair: SortInput, + indexPattern: IndexPattern +): SortPairObj | undefined { + if ( + Array.isArray(sortPair) && + sortPair.length === 2 && + isSortable(String(sortPair[0]), indexPattern) + ) { + const [field, direction] = sortPair as SortPairArr; + return { [field]: direction }; + } else if (_.isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], indexPattern)) { + return sortPair as SortPairObj; + } +} + +/** + * Take a sorting array and make it into an object + * @param {array} sort two dimensional array [[fieldToSort, directionToSort]] + * or an array of objects [{fieldToSort: directionToSort}] + * @param {object} indexPattern used for determining default sort + * @returns Array<{object}> an array of sort objects + */ +export function getSort(sort: SortPair[], indexPattern: IndexPattern): SortPairObj[] { + if (Array.isArray(sort)) { + return sort + .map((sortPair: SortPair) => createSortObject(sortPair, indexPattern)) + .filter(sortPairObj => typeof sortPairObj === 'object') as SortPairObj[]; + } + return []; +} + +/** + * compared to getSort it doesn't return an array of objects, it returns an array of arrays + * [[fieldToSort: directionToSort]] + */ +export function getSortArray(sort: SortPair[], indexPattern: IndexPattern) { + return getSort(sort, indexPattern).map(sortPair => Object.entries(sortPair).pop()); +} diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort_for_search_source.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort_for_search_source.ts index 62a44d30adfd55e..6721f7a03584cd5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort_for_search_source.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/lib/get_sort_for_search_source.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { IndexPattern } from '../../../../kibana_services'; +import { EsQuerySortValue, IndexPattern } from '../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; import { getSort } from './get_sort'; import { getDefaultSort } from './get_default_sort'; @@ -31,8 +31,8 @@ import { getDefaultSort } from './get_default_sort'; export function getSortForSearchSource( sort?: SortOrder[], indexPattern?: IndexPattern, - defaultDirection: 'asc' | 'desc' = 'desc' -) { + defaultDirection: string = 'desc' +): EsQuerySortValue[] { if (!sort || !indexPattern) { return []; } else if (Array.isArray(sort) && sort.length === 0) { @@ -46,8 +46,8 @@ export function getSortForSearchSource( order: sortPair[timeFieldName], numeric_type: 'date_nanos', }, - }; + } as EsQuerySortValue; } - return sortPair; + return sortPair as EsQuerySortValue; }); } diff --git a/src/plugins/data/public/search/search_source/types.ts b/src/plugins/data/public/search/search_source/types.ts index 17337c905db8776..268d24aaa6df118 100644 --- a/src/plugins/data/public/search/search_source/types.ts +++ b/src/plugins/data/public/search/search_source/types.ts @@ -26,7 +26,12 @@ export enum SortDirection { desc = 'desc', } -export type EsQuerySortValue = Record<string, SortDirection>; +export interface SortDirectionNumeric { + order: SortDirection; + numeric_type?: 'double' | 'long' | 'date' | 'date_nanos'; +} + +export type EsQuerySortValue = Record<string, SortDirection | SortDirectionNumeric>; export interface SearchSourceFields { type?: string; From d73e15f4147d1de9a7ee15517cbf6a90f4d4b213 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm <matthias.wilhelm@elastic.co> Date: Tue, 4 Feb 2020 14:16:41 +0100 Subject: [PATCH 22/22] [Discover] Inline angular directives only used in this plugin (#56119) * Migrate field_name directive * Migrate collapsible_sidebar directive * Fix FieldName import at table_row.tsx * Migrate css_truncate directive * Migrate fixed_scroll & debounce directives * Migrate render_complete directive * Fix css_truncate test * Use shortenDottedString in the TypesScript version --- .../__tests__/directives}/css_truncate.js | 6 +++-- .../__tests__/directives}/fixed_scroll.js | 7 +++--- .../public/discover/get_inner_angular.ts | 24 ++++++++----------- .../kibana/public/discover/kibana_services.ts | 5 ---- .../np_ready/angular/directives/_index.scss | 3 ++- .../_collapsible_sidebar.scss | 0 .../collapsible_sidebar}/_depth.scss | 0 .../collapsible_sidebar/_index.scss | 2 ++ .../collapsible_sidebar.ts} | 14 ++++++----- .../angular/directives/css_truncate.ts} | 10 ++------ .../directives/debounce/__tests__/debounce.js | 7 ++++-- .../angular}/directives/debounce/debounce.js | 5 ---- .../angular}/directives/debounce/index.js | 0 .../angular}/directives/field_name.js | 6 +---- .../__snapshots__/field_name.test.tsx.snap | 0 .../directives/field_name/field_name.test.tsx | 0 .../directives/field_name/field_name.tsx | 5 ++-- .../directives/field_name/field_type_name.ts | 24 +++++++++---------- .../angular/directives}/fixed_scroll.js | 15 +++++------- .../angular/directives/render_complete.ts} | 9 +++---- .../components/table_header/helpers.tsx | 3 +-- .../np_ready/components/table/table_row.tsx | 2 +- src/legacy/ui/public/_index.scss | 1 - .../ui/public/collapsible_sidebar/_index.scss | 3 --- .../ui/public/collapsible_sidebar/index.js | 20 ---------------- .../ui/public/styles/_legacy/_index.scss | 1 - .../translations/translations/ja-JP.json | 22 ++++++++--------- .../translations/translations/zh-CN.json | 22 ++++++++--------- 28 files changed, 85 insertions(+), 131 deletions(-) rename src/legacy/{ui/public/directives/__tests__ => core_plugins/kibana/public/discover/__tests__/directives}/css_truncate.js (92%) rename src/legacy/{ui/public/directives/__tests__ => core_plugins/kibana/public/discover/__tests__/directives}/fixed_scroll.js (96%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular/directives}/collapsible_sidebar/_collapsible_sidebar.scss (100%) rename src/legacy/{ui/public/styles/_legacy => core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar}/_depth.scss (100%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss rename src/legacy/{ui/public/collapsible_sidebar/collapsible_sidebar.js => core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts} (91%) rename src/legacy/{ui/public/directives/css_truncate.js => core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts} (89%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/debounce/__tests__/debounce.js (95%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/debounce/debounce.js (92%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/debounce/index.js (100%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/field_name.js (86%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/field_name/__snapshots__/field_name.test.tsx.snap (100%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/field_name/field_name.test.tsx (100%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/field_name/field_name.tsx (91%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular}/directives/field_name/field_type_name.ts (62%) rename src/legacy/{ui/public => core_plugins/kibana/public/discover/np_ready/angular/directives}/fixed_scroll.js (96%) rename src/legacy/{ui/public/render_complete/directive.js => core_plugins/kibana/public/discover/np_ready/angular/directives/render_complete.ts} (81%) delete mode 100644 src/legacy/ui/public/collapsible_sidebar/_index.scss delete mode 100644 src/legacy/ui/public/collapsible_sidebar/index.js diff --git a/src/legacy/ui/public/directives/__tests__/css_truncate.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js similarity index 92% rename from src/legacy/ui/public/directives/__tests__/css_truncate.js rename to src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js index bf102f5a29fdb92..8dea9c61475dbe1 100644 --- a/src/legacy/ui/public/directives/__tests__/css_truncate.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js @@ -20,7 +20,7 @@ import angular from 'angular'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import 'plugins/kibana/discover/legacy'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; let $parentScope; @@ -30,7 +30,9 @@ let $elem; const init = function(expandable) { // Load the application - ngMock.module('kibana'); + pluginInstance.initializeServices(); + pluginInstance.initializeInnerAngular(); + ngMock.module('app/discover'); // Create the scope ngMock.inject(function($rootScope, $compile) { diff --git a/src/legacy/ui/public/directives/__tests__/fixed_scroll.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/fixed_scroll.js similarity index 96% rename from src/legacy/ui/public/directives/__tests__/fixed_scroll.js rename to src/legacy/core_plugins/kibana/public/discover/__tests__/directives/fixed_scroll.js index b35836967c3f38e..49a0df54079ea56 100644 --- a/src/legacy/ui/public/directives/__tests__/fixed_scroll.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/fixed_scroll.js @@ -18,8 +18,8 @@ */ import expect from '@kbn/expect'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import ngMock from 'ng_mock'; -import '../../fixed_scroll'; import $ from 'jquery'; import sinon from 'sinon'; @@ -29,8 +29,9 @@ describe('FixedScroll directive', function() { let compile; let flushPendingTasks; const trash = []; - - beforeEach(ngMock.module('kibana')); + beforeEach(() => pluginInstance.initializeServices()); + beforeEach(() => pluginInstance.initializeInnerAngular()); + beforeEach(ngMock.module('app/discover')); beforeEach( ngMock.inject(function($compile, $rootScope, $timeout) { flushPendingTasks = function flushPendingTasks() { diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index fea834686eb4f00..36a6c8eaef40ec7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -42,22 +42,10 @@ import { registerListenEventListener } from 'ui/directives/listen/listen'; // @ts-ignore import { KbnAccessibleClickProvider } from 'ui/accessibility/kbn_accessible_click'; // @ts-ignore -import { FieldNameDirectiveProvider } from 'ui/directives/field_name'; -// @ts-ignore -import { CollapsibleSidebarProvider } from 'ui/collapsible_sidebar/collapsible_sidebar'; -// @ts-ignore -import { CssTruncateProvide } from 'ui/directives/css_truncate'; -// @ts-ignore -import { FixedScrollProvider } from 'ui/fixed_scroll'; -// @ts-ignore -import { DebounceProviderTimeout } from 'ui/directives/debounce/debounce'; -// @ts-ignore import { AppStateProvider } from 'ui/state_management/app_state'; // @ts-ignore import { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore -import { createRenderCompleteDirective } from 'ui/render_complete/directive'; -// @ts-ignore import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore import { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; @@ -81,11 +69,19 @@ import { createFieldSearchDirective } from './np_ready/components/field_chooser/ import { createIndexPatternSelectDirective } from './np_ready/components/field_chooser/discover_index_pattern_directive'; import { createStringFieldProgressBarDirective } from './np_ready/components/field_chooser/string_progress_bar'; // @ts-ignore +import { FieldNameDirectiveProvider } from './np_ready/angular/directives/field_name'; +// @ts-ignore import { createFieldChooserDirective } from './np_ready/components/field_chooser/field_chooser'; - // @ts-ignore import { createDiscoverFieldDirective } from './np_ready/components/field_chooser/discover_field'; +import { CollapsibleSidebarProvider } from './np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar'; import { DiscoverStartPlugins } from './plugin'; +import { createCssTruncateDirective } from './np_ready/angular/directives/css_truncate'; +// @ts-ignore +import { FixedScrollProvider } from './np_ready/angular/directives/fixed_scroll'; +// @ts-ignore +import { DebounceProviderTimeout } from './np_ready/angular/directives/debounce/debounce'; +import { createRenderCompleteDirective } from './np_ready/angular/directives/render_complete'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -181,7 +177,7 @@ export function initializeInnerAngularModule( .directive('kbnAccessibleClick', KbnAccessibleClickProvider) .directive('fieldName', FieldNameDirectiveProvider) .directive('collapsibleSidebar', CollapsibleSidebarProvider) - .directive('cssTruncate', CssTruncateProvide) + .directive('cssTruncate', createCssTruncateDirective) .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) .directive('discoverFieldSearch', createFieldSearchDirective) diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 9a0b0731b6b111a..d1e1dafe7c87833 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -47,10 +47,6 @@ export function setServices(newServices: any) { services = newServices; } -// import directives that -import 'ui/directives/css_truncate'; -import 'ui/directives/field_name'; - // EXPORT legacy static dependencies, should be migrated when available in a new version; export { angular }; export { wrapInI18nContext } from 'ui/i18n'; @@ -90,7 +86,6 @@ export { } from '../../../../../plugins/data/public'; export { ElasticSearchHit } from './np_ready/doc_views/doc_views_types'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; -export { FieldName } from 'ui/directives/field_name/field_name'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; // @ts-ignore export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss index c65243d99c8f492..2bfc74ffa027909 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss @@ -1,2 +1,3 @@ @import 'no_results'; -@import 'histogram'; \ No newline at end of file +@import 'histogram'; +@import './collapsible_sidebar/index'; diff --git a/src/legacy/ui/public/collapsible_sidebar/_collapsible_sidebar.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_collapsible_sidebar.scss similarity index 100% rename from src/legacy/ui/public/collapsible_sidebar/_collapsible_sidebar.scss rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_collapsible_sidebar.scss diff --git a/src/legacy/ui/public/styles/_legacy/_depth.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_depth.scss similarity index 100% rename from src/legacy/ui/public/styles/_legacy/_depth.scss rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_depth.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss new file mode 100644 index 000000000000000..1409920d11aa7f7 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss @@ -0,0 +1,2 @@ +@import 'depth'; +@import 'collapsible_sidebar'; diff --git a/src/legacy/ui/public/collapsible_sidebar/collapsible_sidebar.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts similarity index 91% rename from src/legacy/ui/public/collapsible_sidebar/collapsible_sidebar.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts index 98b8f310bb82f08..5b6de7f16d44464 100644 --- a/src/legacy/ui/public/collapsible_sidebar/collapsible_sidebar.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts @@ -19,7 +19,11 @@ import _ from 'lodash'; import $ from 'jquery'; -import { uiModules } from '../modules'; +import { IScope } from 'angular'; + +interface LazyScope extends IScope { + [key: string]: any; +} export function CollapsibleSidebarProvider() { // simply a list of all of all of angulars .col-md-* classes except 12 @@ -29,7 +33,7 @@ export function CollapsibleSidebarProvider() { return { restrict: 'C', - link: function($scope, $elem) { + link: ($scope: LazyScope, $elem: any) => { let isCollapsed = false; const $collapser = $( `<button @@ -48,10 +52,10 @@ export function CollapsibleSidebarProvider() { $collapser.append($icon); const $siblings = $elem.siblings(); - const siblingsClass = listOfWidthClasses.reduce(function(prev, className) { + const siblingsClass = listOfWidthClasses.reduce((prev: string, className: string) => { if (prev) return prev; return $siblings.hasClass(className) && className; - }, false); + }, ''); // If there is are only two elements we can assume the other one will take 100% of the width. const hasSingleSibling = $siblings.length === 1 && siblingsClass; @@ -82,5 +86,3 @@ export function CollapsibleSidebarProvider() { }, }; } - -uiModules.get('kibana').directive('collapsibleSidebar', CollapsibleSidebarProvider); diff --git a/src/legacy/ui/public/directives/css_truncate.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts similarity index 89% rename from src/legacy/ui/public/directives/css_truncate.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts index 9b2c8a49f719f84..6aa645ea9368ec6 100644 --- a/src/legacy/ui/public/directives/css_truncate.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/css_truncate.ts @@ -16,15 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -import { uiModules } from '../modules'; -const module = uiModules.get('kibana'); - -export function CssTruncateProvide() { +export function createCssTruncateDirective() { return { restrict: 'A', scope: {}, - link: function($scope, $elem, attrs) { + link: ($scope: any, $elem: any, attrs: any) => { $elem.css({ overflow: 'hidden', 'white-space': 'nowrap', @@ -63,5 +59,3 @@ export function CssTruncateProvide() { }, }; } - -module.directive('cssTruncate', CssTruncateProvide); diff --git a/src/legacy/ui/public/directives/debounce/__tests__/debounce.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/__tests__/debounce.js similarity index 95% rename from src/legacy/ui/public/directives/debounce/__tests__/debounce.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/__tests__/debounce.js index bfbe809139204fa..43fa5ffbf299a21 100644 --- a/src/legacy/ui/public/directives/debounce/__tests__/debounce.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/__tests__/debounce.js @@ -20,14 +20,17 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { DebounceProvider } from '..'; +import { DebounceProvider } from '../index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; let debounce; let debounceFromProvider; let $timeout; function init() { - ngMock.module('kibana'); + pluginInstance.initializeServices(); + pluginInstance.initializeInnerAngular(); + ngMock.module('app/discover'); ngMock.inject(function($injector, _$timeout_, Private) { $timeout = _$timeout_; diff --git a/src/legacy/ui/public/directives/debounce/debounce.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/debounce.js similarity index 92% rename from src/legacy/ui/public/directives/debounce/debounce.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/debounce.js index 7515ff4c0078515..54507f673c2d6a8 100644 --- a/src/legacy/ui/public/directives/debounce/debounce.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/debounce.js @@ -18,12 +18,9 @@ */ import _ from 'lodash'; -import { uiModules } from '../../modules'; // Debounce service, angularized version of lodash debounce // borrowed heavily from https://github.com/shahata/angular-debounce -const module = uiModules.get('kibana'); - export function DebounceProviderTimeout($timeout) { return function(func, wait, options) { let timeout; @@ -70,8 +67,6 @@ export function DebounceProviderTimeout($timeout) { }; } -module.service('debounce', ['$timeout', DebounceProviderTimeout]); - export function DebounceProvider(debounce) { return debounce; } diff --git a/src/legacy/ui/public/directives/debounce/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/index.js similarity index 100% rename from src/legacy/ui/public/directives/debounce/index.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/debounce/index.js diff --git a/src/legacy/ui/public/directives/field_name.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js similarity index 86% rename from src/legacy/ui/public/directives/field_name.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js index aff849fc5602f37..4bc498928be52a3 100644 --- a/src/legacy/ui/public/directives/field_name.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name.js @@ -17,9 +17,7 @@ * under the License. */ import { FieldName } from './field_name/field_name'; -import { uiModules } from '../modules'; -import { wrapInI18nContext } from 'ui/i18n'; -const module = uiModules.get('kibana'); +import { wrapInI18nContext } from '../../../kibana_services'; export function FieldNameDirectiveProvider(config, reactDirective) { return reactDirective( @@ -35,5 +33,3 @@ export function FieldNameDirectiveProvider(config, reactDirective) { } ); } - -module.directive('fieldName', FieldNameDirectiveProvider); diff --git a/src/legacy/ui/public/directives/field_name/__snapshots__/field_name.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap similarity index 100% rename from src/legacy/ui/public/directives/field_name/__snapshots__/field_name.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/__snapshots__/field_name.test.tsx.snap diff --git a/src/legacy/ui/public/directives/field_name/field_name.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.test.tsx similarity index 100% rename from src/legacy/ui/public/directives/field_name/field_name.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.test.tsx diff --git a/src/legacy/ui/public/directives/field_name/field_name.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx similarity index 91% rename from src/legacy/ui/public/directives/field_name/field_name.tsx rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx index 0340ce9cb5d1b2d..95720bee38df8d8 100644 --- a/src/legacy/ui/public/directives/field_name/field_name.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_name.tsx @@ -18,9 +18,8 @@ */ import React from 'react'; import classNames from 'classnames'; -// @ts-ignore -import { shortenDottedString } from '../../../../core_plugins/kibana/common/utils/shorten_dotted_string'; -import { FieldIcon } from '../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon } from '../../../../../../../../../plugins/kibana_react/public'; +import { shortenDottedString } from '../../../../../../../../../plugins/data/common/utils'; import { getFieldTypeName } from './field_type_name'; // property field is provided at discover's field chooser diff --git a/src/legacy/ui/public/directives/field_name/field_type_name.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_type_name.ts similarity index 62% rename from src/legacy/ui/public/directives/field_name/field_type_name.ts rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_type_name.ts index c8c886015cea322..0cf428ee48b9d87 100644 --- a/src/legacy/ui/public/directives/field_name/field_type_name.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/field_name/field_type_name.ts @@ -21,52 +21,52 @@ import { i18n } from '@kbn/i18n'; export function getFieldTypeName(type: string) { switch (type) { case 'boolean': - return i18n.translate('common.ui.directives.fieldNameIcons.booleanAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.booleanAriaLabel', { defaultMessage: 'Boolean field', }); case 'conflict': - return i18n.translate('common.ui.directives.fieldNameIcons.conflictFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.conflictFieldAriaLabel', { defaultMessage: 'Conflicting field', }); case 'date': - return i18n.translate('common.ui.directives.fieldNameIcons.dateFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.dateFieldAriaLabel', { defaultMessage: 'Date field', }); case 'geo_point': - return i18n.translate('common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.geoPointFieldAriaLabel', { defaultMessage: 'Geo point field', }); case 'geo_shape': - return i18n.translate('common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.geoShapeFieldAriaLabel', { defaultMessage: 'Geo shape field', }); case 'ip': - return i18n.translate('common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.ipAddressFieldAriaLabel', { defaultMessage: 'IP address field', }); case 'murmur3': - return i18n.translate('common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.murmur3FieldAriaLabel', { defaultMessage: 'Murmur3 field', }); case 'number': - return i18n.translate('common.ui.directives.fieldNameIcons.numberFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.numberFieldAriaLabel', { defaultMessage: 'Number field', }); case 'source': // Note that this type is currently not provided, type for _source is undefined - return i18n.translate('common.ui.directives.fieldNameIcons.sourceFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.sourceFieldAriaLabel', { defaultMessage: 'Source field', }); case 'string': - return i18n.translate('common.ui.directives.fieldNameIcons.stringFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String field', }); case 'nested': - return i18n.translate('common.ui.directives.fieldNameIcons.nestedFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.nestedFieldAriaLabel', { defaultMessage: 'Nested field', }); default: - return i18n.translate('common.ui.directives.fieldNameIcons.unknownFieldAriaLabel', { + return i18n.translate('kbn.discover.fieldNameIcons.unknownFieldAriaLabel', { defaultMessage: 'Unknown field', }); } diff --git a/src/legacy/ui/public/fixed_scroll.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/fixed_scroll.js similarity index 96% rename from src/legacy/ui/public/fixed_scroll.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/fixed_scroll.js index 228ba3f8aaa99ae..bc159c14a16a714 100644 --- a/src/legacy/ui/public/fixed_scroll.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/fixed_scroll.js @@ -19,11 +19,15 @@ import $ from 'jquery'; import _ from 'lodash'; -import { uiModules } from './modules'; -import { DebounceProvider } from 'ui/directives/debounce'; +import { DebounceProvider } from './debounce'; const SCROLLER_HEIGHT = 20; +/** + * This directive adds a fixed horizontal scrollbar to the bottom of the window that proxies its scroll events + * to the target element's real scrollbar. This is useful when the target element's horizontal scrollbar + * might be waaaay down the page, like the doc table on Discover. + */ export function FixedScrollProvider(Private) { const debounce = Private(DebounceProvider); @@ -145,10 +149,3 @@ export function FixedScrollProvider(Private) { }, }; } - -/** - * This directive adds a fixed horizontal scrollbar to the bottom of the window that proxies its scroll events - * to the target element's real scrollbar. This is useful when the target element's horizontal scrollbar - * might be waaaay down the page, like the doc table on Discover. - */ -uiModules.get('kibana').directive('fixedScroll', FixedScrollProvider); diff --git a/src/legacy/ui/public/render_complete/directive.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/render_complete.ts similarity index 81% rename from src/legacy/ui/public/render_complete/directive.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/render_complete.ts index 961a07f58293a02..7757deb806a18e8 100644 --- a/src/legacy/ui/public/render_complete/directive.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/render_complete.ts @@ -16,18 +16,15 @@ * specific language governing permissions and limitations * under the License. */ - -import { uiModules } from '../modules'; -import { RenderCompleteHelper } from '../../../../plugins/kibana_utils/public'; +import { IScope } from 'angular'; +import { RenderCompleteHelper } from '../../../../../../../../plugins/kibana_utils/public'; export function createRenderCompleteDirective() { return { - controller($scope, $element) { + controller($scope: IScope, $element: JQLite) { const el = $element[0]; const renderCompleteHelper = new RenderCompleteHelper(el); $scope.$on('$destroy', renderCompleteHelper.destroy); }, }; } - -uiModules.get('kibana').directive('renderComplete', createRenderCompleteDirective); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx index 990ace32e644912..68ba508ffebdd67 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/doc_table/components/table_header/helpers.tsx @@ -17,8 +17,7 @@ * under the License. */ import { IndexPattern } from '../../../../../kibana_services'; -// @ts-ignore -import { shortenDottedString } from '../../../../../../../common/utils/shorten_dotted_string'; +import { shortenDottedString } from '../../../../../../../../../../plugins/data/common/utils'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx index d1b05f20a6cb0d7..7a78e8941636130 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table_row.tsx @@ -18,7 +18,6 @@ */ import classNames from 'classnames'; import React, { ReactNode } from 'react'; -import { FieldName } from '../../../kibana_services'; import { FieldMapping, DocViewFilterFn } from '../../doc_views/doc_views_types'; import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add'; import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; @@ -27,6 +26,7 @@ import { DocViewTableRowBtnCollapse } from './table_row_btn_collapse'; import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; import { DocViewTableRowIconNoMapping } from './table_row_icon_no_mapping'; import { DocViewTableRowIconUnderscore } from './table_row_icon_underscore'; +import { FieldName } from '../../angular/directives/field_name/field_name'; export interface Props { field: string; diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index e4e58019dda6916..e990ba2a46de531 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -10,7 +10,6 @@ @import './accessibility/index'; @import './chrome/index'; -@import './collapsible_sidebar/index'; @import './directives/index'; @import './error_auto_create_index/index'; @import './error_url_overflow/index'; diff --git a/src/legacy/ui/public/collapsible_sidebar/_index.scss b/src/legacy/ui/public/collapsible_sidebar/_index.scss deleted file mode 100644 index 84b759f66720ed5..000000000000000 --- a/src/legacy/ui/public/collapsible_sidebar/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../styles/_legacy/depth'; - -@import './collapsible_sidebar'; diff --git a/src/legacy/ui/public/collapsible_sidebar/index.js b/src/legacy/ui/public/collapsible_sidebar/index.js deleted file mode 100644 index 9f818922f713601..000000000000000 --- a/src/legacy/ui/public/collapsible_sidebar/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. - */ - -import './collapsible_sidebar'; diff --git a/src/legacy/ui/public/styles/_legacy/_index.scss b/src/legacy/ui/public/styles/_legacy/_index.scss index 106730fecd2b8e6..a0b1a98b09b7dd7 100644 --- a/src/legacy/ui/public/styles/_legacy/_index.scss +++ b/src/legacy/ui/public/styles/_legacy/_index.scss @@ -1,6 +1,5 @@ // // // // KIBANA THEME -@import './depth'; @import './base'; @import './mixins'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3a6ba45413de600..74fbbdd5f967b68 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -183,17 +183,6 @@ "common.ui.chrome.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "common.ui.chrome.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", - "common.ui.directives.fieldNameIcons.booleanAriaLabel": "ブールフィールド", - "common.ui.directives.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド", - "common.ui.directives.fieldNameIcons.dateFieldAriaLabel": "日付フィールド", - "common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイント", - "common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形", - "common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel": "IP アドレスフィールド", - "common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 フィールド", - "common.ui.directives.fieldNameIcons.numberFieldAriaLabel": "数値フィールド", - "common.ui.directives.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", - "common.ui.directives.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド", - "common.ui.directives.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", "common.ui.directives.paginate.size.allDropDownOptionLabel": "すべて", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "値は {min} と {max} の間でなければなりません", @@ -1114,6 +1103,17 @@ "kbn.discover.fieldChooser.searchPlaceHolder": "検索フィールド", "kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "フィールド設定を非表示", "kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "フィールド設定を表示", + "kbn.discover.fieldNameIcons.booleanAriaLabel": "ブールフィールド", + "kbn.discover.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド", + "kbn.discover.fieldNameIcons.dateFieldAriaLabel": "日付フィールド", + "kbn.discover.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイント", + "kbn.discover.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形", + "kbn.discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP アドレスフィールド", + "kbn.discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 フィールド", + "kbn.discover.fieldNameIcons.numberFieldAriaLabel": "数値フィールド", + "kbn.discover.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", + "kbn.discover.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド", + "kbn.discover.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", "kbn.discover.histogram.partialData.bucketTooltipText": "選択された時間範囲にはこのバケット全体は含まれていませんが、一部データが含まれている可能性があります。", "kbn.discover.histogramOfFoundDocumentsAriaLabel": "発見されたドキュメントのヒストグラム", "kbn.discover.hitsPluralTitle": "{hits, plural, one {ヒット} other {ヒット}}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f7cbaa7d72158db..97c4ea7d9f5a9b8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -183,17 +183,6 @@ "common.ui.chrome.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "common.ui.chrome.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", - "common.ui.directives.fieldNameIcons.booleanAriaLabel": "布尔字段", - "common.ui.directives.fieldNameIcons.conflictFieldAriaLabel": "冲突字段", - "common.ui.directives.fieldNameIcons.dateFieldAriaLabel": "日期字段", - "common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel": "地理位置点字段", - "common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段", - "common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段", - "common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段", - "common.ui.directives.fieldNameIcons.numberFieldAriaLabel": "数字字段", - "common.ui.directives.fieldNameIcons.sourceFieldAriaLabel": "源字段", - "common.ui.directives.fieldNameIcons.stringFieldAriaLabel": "字符串字段", - "common.ui.directives.fieldNameIcons.unknownFieldAriaLabel": "未知字段", "common.ui.directives.paginate.size.allDropDownOptionLabel": "全部", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", @@ -1114,6 +1103,17 @@ "kbn.discover.fieldChooser.searchPlaceHolder": "搜索字段", "kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "隐藏字段设置", "kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "显示字段设置", + "kbn.discover.fieldNameIcons.booleanAriaLabel": "布尔字段", + "kbn.discover.fieldNameIcons.conflictFieldAriaLabel": "冲突字段", + "kbn.discover.fieldNameIcons.dateFieldAriaLabel": "日期字段", + "kbn.discover.fieldNameIcons.geoPointFieldAriaLabel": "地理位置点字段", + "kbn.discover.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段", + "kbn.discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段", + "kbn.discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段", + "kbn.discover.fieldNameIcons.numberFieldAriaLabel": "数字字段", + "kbn.discover.fieldNameIcons.sourceFieldAriaLabel": "源字段", + "kbn.discover.fieldNameIcons.stringFieldAriaLabel": "字符串字段", + "kbn.discover.fieldNameIcons.unknownFieldAriaLabel": "未知字段", "kbn.discover.histogram.partialData.bucketTooltipText": "选定的时间范围不包括此整个存储桶,其可能包含部分数据。", "kbn.discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图", "kbn.discover.hitsPluralTitle": "{hits, plural, one {次命中} other {次命中}}",