From d00adca730204ee3dc049feda74e70211b8cf12e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 13 Feb 2020 09:58:16 +1300 Subject: [PATCH 01/14] fixed flaky test (#57490) --- .../apps/triggers_actions_ui/details.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 938b98591b6a2..ce9160abdb086 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -256,12 +256,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }) ) .forEach(alertInstanceDuration => { - // make sure the duration is within a 2 second range + // make sure the duration is within a 10 second range which is + // good enough as the alert interval is 1m, so we know it is a fresh value expect(alertInstanceDuration.as('milliseconds')).to.greaterThan( - durationFromInstanceTillPageLoad.subtract(1000 * 2).as('milliseconds') + durationFromInstanceTillPageLoad.subtract(1000 * 10).as('milliseconds') ); expect(alertInstanceDuration.as('milliseconds')).to.lessThan( - durationFromInstanceTillPageLoad.add(1000 * 2).as('milliseconds') + durationFromInstanceTillPageLoad.add(1000 * 10).as('milliseconds') ); }); }); From fd193fdf5943914a6e9d19aa3756df1b4819f43e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 13 Feb 2020 10:21:48 +1300 Subject: [PATCH 02/14] [Alerting] make actionGroup name's i18n-able (#57404) We want to make the Action Group i18n-able for display in the AlertDetails page, so instead of just a list of ids, the AlertType now registers an object where key is the id and value is the human readable, and translatable, value. --- x-pack/legacy/plugins/alerting/README.md | 2 +- .../server/alert_type_registry.test.ts | 12 +++++-- .../alerting/server/alerts_client.test.ts | 8 ++--- .../plugins/alerting/server/alerts_client.ts | 5 +-- .../create_execution_handler.test.ts | 5 ++- .../task_runner/create_execution_handler.ts | 4 ++- .../server/task_runner/task_runner.test.ts | 2 +- .../task_runner/task_runner_factory.test.ts | 2 +- .../legacy/plugins/alerting/server/types.ts | 7 +++- .../server/alerts/license_expiration.test.ts | 2 +- .../server/alerts/license_expiration.ts | 10 +++++- .../signals/signal_rule_alert_type.ts | 10 +++++- .../public/application/lib/alert_api.test.ts | 2 +- .../sections/alert_add/alert_form.tsx | 17 ++++++---- .../components/alert_details.test.tsx | 32 +++++++++---------- .../triggers_actions_ui/public/types.ts | 6 +++- .../common/fixtures/plugins/alerts/index.ts | 12 +++++-- .../tests/alerting/list_alert_types.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- .../fixtures/plugins/alerts/index.ts | 7 ++-- 20 files changed, 101 insertions(+), 48 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index eb9df042f9254..2a10c41f12b85 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -85,7 +85,7 @@ The following table describes the properties of the `options` object. |---|---|---| |id|Unique identifier for the alert type. For convention purposes, ids starting with `.` are reserved for built in alert types. We recommend using a convention like `.mySpecialAlert` for your alert types to avoid conflicting with another plugin.|string| |name|A user-friendly name for the alert type. These will be displayed in dropdowns when choosing alert types.|string| -|actionGroups|An explicit list of groups the alert type may schedule actions for. Alert `actions` validation will use this array to ensure groups are valid.|string[]| +|actionGroups|An explicit list of groups the alert type may schedule actions for, each specifying the ActionGroup's unique ID and human readable name. Alert `actions` validation will use this configuartion to ensure groups are valid. We highly encourage using `kbn-i18n` to translate the names of actionGroup when registering the AlertType. |Array<{id:string, name:string}>| |validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema| |executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function| diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts index 1087ee9109885..976bed884cd43 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts @@ -118,7 +118,12 @@ describe('list()', () => { registry.register({ id: 'test', name: 'Test', - actionGroups: ['testActionGroup'], + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], executor: jest.fn(), }); const result = registry.list(); @@ -126,7 +131,10 @@ describe('list()', () => { Array [ Object { "actionGroups": Array [ - "testActionGroup", + Object { + "id": "testActionGroup", + "name": "Test Action Group", + }, ], "id": "test", "name": "Test", diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 38521eea20481..1555a0537158a 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -86,7 +86,7 @@ describe('create()', () => { alertTypeRegistry.get.mockReturnValue({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }); }); @@ -1884,7 +1884,7 @@ describe('update()', () => { alertTypeRegistry.get.mockReturnValue({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }); }); @@ -2414,7 +2414,7 @@ describe('update()', () => { alertTypeRegistry.get.mockReturnValueOnce({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], validate: { params: schema.object({ param1: schema.string(), @@ -2646,7 +2646,7 @@ describe('update()', () => { alertTypeRegistry.get.mockReturnValueOnce({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 334eacc05c771..eef6f662a20a2 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual } from 'lodash'; +import { omit, isEqual, pluck } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -639,8 +639,9 @@ export class AlertsClient { private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map(action => action.group); + const availableAlertTypeActionGroups = new Set(pluck(alertTypeActionGroups, 'id')); const invalidActionGroups = usedAlertActionGroups.filter( - group => !alertTypeActionGroups.includes(group) + group => !availableAlertTypeActionGroups.has(group) ); if (invalidActionGroups.length) { throw Boom.badRequest( diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts index d86a06767c9d1..02fa09ba97a65 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -11,7 +11,10 @@ import { loggingServiceMock } from '../../../../../../src/core/server/mocks'; const alertType: AlertType = { id: 'test', name: 'Test', - actionGroups: ['default', 'other-group'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other-group', name: 'Other Group' }, + ], executor: jest.fn(), }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts index 6b4b47b87b300..737f86a881c1f 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pluck } from 'lodash'; import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; @@ -35,8 +36,9 @@ export function createExecutionHandler({ apiKey, alertType, }: CreateExecutionHandlerOptions) { + const alertTypeActionGroups = new Set(pluck(alertType.actionGroups, 'id')); return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => { - if (!alertType.actionGroups.includes(actionGroup)) { + if (!alertTypeActionGroups.has(actionGroup)) { logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`); return; } diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts index 394c13e1bd24f..b6dd4b3435fcb 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts @@ -19,7 +19,7 @@ import { const alertType = { id: 'test', name: 'My test alert', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], executor: jest.fn(), }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 543b9e7d32e12..7474fcfb4baaa 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -16,7 +16,7 @@ import { const alertType = { id: 'test', name: 'My test alert', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], executor: jest.fn(), }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 5e8adadf74ac0..95a96fa384c2c 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -42,13 +42,18 @@ export interface AlertExecutorOptions { updatedBy: string | null; } +export interface ActionGroup { + id: string; + name: string; +} + export interface AlertType { id: string; name: string; validate?: { params?: { validate: (object: any) => any }; }; - actionGroups: string[]; + actionGroups: ActionGroup[]; executor: ({ services, params, state }: AlertExecutorOptions) => Promise; } diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts index ec00ece9e6ee2..38b4e6c60ca48 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -102,7 +102,7 @@ describe('getLicenseExpiration', () => { 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']); + expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); }); it('should return the state if no license is provided', async () => { diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts index 197c5c9cdcbc7..8688a2b08efc4 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -8,6 +8,7 @@ import moment from 'moment-timezone'; import { get } from 'lodash'; import { Legacy } from 'kibana'; import { Logger } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertType } from '../../../alerting'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; @@ -45,7 +46,14 @@ export const getLicenseExpiration = ( return { id: ALERT_TYPE_LICENSE_EXPIRATION, name: 'Monitoring Alert - License Expiration', - actionGroups: ['default'], + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.monitoring.alerts.licenseExpiration.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], async executor({ services, params, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index cd28f348a27c3..79337aa91b1fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; import { SIGNALS_ID, DEFAULT_MAX_SIGNALS, @@ -32,7 +33,14 @@ export const signalRulesAlertType = ({ return { id: SIGNALS_ID, name: 'SIEM Signals', - actionGroups: ['default'], + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], validate: { params: schema.object({ description: schema.string(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 9c6f4daccc705..93a46862f4cd2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -39,7 +39,7 @@ describe('loadAlertTypes', () => { id: 'test', name: 'Test', actionVariables: ['var1'], - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx index 90b84e11fccd2..1c64f599721d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx @@ -40,6 +40,7 @@ import { ActionTypeIndex, ActionConnector, AlertTypeIndex, + ActionGroup, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from '../action_connector_form/connector_add_modal'; @@ -118,7 +119,7 @@ export const AlertForm = ({ const [alertThrottleUnit, setAlertThrottleUnit] = useState('m'); const [isAddActionPanelOpen, setIsAddActionPanelOpen] = useState(true); const [connectors, setConnectors] = useState([]); - const [defaultActionGroup, setDefaultActionGroup] = useState(undefined); + const [defaultActionGroup, setDefaultActionGroup] = useState(undefined); const [activeActionItem, setActiveActionItem] = useState( undefined ); @@ -158,7 +159,11 @@ export const AlertForm = ({ // temp hack of API result alertTypes.push({ id: 'threshold', - actionGroups: ['Alert', 'Warning', 'If unacknowledged'], + actionGroups: [ + { id: 'alert', name: 'Alert' }, + { id: 'warning', name: 'Warning' }, + { id: 'ifUnacknowledged', name: 'If unacknowledged' }, + ], name: 'threshold', actionVariables: ['ctx.metadata.name', 'ctx.metadata.test'], }); @@ -261,7 +266,7 @@ export const AlertForm = ({ alert.actions.push({ id: '', actionTypeId: actionTypeModel.id, - group: defaultActionGroup ?? 'Alert', + group: defaultActionGroup?.id ?? 'Alert', params: {}, }); setActionProperty('id', freeConnectors[0].id, alert.actions.length - 1); @@ -273,7 +278,7 @@ export const AlertForm = ({ alert.actions.push({ id: '', actionTypeId: actionTypeModel.id, - group: defaultActionGroup ?? 'Alert', + group: defaultActionGroup?.id ?? 'Alert', params: {}, }); setActionProperty('id', alert.actions.length, alert.actions.length - 1); @@ -351,7 +356,7 @@ export const AlertForm = ({ id, })); const actionTypeRegisterd = actionTypeRegistry.get(actionConnector.actionTypeId); - if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup) return null; + if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup?.id) return null; const ParamsFieldsComponent = actionTypeRegisterd.actionParamsFields; const actionParamsErrors: { errors: IErrorObject } = Object.keys(actionsErrors).length > 0 ? actionsErrors[actionItem.id] : { errors: {} }; @@ -474,7 +479,7 @@ export const AlertForm = ({ ? actionTypesIndex[actionItem.actionTypeId].name : actionItem.actionTypeId; const actionTypeRegisterd = actionTypeRegistry.get(actionItem.actionTypeId); - if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup) return null; + if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup?.id) return null; return ( { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -64,7 +64,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -91,7 +91,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -140,7 +140,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; const actionTypes: ActionType[] = [ @@ -190,7 +190,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -214,7 +214,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -238,7 +238,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -267,7 +267,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -292,7 +292,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -317,7 +317,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -351,7 +351,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -388,7 +388,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -414,7 +414,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -440,7 +440,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -475,7 +475,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -510,7 +510,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 30718f702c9cb..86853e88a81cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -70,10 +70,14 @@ export interface ActionConnectorTableItem extends ActionConnector { actionType: ActionType['name']; } +export interface ActionGroup { + id: string; + name: string; +} export interface AlertType { id: string; name: string; - actionGroups: string[]; + actionGroups: ActionGroup[]; actionVariables: string[]; } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index f7f3d0fa91fff..0cc45a624bc1a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -202,7 +202,10 @@ export default function(kibana: any) { const alwaysFiringAlertType: AlertType = { id: 'test.always-firing', name: 'Test: Always Firing', - actionGroups: ['default', 'other'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, @@ -253,7 +256,10 @@ export default function(kibana: any) { const cumulativeFiringAlertType: AlertType = { id: 'test.cumulative-firing', name: 'Test: Cumulative Firing', - actionGroups: ['default', 'other'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, state } = alertExecutorOptions; const group = 'default'; @@ -383,7 +389,7 @@ export default function(kibana: any) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor({ services, params, state }: AlertExecutorOptions) {}, }; server.plugins.alerting.setup.registerType(alwaysFiringAlertType); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 21f61eb713753..517a60f77849e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -40,7 +40,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { (alertType: any) => alertType.id === 'test.noop' ); expect(fixtureAlertType).to.eql({ - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], id: 'test.noop', name: 'Test: Noop', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index efa9dbf507199..55570744f6af9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -20,7 +20,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); expect(fixtureAlertType).to.eql({ - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], id: 'test.noop', name: 'Test: Noop', }); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index 15d1baadf7806..d7551345203b4 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -22,7 +22,7 @@ function createNoopAlertType(setupContract: any) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }; setupContract.registerType(noopAlertType); @@ -33,7 +33,10 @@ function createAlwaysFiringAlertType(setupContract: any) { const alwaysFiringAlertType: any = { id: 'test.always-firing', name: 'Always Firing', - actionGroups: ['default', 'other'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], async executor(alertExecutorOptions: any) { const { services, state, params } = alertExecutorOptions; From be7daa805747a26baea1746ad3b6f3b69a5f71a8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 12 Feb 2020 14:23:47 -0700 Subject: [PATCH 03/14] [Maps] Autocomplete for custom color palettes and custom icon palettes (#56446) * [Maps] type ahead for stop values for custom color maps and custom icon maps * use Popover to show type ahead suggestions * datalist version * use EuiComboBox * clean up * wire ColorStopsCategorical to use StopInput component for autocomplete * clean up * cast suggestion values to string so boolean fields work * review feedback * fix problem with stall suggestions from previous field Co-authored-by: Elastic Machine --- .../plugins/maps/public/kibana_services.js | 1 + .../maps/public/layers/sources/es_source.js | 22 +++ .../maps/public/layers/sources/source.js | 4 + .../components/color/color_map_select.js | 2 + .../vector/components/color/color_stops.js | 11 +- .../color/color_stops_categorical.js | 45 +++--- .../components/color/color_stops_ordinal.js | 13 +- .../components/color/dynamic_color_form.js | 4 +- .../styles/vector/components/stop_input.js | 148 ++++++++++++++++++ .../components/symbol/dynamic_icon_form.js | 1 + .../components/symbol/icon_map_select.js | 3 + .../vector/components/symbol/icon_stops.js | 45 ++++-- .../properties/dynamic_size_property.js | 12 +- .../properties/dynamic_style_property.js | 8 +- .../layers/styles/vector/vector_style.js | 10 +- 15 files changed, 264 insertions(+), 65 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 60fda398b4f3e..a1b1c9ec1518e 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -14,6 +14,7 @@ import { npStart } from 'ui/new_platform'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; export { SearchSource } from '../../../../../src/plugins/data/public'; export const indexPatternService = npStart.plugins.data.indexPatterns; +export const autocompleteService = npStart.plugins.data.autocomplete; let licenseId; export const setLicenseId = latestLicenseId => (licenseId = latestLicenseId); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 26cc7ece66753..d78d3038f870d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -6,6 +6,7 @@ import { AbstractVectorSource } from './vector_source'; import { + autocompleteService, fetchSearchSourceAndRecordWithInspector, indexPatternService, SearchSource, @@ -344,4 +345,25 @@ export class AbstractESSource extends AbstractVectorSource { return resp.aggregations; } + + getValueSuggestions = async (fieldName, query) => { + if (!fieldName) { + return []; + } + + try { + const indexPattern = await this.getIndexPattern(); + const field = indexPattern.fields.getByName(fieldName); + return await autocompleteService.getValueSuggestions({ + indexPattern, + field, + query, + }); + } catch (error) { + console.warn( + `Unable to fetch suggestions for field: ${fieldName}, query: ${query}, error: ${error.message}` + ); + return []; + } + }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index cc5d62bbdfeef..3c6ddb74bedeb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -139,4 +139,8 @@ export class AbstractSource { async loadStylePropsMeta() { throw new Error(`Source#loadStylePropsMeta not implemented`); } + + async getValueSuggestions(/* fieldName, query */) { + return []; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js index fde088ab4475e..e8d5754ef4206 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js @@ -72,6 +72,8 @@ export class ColorMapSelect extends Component { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js index 6b403ff61532d..47c2d037e0c79 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js @@ -59,26 +59,23 @@ export const ColorStops = ({ onChange, colorStops, isStopsInvalid, - sanitizeStopInput, getStopError, renderStopInput, addNewRow, canDeleteStop, }) => { function getStopInput(stop, index) { - const onStopChange = e => { + const onStopChange = newStopValue => { const newColorStops = _.cloneDeep(colorStops); - newColorStops[index].stop = sanitizeStopInput(e.target.value); - const invalid = isStopsInvalid(newColorStops); + newColorStops[index].stop = newStopValue; onChange({ colorStops: newColorStops, - isInvalid: invalid, + isInvalid: isStopsInvalid(newColorStops), }); }; - const error = getStopError(stop, index); return { - stopError: error, + stopError: getStopError(stop, index), stopInput: renderStopInput(stop, onStopChange, index), }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js index d52c3dbcfa1df..124c2bf0cff55 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js @@ -17,18 +17,17 @@ import { import { i18n } from '@kbn/i18n'; import { ColorStops } from './color_stops'; import { getOtherCategoryLabel } from '../../style_util'; +import { StopInput } from '../stop_input'; export const ColorStopsCategorical = ({ colorStops = [ { stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color { stop: '', color: DEFAULT_NEXT_COLOR }, ], + field, onChange, + getValueSuggestions, }) => { - const sanitizeStopInput = value => { - return value; - }; - const getStopError = (stop, index) => { let count = 0; for (let i = 1; i < colorStops.length; i++) { @@ -49,34 +48,23 @@ export const ColorStopsCategorical = ({ if (index === 0) { return ( - ); - } else { - return ( - ); } + + return ( + + ); }; const canDeleteStop = (colorStops, index) => { @@ -88,7 +76,6 @@ export const ColorStopsCategorical = ({ onChange={onChange} colorStops={colorStops} isStopsInvalid={isCategoricalStopsInvalid} - sanitizeStopInput={sanitizeStopInput} getStopError={getStopError} renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} @@ -114,4 +101,8 @@ ColorStopsCategorical.propTypes = { * Callback for when the color stops changes. Called with { colorStops, isInvalid } */ onChange: PropTypes.func.isRequired, + /** + * Callback for fetching stop value suggestions. Called with query. + */ + getValueSuggestions: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js index 61fbb376ad601..0f6a0583d3dbc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js @@ -21,11 +21,6 @@ export const ColorStopsOrdinal = ({ colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }], onChange, }) => { - const sanitizeStopInput = value => { - const sanitizedValue = parseFloat(value); - return isNaN(sanitizedValue) ? '' : sanitizedValue; - }; - const getStopError = (stop, index) => { let error; if (isOrdinalStopInvalid(stop)) { @@ -44,13 +39,18 @@ export const ColorStopsOrdinal = ({ }; const renderStopInput = (stop, onStopChange) => { + function handleOnChangeEvent(event) { + const sanitizedValue = parseFloat(event.target.value); + const newStopValue = isNaN(sanitizedValue) ? '' : sanitizedValue; + onStopChange(newStopValue); + } return ( ); @@ -65,7 +65,6 @@ export const ColorStopsOrdinal = ({ onChange={onChange} colorStops={colorStops} isStopsInvalid={isOrdinalStopsInvalid} - sanitizeStopInput={sanitizeStopInput} getStopError={getStopError} renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index 5491d5d567f84..af5e5b37f5467 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -67,7 +67,7 @@ export function DynamicColorForm({ color={styleOptions.color} customColorMap={styleOptions.customColorRamp} useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)} - compressed + styleProperty={styleProperty} /> ); } @@ -83,7 +83,7 @@ export function DynamicColorForm({ color={styleOptions.colorCategory} customColorMap={styleOptions.customColorPalette} useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)} - compressed + styleProperty={styleProperty} /> ); }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js new file mode 100644 index 0000000000000..d12a3d77d0b29 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js @@ -0,0 +1,148 @@ +/* + * 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 _ from 'lodash'; +import React, { Component } from 'react'; + +import { EuiComboBox, EuiFieldText } from '@elastic/eui'; + +export class StopInput extends Component { + constructor(props) { + super(props); + this.state = { + suggestions: [], + isLoadingSuggestions: false, + hasPrevFocus: false, + fieldDataType: undefined, + localFieldTextValue: props.value, + }; + } + + componentDidMount() { + this._isMounted = true; + this._loadFieldDataType(); + } + + componentWillUnmount() { + this._isMounted = false; + this._loadSuggestions.cancel(); + } + + async _loadFieldDataType() { + const fieldDataType = await this.props.field.getDataType(); + if (this._isMounted) { + this.setState({ fieldDataType }); + } + } + + _onFocus = () => { + if (!this.state.hasPrevFocus) { + this.setState({ hasPrevFocus: true }); + this._onSearchChange(''); + } + }; + + _onChange = selectedOptions => { + this.props.onChange(_.get(selectedOptions, '[0].label', '')); + }; + + _onCreateOption = newValue => { + this.props.onChange(newValue); + }; + + _onSearchChange = async searchValue => { + this.setState( + { + isLoadingSuggestions: true, + searchValue, + }, + () => { + this._loadSuggestions(searchValue); + } + ); + }; + + _loadSuggestions = _.debounce(async searchValue => { + let suggestions = []; + try { + suggestions = await this.props.getValueSuggestions(searchValue); + } catch (error) { + // ignore suggestions error + } + + if (this._isMounted && searchValue === this.state.searchValue) { + this.setState({ + isLoadingSuggestions: false, + suggestions, + }); + } + }, 300); + + _onFieldTextChange = event => { + this.setState({ localFieldTextValue: event.target.value }); + // onChange can cause UI lag, ensure smooth input typing by debouncing onChange + this._debouncedOnFieldTextChange(); + }; + + _debouncedOnFieldTextChange = _.debounce(() => { + this.props.onChange(this.state.localFieldTextValue); + }, 500); + + _renderSuggestionInput() { + const suggestionOptions = this.state.suggestions.map(suggestion => { + return { label: `${suggestion}` }; + }); + + const selectedOptions = []; + if (this.props.value) { + let option = suggestionOptions.find(({ label }) => { + return label === this.props.value; + }); + if (!option) { + option = { label: this.props.value }; + suggestionOptions.unshift(option); + } + selectedOptions.push(option); + } + + return ( + + ); + } + + _renderTextInput() { + return ( + + ); + } + + render() { + if (!this.state.fieldDataType) { + return null; + } + + // autocomplete service can not provide suggestions for non string fields (and boolean) because it uses + // term aggregation include parameter. Include paramerter uses a regular expressions that only supports string type + return this.state.fieldDataType === 'string' || this.state.fieldDataType === 'boolean' + ? this._renderSuggestionInput() + : this._renderTextInput(); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js index 9a0d73cef616c..afa11daf45217 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js @@ -43,6 +43,7 @@ export function DynamicIconForm({ return ( { @@ -23,7 +24,14 @@ const DEFAULT_ICON_STOPS = [ { stop: '', icon: DEFAULT_ICON }, ]; -export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange, symbolOptions }) { +export function IconStops({ + field, + getValueSuggestions, + iconStops = DEFAULT_ICON_STOPS, + isDarkMode, + onChange, + symbolOptions, +}) { return iconStops.map(({ stop, icon }, index) => { const onIconSelect = selectedIconId => { const newIconStops = [...iconStops]; @@ -33,8 +41,7 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange }; onChange({ customMapStops: newIconStops }); }; - const onStopChange = e => { - const newStopValue = e.target.value; + const onStopChange = newStopValue => { const newIconStops = [...iconStops]; newIconStops[index] = { ...iconStops[index], @@ -83,7 +90,24 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange const errors = []; // TODO check for duplicate values and add error messages here - const isOtherCategoryRow = index === 0; + const stopInput = + index === 0 ? ( + + ) : ( + + ); + return (
- - - + {stopInput} { + const fieldName = this.getFieldName(); + return this._source && fieldName ? this._source.getValueSuggestions(fieldName, query) : []; + }; + getFieldMeta() { return this._getFieldMeta && this._field ? this._getFieldMeta(this._field.getName()) : null; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 97259a908f1e4..1f96c37c9d286 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -612,6 +612,7 @@ export class VectorStyle extends AbstractStyle { field, this._getFieldMeta, this._getFieldFormatter, + this._source, isSymbolizedAsIcon ); } else { @@ -631,7 +632,8 @@ export class VectorStyle extends AbstractStyle { styleName, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); @@ -663,7 +665,8 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.LABEL_TEXT, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); @@ -682,7 +685,8 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.ICON, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); From 5f8959c52df77a7304caeb6474f5a95586744b7a Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Wed, 12 Feb 2020 16:54:43 -0500 Subject: [PATCH 04/14] fixing maps (#56706) --- .../search/aggs/buckets/geo_hash.test.ts | 103 +++----------- .../public/search/aggs/buckets/geo_hash.ts | 132 +++--------------- .../search/aggs/buckets/lib/geo_utils.ts | 75 ---------- .../metrics/lib/parent_pipeline_agg_helper.ts | 5 +- .../lib/sibling_pipeline_agg_helper.ts | 7 +- .../public/vis_controller.tsx | 14 +- .../tile_map/public/tile_map_fn.js | 4 + .../tile_map/public/tile_map_visualization.js | 85 ++++++----- .../public/components/table_vis_options.tsx | 6 +- .../vis_type_table/public/components/utils.ts | 16 +-- .../vis_type_table/public/vis_controller.ts | 2 +- .../vis_type_vega/public/vega_fn.ts | 2 +- .../public/embeddable/visualize_embeddable.ts | 31 ++-- .../expressions/visualization_renderer.tsx | 32 ++--- .../ui/public/vis/map/decode_geo_hash.ts | 97 +++++++++++++ 15 files changed, 244 insertions(+), 367 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts index 5ff68c5426e34..f0ad595476486 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -17,9 +17,10 @@ * under the License. */ -import { geoHashBucketAgg, IBucketGeoHashGridAggConfig } from './geo_hash'; +import { geoHashBucketAgg } from './geo_hash'; import { AggConfigs, IAggConfigs } from '../agg_configs'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { IBucketAggConfig } from './_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -77,79 +78,26 @@ describe('Geohash Agg', () => { it('should select precision parameter', () => { expect(precisionParam.name).toEqual('precision'); }); - - describe('precision parameter write', () => { - const zoomToGeoHashPrecision: Record = { - 0: 1, - 1: 2, - 2: 2, - 3: 2, - 4: 3, - 5: 3, - 6: 4, - 7: 4, - 8: 4, - 9: 5, - 10: 5, - 11: 6, - 12: 6, - 13: 6, - 14: 7, - 15: 7, - 16: 8, - 17: 8, - 18: 8, - 19: 9, - 20: 9, - 21: 10, - }; - - Object.keys(zoomToGeoHashPrecision).forEach((zoomLevel: string) => { - it(`zoom level ${zoomLevel} should correspond to correct geohash-precision`, () => { - const aggConfigs = getAggConfigs({ - autoPrecision: true, - mapZoom: zoomLevel, - }); - - const { [BUCKET_TYPES.GEOHASH_GRID]: params } = aggConfigs.aggs[0].toDsl(); - - expect(params.precision).toEqual(zoomToGeoHashPrecision[zoomLevel]); - }); - }); - }); }); describe('getRequestAggs', () => { describe('initial aggregation creation', () => { let aggConfigs: IAggConfigs; - let geoHashGridAgg: IBucketGeoHashGridAggConfig; + let geoHashGridAgg: IBucketAggConfig; beforeEach(() => { aggConfigs = getAggConfigs(); - geoHashGridAgg = aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig; + geoHashGridAgg = aggConfigs.aggs[0] as IBucketAggConfig; }); it('should create filter, geohash_grid, and geo_centroid aggregations', () => { - const requestAggs = geoHashBucketAgg.getRequestAggs( - geoHashGridAgg - ) as IBucketGeoHashGridAggConfig[]; + const requestAggs = geoHashBucketAgg.getRequestAggs(geoHashGridAgg) as IBucketAggConfig[]; expect(requestAggs.length).toEqual(3); expect(requestAggs[0].type.name).toEqual('filter'); expect(requestAggs[1].type.name).toEqual('geohash_grid'); expect(requestAggs[2].type.name).toEqual('geo_centroid'); }); - - it('should set mapCollar in vis session state', () => { - const [, geoHashAgg] = geoHashBucketAgg.getRequestAggs( - geoHashGridAgg - ) as IBucketGeoHashGridAggConfig[]; - - expect(geoHashAgg).toHaveProperty('lastMapCollar'); - expect(geoHashAgg.lastMapCollar).toHaveProperty('top_left'); - expect(geoHashAgg.lastMapCollar).toHaveProperty('bottom_right'); - expect(geoHashAgg.lastMapCollar).toHaveProperty('zoom'); - }); }); }); @@ -157,8 +105,8 @@ describe('Geohash Agg', () => { it('should only create geohash_grid and geo_centroid aggregations when isFilteredByCollar is false', () => { const aggConfigs = getAggConfigs({ isFilteredByCollar: false }); const requestAggs = geoHashBucketAgg.getRequestAggs( - aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + aggConfigs.aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(requestAggs.length).toEqual(2); expect(requestAggs[0].type.name).toEqual('geohash_grid'); @@ -168,8 +116,8 @@ describe('Geohash Agg', () => { it('should only create filter and geohash_grid aggregations when useGeocentroid is false', () => { const aggConfigs = getAggConfigs({ useGeocentroid: false }); const requestAggs = geoHashBucketAgg.getRequestAggs( - aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + aggConfigs.aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(requestAggs.length).toEqual(2); expect(requestAggs[0].type.name).toEqual('filter'); @@ -178,23 +126,28 @@ describe('Geohash Agg', () => { }); describe('aggregation creation after map interaction', () => { - let originalRequestAggs: IBucketGeoHashGridAggConfig[]; + let originalRequestAggs: IBucketAggConfig[]; beforeEach(() => { originalRequestAggs = geoHashBucketAgg.getRequestAggs( - getAggConfigs().aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + getAggConfigs({ + boundingBox: { + top_left: { lat: 1, lon: -1 }, + bottom_right: { lat: -1, lon: 1 }, + }, + }).aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; }); it('should change geo_bounding_box filter aggregation and vis session state when map movement is outside map collar', () => { const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( getAggConfigs({ - mapBounds: { + boundingBox: { top_left: { lat: 10.0, lon: -10.0 }, bottom_right: { lat: 9.0, lon: -9.0 }, }, - }).aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + }).aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(originalRequestAggs[1].params).not.toEqual(geoBoxingBox.params); }); @@ -202,24 +155,14 @@ describe('Geohash Agg', () => { it('should not change geo_bounding_box filter aggregation and vis session state when map movement is within map collar', () => { const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( getAggConfigs({ - mapBounds: { + boundingBox: { top_left: { lat: 1, lon: -1 }, bottom_right: { lat: -1, lon: 1 }, }, - }).aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + }).aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(originalRequestAggs[1].params).toEqual(geoBoxingBox.params); }); - - it('should change geo_bounding_box filter aggregation and vis session state when map zoom level changes', () => { - const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( - getAggConfigs({ - mapZoom: -1, - }).aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; - - expect(originalRequestAggs[1].lastMapCollar).not.toEqual(geoBoxingBox.lastMapCollar); - }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts index afd4e18dd266c..8732f926b0fb2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts @@ -18,69 +18,22 @@ */ import { i18n } from '@kbn/i18n'; -import { geohashColumns } from 'ui/vis/map/decode_geo_hash'; -import chrome from 'ui/chrome'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; - -import { geoContains, scaleBounds, GeoBoundingBox } from './lib/geo_utils'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { AggGroupNames } from '../agg_groups'; -const config = chrome.getUiSettingsClient(); +const defaultBoundingBox = { + top_left: { lat: 1, lon: 1 }, + bottom_right: { lat: 0, lon: 0 }, +}; const defaultPrecision = 2; -const maxPrecision = parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12; -/** - * Map Leaflet zoom levels to geohash precision levels. - * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. - */ -const zoomPrecision: any = {}; -const minGeohashPixels = 16; - -for (let zoom = 0; zoom <= 21; zoom += 1) { - const worldPixels = 256 * Math.pow(2, zoom); - zoomPrecision[zoom] = 1; - for (let precision = 2; precision <= maxPrecision; precision += 1) { - const columns = geohashColumns(precision); - if (worldPixels / columns >= minGeohashPixels) { - zoomPrecision[zoom] = precision; - } else { - break; - } - } -} - -function getPrecision(val: string) { - let precision = parseInt(val, 10); - - if (Number.isNaN(precision)) { - precision = defaultPrecision; - } - - if (precision > maxPrecision) { - return maxPrecision; - } - - return precision; -} - -const isOutsideCollar = (bounds: GeoBoundingBox, collar: MapCollar) => - bounds && collar && !geoContains(collar, bounds); const geohashGridTitle = i18n.translate('data.search.aggs.buckets.geohashGridTitle', { defaultMessage: 'Geohash', }); -interface MapCollar extends GeoBoundingBox { - zoom?: unknown; -} - -export interface IBucketGeoHashGridAggConfig extends IBucketAggConfig { - lastMapCollar: MapCollar; -} - -export const geoHashBucketAgg = new BucketAggType({ +export const geoHashBucketAgg = new BucketAggType({ name: BUCKET_TYPES.GEOHASH_GRID, title: geohashGridTitle, params: [ @@ -97,13 +50,8 @@ export const geoHashBucketAgg = new BucketAggType({ { name: 'precision', default: defaultPrecision, - deserialize: getPrecision, write(aggConfig, output) { - const currZoom = aggConfig.params.mapZoom; - const autoPrecisionVal = zoomPrecision[currZoom]; - output.params.precision = aggConfig.params.autoPrecision - ? autoPrecisionVal - : getPrecision(aggConfig.params.precision); + output.params.precision = aggConfig.params.precision; }, }, { @@ -117,17 +65,7 @@ export const geoHashBucketAgg = new BucketAggType({ write: () => {}, }, { - name: 'mapZoom', - default: 2, - write: () => {}, - }, - { - name: 'mapCenter', - default: [0, 0], - write: () => {}, - }, - { - name: 'mapBounds', + name: 'boundingBox', default: null, write: () => {}, }, @@ -137,46 +75,22 @@ export const geoHashBucketAgg = new BucketAggType({ const params = agg.params; if (params.isFilteredByCollar && agg.getField()) { - const { mapBounds, mapZoom } = params; - if (mapBounds) { - let mapCollar: MapCollar; - - if ( - mapBounds && - (!agg.lastMapCollar || - agg.lastMapCollar.zoom !== mapZoom || - isOutsideCollar(mapBounds, agg.lastMapCollar)) - ) { - mapCollar = scaleBounds(mapBounds); - mapCollar.zoom = mapZoom; - agg.lastMapCollar = mapCollar; - } else { - mapCollar = agg.lastMapCollar; - } - const boundingBox = { - ignore_unmapped: true, - [agg.getField().name]: { - top_left: mapCollar.top_left, - bottom_right: mapCollar.bottom_right, - }, - }; - aggs.push( - agg.aggConfigs.createAggConfig( - { - type: 'filter', - id: 'filter_agg', - enabled: true, - params: { - geo_bounding_box: boundingBox, - }, - schema: { - group: AggGroupNames.Buckets, + aggs.push( + agg.aggConfigs.createAggConfig( + { + type: 'filter', + id: 'filter_agg', + enabled: true, + params: { + geo_bounding_box: { + ignore_unmapped: true, + [agg.getField().name]: params.boundingBox || defaultBoundingBox, }, - } as any, - { addToAggConfigs: false } - ) - ); - } + }, + } as any, + { addToAggConfigs: false } + ) + ); } aggs.push(agg); @@ -196,6 +110,6 @@ export const geoHashBucketAgg = new BucketAggType({ ); } - return aggs as IBucketGeoHashGridAggConfig[]; + return aggs; }, }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts deleted file mode 100644 index 639b6d1fbb03e..0000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts +++ /dev/null @@ -1,75 +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'; - -interface GeoBoundingBoxCoordinate { - lat: number; - lon: number; -} - -export interface GeoBoundingBox { - top_left: GeoBoundingBoxCoordinate; - bottom_right: GeoBoundingBoxCoordinate; -} - -export function geoContains(collar: GeoBoundingBox, bounds: GeoBoundingBox) { - // test if bounds top_left is outside collar - if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { - return false; - } - - // test if bounds bottom_right is outside collar - if ( - bounds.bottom_right.lat < collar.bottom_right.lat || - bounds.bottom_right.lon > collar.bottom_right.lon - ) { - return false; - } - - // both corners are inside collar so collar contains bounds - return true; -} - -export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { - const scale = 0.5; // scale bounds by 50% - - const topLeft = bounds.top_left; - const bottomRight = bounds.bottom_right; - let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); - const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); - // map height can be zero when vis is first created - if (latDiff === 0) latDiff = lonDiff; - - const latDelta = latDiff * scale; - let topLeftLat = _.round(topLeft.lat, 5) + latDelta; - if (topLeftLat > 90) topLeftLat = 90; - let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; - if (bottomRightLat < -90) bottomRightLat = -90; - const lonDelta = lonDiff * scale; - let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; - if (topLeftLon < -180) topLeftLon = -180; - let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; - if (bottomRightLon > 180) bottomRightLon = 180; - - return { - top_left: { lat: topLeftLat, lon: topLeftLon }, - bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, - }; -} diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 0d1b2472bb8e2..e24aca08271c7 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -18,13 +18,14 @@ */ import { i18n } from '@kbn/i18n'; -import { noop } from 'lodash'; +import { noop, identity } from 'lodash'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; import { Schemas } from '../../schemas'; +import { fieldFormats } from '../../../../../../../../plugins/data/public'; const metricAggFilter = [ '!top_hits', @@ -100,7 +101,7 @@ const parentPipelineAggHelper = { } else { subAgg = agg.aggConfigs.byId(agg.getParam('metricAgg')); } - return subAgg.type.getFormat(subAgg); + return subAgg ? subAgg.type.getFormat(subAgg) : new (fieldFormats.FieldFormat.from(identity))(); }, }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index 3956bda1812ad..e7c98e575fdb4 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -17,12 +17,14 @@ * under the License. */ +import { identity } from 'lodash'; import { i18n } from '@kbn/i18n'; import { siblingPipelineAggWriter } from './sibling_pipeline_agg_writer'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { Schemas } from '../../schemas'; +import { fieldFormats } from '../../../../../../../../plugins/data/public'; const metricAggFilter: string[] = [ '!top_hits', @@ -115,8 +117,9 @@ const siblingPipelineAggHelper = { getFormat(agg: IMetricAggConfig) { const customMetric = agg.getParam('customMetric'); - - return customMetric.type.getFormat(customMetric); + return customMetric + ? customMetric.type.getFormat(customMetric) + : new (fieldFormats.FieldFormat.from(identity))(); }, }; diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx index 4ceffbfc1c197..624d000dd8d7a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx @@ -55,14 +55,12 @@ export const createInputControlVisController = (deps: InputControlVisDependencie } async render(visData: any, visParams: VisParams, status: any) { - if (status.params || (visParams.useTimeFilter && status.time)) { - this.visParams = visParams; - this.controls = []; - this.controls = await this.initControls(); - const [{ i18n }] = await deps.core.getStartServices(); - this.I18nContext = i18n.Context; - this.drawVis(); - } + this.visParams = visParams; + this.controls = []; + this.controls = await this.initControls(); + const [{ i18n }] = await deps.core.getStartServices(); + this.I18nContext = i18n.Context; + this.drawVis(); } destroy() { diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js b/src/legacy/core_plugins/tile_map/public/tile_map_fn.js index f5cb6fdf93002..2f54d23590c33 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_fn.js @@ -43,6 +43,10 @@ export const createTileMapFn = () => ({ geocentroid, }); + if (geohash && geohash.accessor) { + convertedData.meta.geohash = context.columns[geohash.accessor].meta; + } + return { type: 'render', as: 'visualization', diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js index 772edaa4ff4f5..910def8a0c78e 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js @@ -23,6 +23,12 @@ import { BaseMapsVisualizationProvider } from './base_maps_visualization'; import { TileMapTooltipFormatterProvider } from './editors/_tooltip_formatter'; import { npStart } from 'ui/new_platform'; import { getFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; +import { + scaleBounds, + zoomPrecision, + getPrecision, + geoContains, +} from '../../../ui/public/vis/map/decode_geo_hash'; export const createTileMapVisualization = ({ serviceSettings, $injector }) => { const BaseMapsVisualization = new BaseMapsVisualizationProvider(serviceSettings); @@ -35,42 +41,47 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { this._geohashLayer = null; } + updateGeohashAgg = () => { + const geohashAgg = this._getGeoHashAgg(); + if (!geohashAgg) return; + const updateVarsObject = { + name: 'bounds', + data: {}, + }; + const bounds = this._kibanaMap.getBounds(); + const mapCollar = scaleBounds(bounds); + if (!geoContains(geohashAgg.aggConfigParams.boundingBox, mapCollar)) { + updateVarsObject.data.boundingBox = { + top_left: mapCollar.top_left, + bottom_right: mapCollar.bottom_right, + }; + } else { + updateVarsObject.data.boundingBox = geohashAgg.aggConfigParams.boundingBox; + } + // todo: autoPrecision should be vis parameter, not aggConfig one + updateVarsObject.data.precision = geohashAgg.aggConfigParams.autoPrecision + ? zoomPrecision[this.vis.getUiState().get('mapZoom')] + : getPrecision(geohashAgg.aggConfigParams.precision); + + this.vis.eventsSubject.next(updateVarsObject); + }; + async _makeKibanaMap() { await super._makeKibanaMap(); - const updateGeohashAgg = () => { - const geohashAgg = this._getGeoHashAgg(); - if (!geohashAgg) return; - geohashAgg.params.mapBounds = this._kibanaMap.getBounds(); - geohashAgg.params.mapZoom = this._kibanaMap.getZoomLevel(); - geohashAgg.params.mapCenter = this._kibanaMap.getCenter(); - }; - - updateGeohashAgg(); + let previousPrecision = this._kibanaMap.getGeohashPrecision(); + let precisionChange = false; const uiState = this.vis.getUiState(); uiState.on('change', prop => { if (prop === 'mapZoom' || prop === 'mapCenter') { - updateGeohashAgg(); + this.updateGeohashAgg(); } }); - let previousPrecision = this._kibanaMap.getGeohashPrecision(); - let precisionChange = false; this._kibanaMap.on('zoomchange', () => { - const geohashAgg = this._getGeoHashAgg(); precisionChange = previousPrecision !== this._kibanaMap.getGeohashPrecision(); previousPrecision = this._kibanaMap.getGeohashPrecision(); - if (!geohashAgg) { - return; - } - const isAutoPrecision = - typeof geohashAgg.params.autoPrecision === 'boolean' - ? geohashAgg.params.autoPrecision - : true; - if (isAutoPrecision) { - geohashAgg.params.precision = previousPrecision; - } }); this._kibanaMap.on('zoomend', () => { const geohashAgg = this._getGeoHashAgg(); @@ -78,15 +89,14 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } const isAutoPrecision = - typeof geohashAgg.params.autoPrecision === 'boolean' - ? geohashAgg.params.autoPrecision + typeof geohashAgg.aggConfigParams.autoPrecision === 'boolean' + ? geohashAgg.aggConfigParams.autoPrecision : true; if (!isAutoPrecision) { return; } if (precisionChange) { - updateGeohashAgg(); - this.vis.updateState(); + this.updateGeohashAgg(); } else { //when we filter queries by collar this._updateData(this._geoJsonFeatureCollectionAndMeta); @@ -126,6 +136,14 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } + if ( + !this._geoJsonFeatureCollectionAndMeta || + !geojsonFeatureCollectionAndMeta.featureCollection.features.length + ) { + this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta; + this.updateGeohashAgg(); + } + this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta; this._recreateGeohashLayer(); } @@ -181,7 +199,6 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { tooltipFormatter: this._geoJsonFeatureCollectionAndMeta ? boundTooltipFormatter : null, mapType: newParams.mapType, isFilteredByCollar: this._isFilteredByCollar(), - fetchBounds: () => this.vis.API.getGeohashBounds(), // TODO: Remove this (elastic/kibana#30593) colorRamp: newParams.colorSchema, heatmap: { heatClusterSize: newParams.heatClusterSize, @@ -194,8 +211,8 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } - const indexPatternName = agg.getIndexPattern().id; - const field = agg.fieldName(); + const indexPatternName = agg.indexPatternId; + const field = agg.aggConfigParams.field; const filter = { meta: { negate: false, index: indexPatternName } }; filter[filterName] = { ignore_unmapped: true }; filter[filterName][field] = filterData; @@ -207,16 +224,16 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { } _getGeoHashAgg() { - return this.vis.getAggConfig().aggs.find(agg => { - return get(agg, 'type.dslName') === 'geohash_grid'; - }); + return ( + this._geoJsonFeatureCollectionAndMeta && this._geoJsonFeatureCollectionAndMeta.meta.geohash + ); } _isFilteredByCollar() { const DEFAULT = false; const agg = this._getGeoHashAgg(); if (agg) { - return get(agg, 'params.isFilteredByCollar', DEFAULT); + return get(agg, 'aggConfigParams.isFilteredByCollar', DEFAULT); } else { return DEFAULT; } diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 72838d2d97421..5729618b6ae07 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -17,8 +17,8 @@ * under the License. */ -import React, { useEffect, useMemo } from 'react'; import { get } from 'lodash'; +import React, { useEffect, useMemo } from 'react'; import { EuiIconTip, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -27,7 +27,7 @@ import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/publ import { tabifyGetColumns } from '../legacy_imports'; import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; import { TableVisParams } from '../types'; -import { totalAggregations, isAggConfigNumeric } from './utils'; +import { totalAggregations } from './utils'; function TableOptions({ aggs, @@ -44,7 +44,7 @@ function TableOptions({ }), }, ...tabifyGetColumns(aggs.getResponseAggs(), true) - .filter(col => isAggConfigNumeric(get(col, 'aggConfig.type.name'), stateParams.dimensions)) + .filter(col => get(col.aggConfig.type.getFormat(col.aggConfig), 'type.id') === 'number') .map(({ name }) => ({ value: name, text: name })), ], [aggs, stateParams.percentageCol, stateParams.dimensions] diff --git a/src/legacy/core_plugins/vis_type_table/public/components/utils.ts b/src/legacy/core_plugins/vis_type_table/public/components/utils.ts index 365566503e25b..b97c7ccbac0f7 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/utils.ts +++ b/src/legacy/core_plugins/vis_type_table/public/components/utils.ts @@ -17,20 +17,8 @@ * under the License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { AggTypes, Dimensions } from '../types'; - -function isAggConfigNumeric( - type: AggTypes, - { buckets, metrics }: Dimensions = { buckets: [], metrics: [] } -) { - const dimension = - buckets.find(({ aggType }) => aggType === type) || - metrics.find(({ aggType }) => aggType === type); - const formatType = get(dimension, 'format.id') || get(dimension, 'format.params.id'); - return formatType === 'number'; -} +import { AggTypes } from '../types'; const totalAggregations = [ { @@ -65,4 +53,4 @@ const totalAggregations = [ }, ]; -export { isAggConfigNumeric, totalAggregations }; +export { totalAggregations }; diff --git a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts index a792fc98842f1..2d27a99bdd8af 100644 --- a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts +++ b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts @@ -74,7 +74,7 @@ export class TableVisualizationController { return; } this.$scope.vis = this.vis; - this.$scope.visState = this.vis.getState(); + this.$scope.visState = { params: visParams }; this.$scope.esResponse = esResponse; if (!isEqual(this.$scope.visParams, visParams)) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts index afb476472a273..2a0da81a31a96 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts @@ -73,7 +73,7 @@ export const createVegaFn = ( as: 'visualization', value: { visData: response, - visType: name, + visType: 'vega', visConfig: { spec: args.spec, }, diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 126e9d769f0a2..5e593398333c9 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import _, { get } from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; @@ -46,7 +46,6 @@ import { import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; import { Vis } from '../np_ready/public'; -import { queryGeohashBounds } from './query_geohash_bounds'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -253,17 +252,6 @@ export class VisualizeEmbeddable extends Embeddable { - return queryGeohashBounds(this.savedVisualization.vis, { - filters: this.filters, - query: this.query, - searchSource: this.savedVisualization.searchSource, - }); - }; - // this is a hack to make editor still work, will be removed once we clean up editor this.vis.hasInspector = () => { const visTypesWithoutInspector = [ @@ -290,6 +278,22 @@ export class VisualizeEmbeddable extends Embeddable { + // maps hack, remove once esaggs function is cleaned up and ready to accept variables + if (event.name === 'bounds') { + const agg = this.vis.getAggConfig().aggs.find((a: any) => { + return get(a, 'type.dslName') === 'geohash_grid'; + }); + if ( + agg.params.precision !== event.data.precision || + !_.isEqual(agg.params.boundingBox, event.data.boundingBox) + ) { + agg.params.boundingBox = event.data.boundingBox; + agg.params.precision = event.data.precision; + this.reload(); + } + return; + } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; npStart.plugins.uiActions.executeTriggerActions(eventName, { @@ -355,7 +359,6 @@ export class VisualizeEmbeddable extends Embeddable ({ render: async (domNode: HTMLElement, config: any, handlers: any) => { const { visData, visConfig, params } = config; const visType = config.visType || visConfig.type; - const $injector = await legacyChrome.dangerouslyGetActiveInjector(); - const $rootScope = $injector.get('$rootScope') as any; - if (handlers.vis) { - // special case in visualize, we need to render first (without executing the expression), for maps to work - if (visConfig) { - $rootScope.$apply(() => { - handlers.vis.setCurrentState({ - type: visType, - params: visConfig, - title: handlers.vis.title, - }); - }); - } - } else { - handlers.vis = new Vis({ - type: visType, - params: visConfig, - }); - } + const vis = new Vis({ + type: visType, + params: visConfig, + }); - handlers.vis.eventsSubject = { next: handlers.event }; + vis.eventsSubject = { next: handlers.event }; - const uiState = handlers.uiState || handlers.vis.getUiState(); + const uiState = handlers.uiState || vis.getUiState(); handlers.onDestroy(() => { unmountComponentAtNode(domNode); @@ -63,9 +47,9 @@ export const visualization = () => ({ const listenOnChange = params ? params.listenOnChange : false; render( = minGeohashPixels) { + zoomPrecision[zoom] = precision; + } else { + break; + } + } +} + +export function getPrecision(val: string) { + let precision = parseInt(val, 10); + + if (Number.isNaN(precision)) { + precision = defaultPrecision; + } + + if (precision > maxPrecision) { + return maxPrecision; + } + + return precision; +} + +interface GeoBoundingBoxCoordinate { + lat: number; + lon: number; +} + +interface GeoBoundingBox { + top_left: GeoBoundingBoxCoordinate; + bottom_right: GeoBoundingBoxCoordinate; +} + +export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { + const scale = 0.5; // scale bounds by 50% + + const topLeft = bounds.top_left; + const bottomRight = bounds.bottom_right; + let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); + const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); + // map height can be zero when vis is first created + if (latDiff === 0) latDiff = lonDiff; + + const latDelta = latDiff * scale; + let topLeftLat = _.round(topLeft.lat, 5) + latDelta; + if (topLeftLat > 90) topLeftLat = 90; + let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; + if (bottomRightLat < -90) bottomRightLat = -90; + const lonDelta = lonDiff * scale; + let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; + if (topLeftLon < -180) topLeftLon = -180; + let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; + if (bottomRightLon > 180) bottomRightLon = 180; + + return { + top_left: { lat: topLeftLat, lon: topLeftLon }, + bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, + }; +} + +export function geoContains(collar?: GeoBoundingBox, bounds?: GeoBoundingBox) { + if (!bounds || !collar) return false; + // test if bounds top_left is outside collar + if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { + return false; + } + + // test if bounds bottom_right is outside collar + if ( + bounds.bottom_right.lat < collar.bottom_right.lat || + bounds.bottom_right.lon > collar.bottom_right.lon + ) { + return false; + } + + // both corners are inside collar so collar contains bounds + return true; +} From a5d661c7f4e7cbaffc3efd0cb6b1267c354cbd6b Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 12 Feb 2020 16:18:59 -0600 Subject: [PATCH 05/14] Management Api - add to migration guide (#56892) * update management info in migration guide --- src/core/MIGRATION.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f8699364fa9e2..c942bddc9fd57 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1163,6 +1163,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `import 'ui/management'` | `management.sections` | | | `import 'ui/apply_filters'` | N/A. Replaced by triggering an APPLY_FILTER_TRIGGER trigger. | Directive is deprecated. | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | @@ -1240,7 +1241,7 @@ This table shows where these uiExports have moved to in the New Platform. In mos | `inspectorViews` | | Should be an API on the data (?) plugin. | | `interpreter` | | Should be an API on the interpreter plugin. | | `links` | n/a | Not necessary, just register your app via `core.application.register` | -| `managementSections` | [`plugins.management.sections.register`](/rfcs/text/0006_management_section_service.md) | API finalized, implementation in progress. | +| `managementSections` | [`plugins.management.sections.register`](/rfcs/text/0006_management_section_service.md) | | | `mappings` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | | `migrations` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | | `navbarExtensions` | n/a | Deprecated | From 0faab4aa48509fac2e5a4de3469b594df67f947f Mon Sep 17 00:00:00 2001 From: Andrea Del Rio Date: Wed, 12 Feb 2020 14:58:15 -0800 Subject: [PATCH 06/14] [Alerting] Create alert design cleanup (#56929) --- .../threshold/expression.tsx | 27 +++++++++++++++++-- .../threshold/visualization.tsx | 3 +-- .../action_connector_form/_index.scss | 4 +++ .../sections/alert_add/alert_form.tsx | 25 ++++++++++------- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 1708b2f0ae016..f7d2b8f60157f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexItem, EuiFlexGroup, + EuiFormLabel, EuiExpression, EuiPopover, EuiPopoverTitle, @@ -327,7 +328,15 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent ) : null} - + + + + + + { setIndexPopoverOpen(true); @@ -370,6 +379,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent + + ) : null} + + + + + + + + + = ({ > {error} - ); } @@ -248,7 +247,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
{alertVisualizationDataKeys.length ? ( - + +
@@ -609,17 +610,20 @@ export const AlertForm = ({ {canChangeTrigger ? ( - { setAlertProperty('alertTypeId', null); setAlertTypeModel(null); }} - > - - + /> ) : null} @@ -636,7 +640,7 @@ export const AlertForm = ({ {selectedGroupActions} {isAddActionPanelOpen ? ( - +
{alertTypeDetails} ) : ( +
Date: Wed, 12 Feb 2020 17:38:18 -0600 Subject: [PATCH 07/14] Use default spaces suffix for signals index if spaces disabled (#57244) Addresses #57221. Co-authored-by: Elastic Machine --- .../lib/detection_engine/routes/utils.test.ts | 33 +++++++++++++++++++ .../lib/detection_engine/routes/utils.ts | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 3e3ccfe5babef..2699f687c5106 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -11,6 +11,7 @@ import { transformBulkError, BulkError, createSuccessObject, + getIndex, ImportSuccessError, createImportErrorObject, transformImportError, @@ -292,4 +293,36 @@ describe('utils', () => { expect(transformed).toEqual(expected); }); }); + + describe('getIndex', () => { + it('appends the space ID to the configured index if spaces are enabled', () => { + const mockGet = jest.fn(); + const mockGetSpaceId = jest.fn(); + const config = jest.fn(() => ({ get: mockGet, has: jest.fn() })); + const server = { plugins: { spaces: { getSpaceId: mockGetSpaceId } }, config }; + + mockGet.mockReturnValue('mockSignalsIndex'); + mockGetSpaceId.mockReturnValue('myspace'); + // @ts-ignore-next-line TODO these dependencies are simplified on + // https://github.com/elastic/kibana/pull/56814. We're currently mocking + // out what we need. + const index = getIndex(null, server); + + expect(index).toEqual('mockSignalsIndex-myspace'); + }); + + it('appends the default space ID to the configured index if spaces are disabled', () => { + const mockGet = jest.fn(); + const config = jest.fn(() => ({ get: mockGet, has: jest.fn() })); + const server = { plugins: {}, config }; + + mockGet.mockReturnValue('mockSignalsIndex'); + // @ts-ignore-next-line TODO these dependencies are simplified on + // https://github.com/elastic/kibana/pull/56814. We're currently mocking + // out what we need. + const index = getIndex(null, server); + + expect(index).toEqual('mockSignalsIndex-default'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index af78f60f16ae4..20871e5309c30 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -178,7 +178,7 @@ export const getIndex = ( request: RequestFacade | Omit, server: ServerFacade ): string => { - const spaceId = server.plugins.spaces.getSpaceId(request); + const spaceId = server.plugins.spaces?.getSpaceId?.(request) ?? 'default'; const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); return `${signalsIndex}-${spaceId}`; }; From bc8a41a8868fcd38d8467219872a7805b59f8b5a Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Wed, 12 Feb 2020 16:02:11 -0800 Subject: [PATCH 08/14] Add autocomplete="off" for input type="password" to appease the scanners (#56922) * Add autocomplete="off" for input type="password" to appease the scanners * Using new-password instead of off for the new/confirm passwords * Setting more autoComplete="new-password" attributes Co-authored-by: Elastic Machine --- .../__snapshots__/basic_login_form.test.tsx.snap | 1 + .../login/components/basic_login_form/basic_login_form.tsx | 1 + .../components/change_password_form/change_password_form.tsx | 3 +++ .../public/management/users/edit_user/edit_user_page.tsx | 2 ++ 4 files changed, 7 insertions(+) diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap index 9814a2a7784fc..b09f398ed5ed9 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap @@ -64,6 +64,7 @@ exports[`BasicLoginForm renders as expected 1`] = ` > { } > { } > { } > { } > { {...this.validator.validatePassword(this.state.user)} > { {...this.validator.validateConfirmPassword(this.state.user)} > Date: Wed, 12 Feb 2020 21:13:43 -0500 Subject: [PATCH 09/14] [SIEM] [Detection Engine] Reject if duplicate rule_id in request payload (#57057) * prevents creation of rules when duplicate rule_id is present * adds unit test to reflect change * genericizes duplicate discovery functions, allows creation of non-duplicated rules even when duplicates are discovered, keeps same return type signature, updates relevant test for duplicates in request payload * utilizes countBy and removes reduce in favor of a filter on getDuplicates function * fix type * removes skip from e2e test for duplicates on bulk create, updates expected response in e2e test, fixes bug where duplicated error messages appeared for each instance of a duplicated rule_id (found this one through the e2e tests)! Adds unit test to catch this case. * getDuplicate returns empty array instead of null, removes unnecessary return logic * removes null coalescing from includes in filter Co-authored-by: Elastic Machine --- .../rules/create_rules_bulk_route.test.ts | 32 ++++ .../routes/rules/create_rules_bulk_route.ts | 159 ++++++++++-------- .../routes/rules/utils.test.ts | 22 +++ .../detection_engine/routes/rules/utils.ts | 9 + .../tests/create_rules_bulk.ts | 10 +- 5 files changed, 156 insertions(+), 76 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 5cf6d8955d8b2..f1169442484c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -20,6 +20,8 @@ import { } from '../__mocks__/request_responses'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesBulkRoute } from './create_rules_bulk_route'; +import { BulkError } from '../utils'; +import { OutputRuleAlertRest } from '../../types'; describe('create_rules_bulk', () => { let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); @@ -124,4 +126,34 @@ describe('create_rules_bulk', () => { expect(statusCode).toBe(400); }); }); + + test('returns 409 if duplicate rule_ids found in request payload', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload(), typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array> = JSON.parse(payload); + expect(output.some(item => item.error?.status_code === 409)).toBeTruthy(); + }); + + test('returns one error object in response when duplicate rule_ids found in request payload', async () => { + alertsClient.find.mockResolvedValue(getFindResult()); + alertsClient.get.mockResolvedValue(getResult()); + actionsClient.create.mockResolvedValue(createActionResult()); + alertsClient.create.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload(), typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array> = JSON.parse(payload); + expect(output.length).toBe(1); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 0ffa61e2e2bed..e7145d2a6f055 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,14 +5,14 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; +import { isFunction, countBy } from 'lodash/fp'; import uuid from 'uuid'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; import { BulkRulesRequest } from '../../rules/types'; import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; -import { transformOrBulkError } from './utils'; +import { transformOrBulkError, getDuplicates } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; import { callWithRequestFactory, @@ -48,94 +48,109 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou return headers.response().code(404); } + const ruleDefinitions = request.payload; + const mappedDuplicates = countBy('rule_id', ruleDefinitions); + const dupes = getDuplicates(mappedDuplicates); + const rules = await Promise.all( - request.payload.map(async payloadRule => { - const { - description, - enabled, - false_positives: falsePositives, - from, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = payloadRule; - const ruleIdOrUuid = ruleId ?? uuid.v4(); - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); - if (!indexExists) { - return createBulkErrorObject({ - ruleId: ruleIdOrUuid, - statusCode: 400, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, - }); - } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId }); - if (rule != null) { - return createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }); - } - } - const createdRule = await createRules({ - alertsClient, - actionsClient, + ruleDefinitions + .filter(rule => rule.rule_id == null || !dupes.includes(rule.rule_id)) + .map(async payloadRule => { + const { description, enabled, - falsePositives, + false_positives: falsePositives, from, - immutable: false, query, language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, + output_index: outputIndex, + saved_id: savedId, meta, filters, - ruleId: ruleIdOrUuid, + rule_id: ruleId, index, interval, - maxSignals, - riskScore, + max_signals: maxSignals, + risk_score: riskScore, name, severity, tags, + threat, to, type, - threat, references, + timeline_id: timelineId, + timeline_title: timelineTitle, version, - }); - return transformOrBulkError(ruleIdOrUuid, createdRule); - } catch (err) { - return transformBulkError(ruleIdOrUuid, err); - } - }) + } = payloadRule; + const ruleIdOrUuid = ruleId ?? uuid.v4(); + try { + const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const callWithRequest = callWithRequestFactory(request, server); + const indexExists = await getIndexExists(callWithRequest, finalIndex); + if (!indexExists) { + return createBulkErrorObject({ + ruleId: ruleIdOrUuid, + statusCode: 400, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }); + } + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }); + } + } + const createdRule = await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable: false, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId: ruleIdOrUuid, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + return transformOrBulkError(ruleIdOrUuid, createdRule); + } catch (err) { + return transformBulkError(ruleIdOrUuid, err); + } + }) ); - return rules; + return [ + ...rules, + ...dupes.map(ruleId => + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }) + ), + ]; }, }; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 7e7d67333e78d..fb3262c476b40 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -15,6 +15,7 @@ import { transformRulesToNdjson, transformAlertsToRules, transformOrImportError, + getDuplicates, } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; @@ -1202,4 +1203,25 @@ describe('utils', () => { expect(output).toEqual(expected); }); }); + + describe('getDuplicates', () => { + test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { + const output = getDuplicates({ + value1: 1, + value2: 2, + value3: 2, + }); + const expected = ['value2', 'value3']; + expect(output).toEqual(expected); + }); + test('returns null when given a map of no duplicates', () => { + const output = getDuplicates({ + value1: 1, + value2: 1, + value3: 1, + }); + const expected: string[] = []; + expect(output).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index abb94c10209dc..df9e3021e400f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -5,6 +5,7 @@ */ import { pickBy } from 'lodash/fp'; +import { Dictionary } from 'lodash'; import { SavedObject } from 'kibana/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { @@ -215,3 +216,11 @@ export const transformOrImportError = ( }); } }; + +export const getDuplicates = (lodashDict: Dictionary): string[] => { + const hasDuplicates = Object.values(lodashDict).some(i => i > 1); + if (hasDuplicates) { + return Object.keys(lodashDict).filter(key => lodashDict[key] > 1); + } + return []; +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index dfa297c85dfb8..be008a34343c4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -80,7 +80,7 @@ export default ({ getService }: FtrProviderContext): void => { }); // TODO: This is a valid issue and will be fixed in an upcoming PR and then enabled once that PR is merged - it.skip('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { + it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) .set('kbn-xsrf', 'true') @@ -89,9 +89,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(body).to.eql([ { - error: 'Conflict', - message: 'rule_id: "rule-1" already exists', - statusCode: 409, + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', }, ]); }); From 4e8ab56497cb7bc4d0fb99a72ef69f8de36c0d59 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 12 Feb 2020 19:42:42 -0700 Subject: [PATCH 10/14] build immutable bundles for new platform plugins (#53976) * build immutable bundles for new platform plugins * only inspect workers if configured to do so * [navigation] use an index.scss file * add yarn.lock symlink * set pluginScanDirs in test so fixtures stay consistent * cleanup helpers a little * fix type error * support KBN_OPTIMIZER_MAX_WORKERS for limiting workers via env * test support for KBN_OPTIMIZER_MAX_WORKERS * expand the available memory for workers when only running one or two * add docs about KBN_OPTIMIZER_MAX_WORKERS environment variable * fix README link * update kbn/pm dist * implement bundle caching/reuse * update kbn/pm dist * don't check for cache if --no-cache is passed * update renovate config * standardize on index.scss, move console styles over * add support for --no-cache to cli * include worker config vars in optimizer version * ignore concatenated modules * update integration test * add safari to browserslist to avoid user-agent warnings in dev * update docs, clean up optimizer message/misc naming * always handle initialized messages, don't ignore states that are attached to specific events * reword caching docs, add environment var to disable caching * tweak logging and don't use optimizer.useBundleCache as that's disabled in dev * handle change notifications * batch changes for 1 second * rename CompilerState type to CompilerMsg * getChanges() no longer needs to assign changes to dirs * remove unused deps * split up run_worker.ts and share cacheKey generation logic * add a couple docs * update tests and remove unused imports * specify files when creating bundle cache key * remove one more unused import * match existing dev cli output more closely * update kbn/pm dist * set KBN_NP_PLUGINS_BUILT to avoid warning in CI * avoid extending global window type * add note to keep pluginScanDirs in sync * pass browserslistEnv in workerConfig so it is used for cache key * load commons.bundle.js in parallel too * emit initialized+success states if all bundles are cached * load bootstraps as quickly as possible * skip flaky suite * bump * update jest snapshots * remove hashing from cache key generation * remove unnecessary non-null assertion * improve docs and break up Optimizer#run() * remove unused import * refactor kbn/optimizer to break up observable logic, implement more helpful cache invalidation logic with logging * fix tests * add initializing phase * avoid rxjs observable constructor * remove unnecessary rxjs helper, add tests for bundle cache * update consumers of optimizer * update readme with new call style * replace "new platform" with "kibana platform" * fix a couple more renames * add support for several plain-text file formats * fix naming of OptimizerMsg => OptimizerUpdate, use "store" naming too * one more OptimizerMsg update * ensure bundles are not cached when cache config is false * test for initializing states and bundle cache events * remove unnecessary timeout change * Remove unnecessary helpers * Add tests for BundleCache class * Add tests for Bundle class * test summarizeEvent$ * missing paths are no longer listed in mtimes map * add tests for optimizer/cache_keys * Add some extra docs * Remove labeled loop * add integration test for kbn-optimizer watcher components * querystring-browser removed * tweak logging a smidge, improve info and final message * remove unused imports * remove duplication of getModuleCount() method * move type annotation that validates things * clear up the build completion message Co-authored-by: Elastic Machine --- .browserslistrc | 6 + .github/CODEOWNERS | 1 + package.json | 27 +- packages/kbn-dev-utils/src/index.ts | 7 +- .../serializers/absolute_path_serializer.ts | 4 +- .../kbn-dev-utils/src/tooling_log/index.ts | 2 +- .../tooling_log/tooling_log_text_writer.ts | 24 +- .../package.json | 2 +- packages/kbn-interpreter/package.json | 10 +- packages/kbn-optimizer/README.md | 110 + .../kbn-optimizer/babel.config.js | 9 +- .../kbn-optimizer/index.d.ts | 2 +- packages/kbn-optimizer/package.json | 44 + .../mock_repo/plugins/bar/kibana.json | 4 + .../mock_repo/plugins/bar}/public/index.ts | 7 +- .../mock_repo/plugins/bar/public/lib.ts | 10 +- .../mock_repo/plugins/baz/kibana.json | 3 + .../mock_repo/plugins/baz/server/index.ts | 20 + .../mock_repo/plugins/baz/server/lib.ts | 22 + .../mock_repo/plugins/foo/kibana.json | 4 + .../mock_repo/plugins/foo/public/ext.ts | 20 + .../mock_repo/plugins/foo/public/index.ts | 21 + .../mock_repo/plugins/foo/public/lib.ts | 22 + .../test_plugins/test_baz/kibana.json | 3 + .../test_plugins/test_baz/server/index.ts | 20 + .../test_plugins/test_baz/server/lib.ts | 22 + packages/kbn-optimizer/src/cli.ts | 118 + .../src/common/array_helpers.test.ts | 112 + .../kbn-optimizer/src/common/array_helpers.ts | 84 + .../kbn-optimizer/src/common/bundle.test.ts | 93 + packages/kbn-optimizer/src/common/bundle.ts | 170 + .../src/common/bundle_cache.test.ts | 118 + .../kbn-optimizer/src/common/bundle_cache.ts | 97 + .../src/common/compiler_messages.ts | 98 + .../src/common/event_stream_helpers.test.ts | 69 + .../src/common/event_stream_helpers.ts | 56 + packages/kbn-optimizer/src/common/index.ts | 28 + .../src/common/rxjs_helpers.test.ts | 140 + .../kbn-optimizer/src/common/rxjs_helpers.ts | 75 + .../kbn-optimizer/src/common/ts_helpers.ts | 26 + .../kbn-optimizer/src/common/worker_config.ts | 93 + .../src/common/worker_messages.ts | 64 + packages/kbn-optimizer/src/index.ts | 22 + .../basic_optimization.test.ts.snap | 557 ++ .../basic_optimization.test.ts | 155 + .../integration_tests/bundle_cache.test.ts | 301 + .../watch_bundles_for_changes.test.ts | 143 + .../kbn-optimizer/src/log_optimizer_state.ts | 137 + .../assign_bundles_to_workers.test.ts | 226 + .../optimizer/assign_bundles_to_workers.ts | 121 + .../src/optimizer/bundle_cache.ts | 132 + .../src/optimizer/cache_keys.test.ts | 178 + .../kbn-optimizer/src/optimizer/cache_keys.ts | 155 + .../src/optimizer/get_bundles.test.ts | 68 + .../src/optimizer/get_bundles.ts | 28 +- .../src/optimizer/get_changes.test.ts | 56 + .../src/optimizer/get_changes.ts | 63 + .../src/optimizer/get_mtimes.test.ts | 46 + .../kbn-optimizer/src/optimizer/get_mtimes.ts | 47 + packages/kbn-optimizer/src/optimizer/index.ts | 26 + .../optimizer/kibana_platform_plugins.test.ts | 60 + .../src/optimizer/kibana_platform_plugins.ts | 69 + .../src/optimizer/observe_worker.ts | 199 + .../src/optimizer/optimizer_config.test.ts | 408 ++ .../src/optimizer/optimizer_config.ts | 172 + .../src/optimizer/optimizer_reducer.ts | 170 + .../src/optimizer/run_workers.ts | 67 + .../optimizer/watch_bundles_for_changes.ts | 85 + .../kbn-optimizer/src/optimizer/watcher.ts | 109 + packages/kbn-optimizer/src/run_optimizer.ts | 82 + .../src/worker/postcss.config.js | 22 + .../kbn-optimizer/src/worker/run_compilers.ts | 210 + .../kbn-optimizer/src/worker/run_worker.ts | 107 + .../kbn-optimizer/src/worker/theme_loader.ts | 32 + .../src/worker/webpack.config.ts | 244 + .../src/worker/webpack_helpers.ts | 166 + packages/kbn-optimizer/tsconfig.json | 7 + packages/kbn-optimizer/yarn.lock | 1 + .../integration_tests/generate_plugin.test.js | 33 +- packages/kbn-pm/dist/index.js | 5699 +++++++++++------ packages/kbn-pm/package.json | 6 +- packages/kbn-storybook/package.json | 2 +- .../kbn-test/src/functional_tests/tasks.js | 13 + packages/kbn-ui-framework/package.json | 12 +- packages/kbn-ui-shared-deps/package.json | 4 +- renovate.json5 | 16 + scripts/build_kibana_platform_plugins.js | 20 + src/cli/cluster/cluster_manager.test.ts | 139 +- src/cli/cluster/cluster_manager.ts | 235 +- src/cli/cluster/log.ts | 56 + src/cli/cluster/run_kbn_optimizer.ts | 79 + src/cli/cluster/worker.test.ts | 4 +- src/cli/cluster/worker.ts | 1 + src/cli/command.js | 4 +- src/cli/serve/serve.js | 2 +- src/core/public/plugins/plugin_loader.test.ts | 10 +- src/core/public/plugins/plugin_loader.ts | 2 +- src/core/server/config/env.ts | 5 + .../server/http/base_path_proxy_server.ts | 57 +- src/core/server/legacy/legacy_service.test.ts | 2 +- .../server/plugins/plugins_service.test.ts | 7 +- src/core/server/plugins/plugins_service.ts | 5 +- src/core/server/plugins/types.ts | 6 +- src/core/server/server.api.md | 2 +- src/dev/build/build_distributables.js | 2 + .../tasks/build_kibana_platform_plugins.js | 38 + src/dev/build/tasks/index.js | 1 + .../core_plugins/console_legacy/index.ts | 2 - .../dashboard_embeddable_container/index.ts | 8 +- .../public/index.scss | 3 - src/legacy/core_plugins/data/index.ts | 1 - .../core_plugins/data/public/index.scss | 3 - .../core_plugins/embeddable_api/index.ts | 7 +- .../embeddable_api/public/index.scss | 3 - .../core_plugins/inspector_views/package.json | 4 - .../inspector_views/public/index.scss | 4 - src/legacy/core_plugins/interpreter/index.ts | 1 - .../interpreter/public/index.scss | 4 - .../core_plugins/navigation/package.json | 4 - .../core_plugins/navigation/public/index.scss | 3 - src/legacy/server/sass/build.js | 20 +- src/legacy/server/sass/build_all.js | 4 +- .../ui/ui_exports/ui_export_defaults.js | 2 - .../ui/ui_render/bootstrap/template.js.hbs | 87 +- src/legacy/ui/ui_render/ui_render_mixin.js | 1 + src/optimize/base_optimizer.js | 32 +- .../no_placeholder/no_placeholder.plugin.js | 20 + .../plugin/placeholder/placeholder.plugin.js | 20 + src/optimize/bundles_route/bundles_route.js | 14 +- src/optimize/index.js | 6 +- src/optimize/intentionally_empty_module.js | 18 + .../np_ui_plugin_public_dirs.js} | 42 +- src/optimize/watch/optmzr_role.js | 5 +- src/optimize/watch/watch_optimizer.js | 3 +- src/optimize/watch/watch_server.js | 5 +- src/plugins/console/public/index.scss | 1 + src/plugins/console/public/index.ts | 2 + .../console}/public/styles/_app.scss | 0 .../console/public/styles/_index.scss} | 2 - .../public/styles/components/_help.scss | 0 .../public/styles/components/_history.scss | 2 - .../public/styles/components/_index.scss | 0 .../public/{_index.scss => index.scss} | 0 .../public/index.ts | 2 + .../data/public/{_index.scss => index.scss} | 0 src/plugins/data/public/index.ts | 2 + .../ui/filter_bar/filter_editor/_index.scss | 2 +- .../public/{_index.scss => index.scss} | 0 src/plugins/embeddable/public/index.ts | 2 + .../public/{_index.scss => index.scss} | 0 src/plugins/expressions/public/index.ts | 2 + src/plugins/inspector/public/index.scss | 1 + src/plugins/inspector/public/index.ts | 2 + src/plugins/navigation/public/index.scss | 1 + src/plugins/navigation/public/index.ts | 2 + .../test_suites/core_plugins/rendering.ts | 3 +- test/scripts/jenkins_build_kibana.sh | 9 + test/scripts/jenkins_xpack_build_kibana.sh | 8 + vars/kibanaPipeline.groovy | 1 + x-pack/index.js | 4 - .../legacy/plugins/apm/cypress/package.json | 2 +- .../shareable_runtime/webpack.config.js | 11 +- x-pack/legacy/plugins/searchprofiler/index.ts | 27 - .../plugins/searchprofiler/public/index.scss | 12 - .../legacy/plugins/security/public/index.scss | 5 - x-pack/legacy/plugins/watcher/index.ts | 19 - x-pack/package.json | 4 +- .../plugins/searchprofiler/public/README.md | 3 - .../plugins/searchprofiler/public/index.scss | 1 + x-pack/plugins/searchprofiler/public/index.ts | 1 + .../searchprofiler/public/styles/_index.scss | 0 .../searchprofiler/public/styles/_mixins.scss | 0 .../components/_highlight_details_flyout.scss | 0 .../styles/components/_percentage_badge.scss | 0 .../styles/components/_profile_tree.scss | 0 .../public/styles/containers/_main.scss | 0 .../containers/_profile_query_editor.scss | 0 .../public/{_index.scss => index.scss} | 2 + x-pack/plugins/security/public/index.ts | 1 + .../plugins/watcher/public/index.scss | 3 - x-pack/plugins/watcher/public/index.ts | 2 + .../advanced_settings_spaces.ts | 4 +- yarn.lock | 590 +- 183 files changed, 11467 insertions(+), 2594 deletions(-) create mode 100644 packages/kbn-optimizer/README.md rename src/cli/color.js => packages/kbn-optimizer/babel.config.js (84%) rename webpackShims/tinymath.js => packages/kbn-optimizer/index.d.ts (93%) create mode 100644 packages/kbn-optimizer/package.json create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json rename {src/legacy/core_plugins/navigation => packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar}/public/index.ts (79%) rename src/legacy/core_plugins/inspector_views/index.js => packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts (80%) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts create mode 100644 packages/kbn-optimizer/src/cli.ts create mode 100644 packages/kbn-optimizer/src/common/array_helpers.test.ts create mode 100644 packages/kbn-optimizer/src/common/array_helpers.ts create mode 100644 packages/kbn-optimizer/src/common/bundle.test.ts create mode 100644 packages/kbn-optimizer/src/common/bundle.ts create mode 100644 packages/kbn-optimizer/src/common/bundle_cache.test.ts create mode 100644 packages/kbn-optimizer/src/common/bundle_cache.ts create mode 100644 packages/kbn-optimizer/src/common/compiler_messages.ts create mode 100644 packages/kbn-optimizer/src/common/event_stream_helpers.test.ts create mode 100644 packages/kbn-optimizer/src/common/event_stream_helpers.ts create mode 100644 packages/kbn-optimizer/src/common/index.ts create mode 100644 packages/kbn-optimizer/src/common/rxjs_helpers.test.ts create mode 100644 packages/kbn-optimizer/src/common/rxjs_helpers.ts create mode 100644 packages/kbn-optimizer/src/common/ts_helpers.ts create mode 100644 packages/kbn-optimizer/src/common/worker_config.ts create mode 100644 packages/kbn-optimizer/src/common/worker_messages.ts create mode 100644 packages/kbn-optimizer/src/index.ts create mode 100644 packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap create mode 100644 packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts create mode 100644 packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts create mode 100644 packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts create mode 100644 packages/kbn-optimizer/src/log_optimizer_state.ts create mode 100644 packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts create mode 100644 packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts create mode 100644 packages/kbn-optimizer/src/optimizer/bundle_cache.ts create mode 100644 packages/kbn-optimizer/src/optimizer/cache_keys.test.ts create mode 100644 packages/kbn-optimizer/src/optimizer/cache_keys.ts create mode 100644 packages/kbn-optimizer/src/optimizer/get_bundles.test.ts rename src/cli/log.js => packages/kbn-optimizer/src/optimizer/get_bundles.ts (60%) create mode 100644 packages/kbn-optimizer/src/optimizer/get_changes.test.ts create mode 100644 packages/kbn-optimizer/src/optimizer/get_changes.ts create mode 100644 packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts create mode 100644 packages/kbn-optimizer/src/optimizer/get_mtimes.ts create mode 100644 packages/kbn-optimizer/src/optimizer/index.ts create mode 100644 packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts create mode 100644 packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts create mode 100644 packages/kbn-optimizer/src/optimizer/observe_worker.ts create mode 100644 packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts create mode 100644 packages/kbn-optimizer/src/optimizer/optimizer_config.ts create mode 100644 packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts create mode 100644 packages/kbn-optimizer/src/optimizer/run_workers.ts create mode 100644 packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts create mode 100644 packages/kbn-optimizer/src/optimizer/watcher.ts create mode 100644 packages/kbn-optimizer/src/run_optimizer.ts create mode 100644 packages/kbn-optimizer/src/worker/postcss.config.js create mode 100644 packages/kbn-optimizer/src/worker/run_compilers.ts create mode 100644 packages/kbn-optimizer/src/worker/run_worker.ts create mode 100644 packages/kbn-optimizer/src/worker/theme_loader.ts create mode 100644 packages/kbn-optimizer/src/worker/webpack.config.ts create mode 100644 packages/kbn-optimizer/src/worker/webpack_helpers.ts create mode 100644 packages/kbn-optimizer/tsconfig.json create mode 120000 packages/kbn-optimizer/yarn.lock create mode 100644 scripts/build_kibana_platform_plugins.js create mode 100644 src/cli/cluster/log.ts create mode 100644 src/cli/cluster/run_kbn_optimizer.ts create mode 100644 src/dev/build/tasks/build_kibana_platform_plugins.js delete mode 100644 src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss delete mode 100644 src/legacy/core_plugins/data/public/index.scss delete mode 100644 src/legacy/core_plugins/embeddable_api/public/index.scss delete mode 100644 src/legacy/core_plugins/inspector_views/package.json delete mode 100644 src/legacy/core_plugins/inspector_views/public/index.scss delete mode 100644 src/legacy/core_plugins/interpreter/public/index.scss delete mode 100644 src/legacy/core_plugins/navigation/package.json delete mode 100644 src/legacy/core_plugins/navigation/public/index.scss create mode 100644 src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js create mode 100644 src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js create mode 100644 src/optimize/intentionally_empty_module.js rename src/{legacy/core_plugins/navigation/index.ts => optimize/np_ui_plugin_public_dirs.js} (53%) create mode 100644 src/plugins/console/public/index.scss rename src/{legacy/core_plugins/console_legacy => plugins/console}/public/styles/_app.scss (100%) rename src/{legacy/core_plugins/console_legacy/public/styles/index.scss => plugins/console/public/styles/_index.scss} (77%) rename src/{legacy/core_plugins/console_legacy => plugins/console}/public/styles/components/_help.scss (100%) rename src/{legacy/core_plugins/console_legacy => plugins/console}/public/styles/components/_history.scss (89%) rename src/{legacy/core_plugins/console_legacy => plugins/console}/public/styles/components/_index.scss (100%) rename src/plugins/dashboard_embeddable_container/public/{_index.scss => index.scss} (100%) rename src/plugins/data/public/{_index.scss => index.scss} (100%) rename src/plugins/embeddable/public/{_index.scss => index.scss} (100%) rename src/plugins/expressions/public/{_index.scss => index.scss} (100%) create mode 100644 src/plugins/inspector/public/index.scss create mode 100644 src/plugins/navigation/public/index.scss delete mode 100644 x-pack/legacy/plugins/searchprofiler/index.ts delete mode 100644 x-pack/legacy/plugins/searchprofiler/public/index.scss delete mode 100644 x-pack/legacy/plugins/watcher/index.ts delete mode 100644 x-pack/plugins/searchprofiler/public/README.md create mode 100644 x-pack/plugins/searchprofiler/public/index.scss rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/_index.scss (100%) rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/_mixins.scss (100%) rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss (100%) rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/components/_percentage_badge.scss (100%) rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/components/_profile_tree.scss (100%) rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/containers/_main.scss (100%) rename x-pack/{legacy => }/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss (100%) rename x-pack/plugins/security/public/{_index.scss => index.scss} (68%) rename x-pack/{legacy => }/plugins/watcher/public/index.scss (80%) diff --git a/.browserslistrc b/.browserslistrc index a788f9544ab8a..89114f393c462 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,3 +1,9 @@ +[production] last 2 versions > 5% Safari 7 # for PhantomJS support: https://github.com/elastic/kibana/issues/27136 + +[dev] +last 1 chrome versions +last 1 firefox versions +last 1 safari versions diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7901bd331edff..bf1e341c796fa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -103,6 +103,7 @@ /packages/*babel*/ @elastic/kibana-operations /packages/kbn-dev-utils*/ @elastic/kibana-operations /packages/kbn-es/ @elastic/kibana-operations +/packages/kbn-optimizer/ @elastic/kibana-operations /packages/kbn-pm/ @elastic/kibana-operations /packages/kbn-test/ @elastic/kibana-operations /packages/kbn-ui-shared-deps/ @elastic/kibana-operations diff --git a/package.json b/package.json index 5bf33f0ab0bcb..26e1112ead697 100644 --- a/package.json +++ b/package.json @@ -137,12 +137,6 @@ "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", - "@types/flot": "^0.0.31", - "@types/json-stable-stringify": "^1.0.32", - "@types/lodash.clonedeep": "^4.5.4", - "@types/node-forge": "^0.9.0", - "@types/react-grid-layout": "^0.16.7", - "@types/recompose": "^0.30.5", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "angular": "^1.7.9", @@ -152,11 +146,12 @@ "angular-route": "^1.7.9", "angular-sanitize": "^1.7.9", "angular-sortable-view": "^0.0.17", - "autoprefixer": "9.6.1", + "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "browserslist-useragent": "^3.0.2", "cache-loader": "^4.1.0", "chalk": "^2.4.2", "check-disk-space": "^2.1.0", @@ -165,7 +160,7 @@ "commander": "3.0.2", "compare-versions": "3.5.1", "core-js": "^3.2.1", - "css-loader": "2.1.1", + "css-loader": "^3.4.2", "d3": "3.5.17", "d3-cloud": "1.2.5", "deep-freeze-strict": "^1.1.1", @@ -226,7 +221,7 @@ "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", - "postcss-loader": "3.0.0", + "postcss-loader": "^3.0.0", "prop-types": "15.6.0", "proxy-from-env": "1.0.0", "pug": "^2.0.4", @@ -259,7 +254,7 @@ "seedrandom": "^3.0.5", "semver": "^5.5.0", "style-it": "^2.1.3", - "style-loader": "0.23.1", + "style-loader": "^1.1.3", "symbol-observable": "^1.2.0", "tar": "4.4.13", "terser-webpack-plugin": "^2.3.4", @@ -279,7 +274,7 @@ "vega-schema-url-parser": "1.0.0", "vega-tooltip": "^0.12.0", "vision": "^5.3.3", - "webpack": "4.41.0", + "webpack": "^4.41.5", "webpack-merge": "4.2.2", "whatwg-fetch": "^3.0.0", "wrapper-webpack-plugin": "^2.1.0", @@ -300,6 +295,7 @@ "@kbn/eslint-plugin-eslint": "1.0.0", "@kbn/expect": "1.0.0", "@kbn/plugin-generator": "1.0.0", + "@kbn/optimizer": "1.0.0", "@kbn/test": "1.0.0", "@kbn/utility-types": "1.0.0", "@microsoft/api-documenter": "7.7.2", @@ -312,6 +308,7 @@ "@types/babel__core": "^7.1.2", "@types/bluebird": "^3.1.1", "@types/boom": "^7.2.0", + "@types/browserslist-useragent": "^3.0.0", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.10", "@types/chromedriver": "^2.38.0", @@ -324,6 +321,7 @@ "@types/enzyme": "^3.9.0", "@types/eslint": "^6.1.3", "@types/fetch-mock": "^7.3.1", + "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", "@types/glob": "^7.1.1", "@types/globby": "^8.0.0", @@ -337,10 +335,12 @@ "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", "@types/js-yaml": "^3.11.1", + "@types/json-stable-stringify": "^1.0.32", "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", "@types/lodash": "^3.10.1", + "@types/lodash.clonedeep": "^4.5.4", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", "@types/minimatch": "^2.0.29", @@ -348,6 +348,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", + "@types/node-forge": "^0.9.0", "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", @@ -357,11 +358,13 @@ "@types/reach__router": "^1.2.6", "@types/react": "^16.9.11", "@types/react-dom": "^16.9.4", + "@types/react-grid-layout": "^0.16.7", "@types/react-redux": "^6.0.6", "@types/react-resize-detector": "^4.0.1", "@types/react-router": "^5.1.3", "@types/react-router-dom": "^5.1.3", "@types/react-virtualized": "^9.18.7", + "@types/recompose": "^0.30.6", "@types/redux": "^3.6.31", "@types/redux-actions": "^2.6.1", "@types/request": "^2.48.2", @@ -461,7 +464,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.5", + "postcss": "^7.0.26", "postcss-url": "^8.0.0", "prettier": "^1.19.1", "proxyquire": "1.8.0", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 714ed56ac4703..305e29a0e41df 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -18,12 +18,7 @@ */ export { withProcRunner, ProcRunner } from './proc_runner'; -export { - ToolingLog, - ToolingLogTextWriter, - pickLevelFromFlags, - ToolingLogCollectingWriter, -} from './tooling_log'; +export * from './tooling_log'; export { createAbsolutePathSerializer } from './serializers'; export { CA_CERT_PATH, diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts index 661ed7329347f..9edc63dd7d842 100644 --- a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts @@ -17,7 +17,9 @@ * under the License. */ -export function createAbsolutePathSerializer(rootPath: string) { +import { REPO_ROOT } from '../repo_root'; + +export function createAbsolutePathSerializer(rootPath: string = REPO_ROOT) { return { print: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'), test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), diff --git a/packages/kbn-dev-utils/src/tooling_log/index.ts b/packages/kbn-dev-utils/src/tooling_log/index.ts index 1f5afac26d561..f8009a255f010 100644 --- a/packages/kbn-dev-utils/src/tooling_log/index.ts +++ b/packages/kbn-dev-utils/src/tooling_log/index.ts @@ -19,5 +19,5 @@ export { ToolingLog } from './tooling_log'; export { ToolingLogTextWriter, ToolingLogTextWriterConfig } from './tooling_log_text_writer'; -export { pickLevelFromFlags, LogLevel } from './log_levels'; +export { pickLevelFromFlags, parseLogLevel, LogLevel } from './log_levels'; export { ToolingLogCollectingWriter } from './tooling_log_collecting_writer'; diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts index 65b625de9f308..b8c12433a0ebb 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts @@ -82,20 +82,28 @@ export class ToolingLogTextWriter implements Writer { } } - write({ type, indent, args }: Message) { - if (!shouldWriteType(this.level, type)) { + write(msg: Message) { + if (!shouldWriteType(this.level, msg.type)) { return false; } - const txt = type === 'error' ? stringifyError(args[0]) : format(args[0], ...args.slice(1)); - const prefix = has(MSG_PREFIXES, type) ? MSG_PREFIXES[type] : ''; + const prefix = has(MSG_PREFIXES, msg.type) ? MSG_PREFIXES[msg.type] : ''; + ToolingLogTextWriter.write(this.writeTo, prefix, msg); + return true; + } + + static write(writeTo: ToolingLogTextWriter['writeTo'], prefix: string, msg: Message) { + const txt = + msg.type === 'error' + ? stringifyError(msg.args[0]) + : format(msg.args[0], ...msg.args.slice(1)); (prefix + txt).split('\n').forEach((line, i) => { let lineIndent = ''; - if (indent > 0) { + if (msg.indent > 0) { // if we are indenting write some spaces followed by a symbol - lineIndent += ' '.repeat(indent - 1); + lineIndent += ' '.repeat(msg.indent - 1); lineIndent += line.startsWith('-') ? '└' : '│'; } @@ -105,9 +113,7 @@ export class ToolingLogTextWriter implements Writer { lineIndent += PREFIX_INDENT; } - this.writeTo.write(`${lineIndent}${line}\n`); + writeTo.write(`${lineIndent}${line}\n`); }); - - return true; } } diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 9fae27011767e..332f7e8a20cc2 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -16,6 +16,6 @@ "glob-all": "^3.1.0", "lru-cache": "^4.1.5", "resolve": "^1.7.1", - "webpack": "^4.41.0" + "webpack": "^4.41.5" } } diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 4faa1bc8e542f..d2f0b0c358284 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -23,15 +23,15 @@ "@kbn/dev-utils": "1.0.0", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^5.0.4", - "css-loader": "2.1.1", + "css-loader": "^3.4.2", "del": "^5.1.0", "getopts": "^2.2.4", "pegjs": "0.10.0", - "sass-loader": "^7.3.1", - "style-loader": "0.23.1", + "sass-loader": "^8.0.2", + "style-loader": "^1.1.3", "supports-color": "^7.0.0", "url-loader": "2.2.0", - "webpack": "4.41.0", - "webpack-cli": "^3.3.9" + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10" } } diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md new file mode 100644 index 0000000000000..c7f50c6af8dfd --- /dev/null +++ b/packages/kbn-optimizer/README.md @@ -0,0 +1,110 @@ +# @kbn/optimizer + +`@kbn/optimizer` is a package for building Kibana platform UI plugins (and hopefully more soon). + +Kibana Platform plugins with `"ui": true` in their `kibana.json` file will have their `public/index.ts` file (and all of its dependencies) bundled into the `target/public` directory of the plugin. The build output does not need to be updated when other plugins are updated and is included in the distributable without requiring that we ship `@kbn/optimizer` 🎉. + +## Webpack config + +The [Webpack config][WebpackConfig] is designed to provide the majority of what was available in the legacy optimizer and is the same for all plugins to promote consistency and keep things sane for the operations team. It has support for JS/TS built with babel, url imports of image and font files, and support for importing `scss` and `css` files. SCSS is pre-processed by [postcss][PostCss], built for both light and dark mode and injected automatically into the page when the parent module is loaded (page reloads are still required for switching between light/dark mode). CSS is injected into the DOM as it is written on disk when the parent module is loaded (no postcss support). + +Source maps are enabled except when building the distributable. They show the code actually being executed by the browser to strike a balance between debuggability and performance. They are not configurable at this time but will be configurable once we have a developer configuration solution that doesn't rely on the server (see [#55656](https://github.com/elastic/kibana/issues/55656)). + +### IE Support + +To make front-end code easier to debug the optimizer uses the `BROWSERSLIST_ENV=dev` environment variable (by default) to build JS and CSS that is compatible with modern browsers. In order to support older browsers like IE in development you will need to specify the `BROWSERSLIST_ENV=production` environment variable or build a distributable for testing. + +## Running the optimizer + +The `@kbn/optimizer` is automatically executed from the dev cli, the Kibana build scripts, and in CI. If you're running Kibana locally in some other way you might need to build the plugins manually, which you can do by running `node scripts/build_kibana_platform_plugins` (pass `--help` for options). + +### Worker count + +You can limit the number of workers the optimizer uses by setting the `KBN_OPTIMIZER_MAX_WORKERS` environment variable. You might want to do this if your system struggles to keep up while the optimizer is getting started and building all plugins as fast as possible. Setting `KBN_OPTIMIZER_MAX_WORKERS=1` will cause the optimizer to take the longest amount of time but will have the smallest impact on other components of your system. + +We only limit the number of workers we will start at any given time. If we start more workers later we will limit the number of workers we start at that time by the maximum, but we don't take into account the number of workers already started because it is assumed that those workers are doing very little work. This greatly simplifies the logic as we don't ever have to reallocate workers and provides the best performance in most cases. + +### Caching + +Bundles built by the the optimizer include a cache file which describes the information needed to determine if the bundle needs to be rebuilt when the optimizer is restarted. Caching is enabled by default and is very aggressive about invalidating the cache output, but if you need to disable caching you can pass `--no-cache` to `node scripts/build_kibana_platform_plugins`, or set the `KBN_OPTIMIZER_NO_CACHE` environment variable to anything (env overrides everything). + +When a bundle is determined to be up-to-date a worker is not started for the bundle. If running the optimizer with the `--dev/--watch` flag, then all the files referenced by cached bundles are watched for changes. Once a change is detected in any of the files referenced by the built bundle a worker is started. If a file is changed that is referenced by several bundles then workers will be started for each bundle, combining workers together to respect the worker limit. + +## API + +To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used. + +Example: +```ts +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; +import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; + +const log = new ToolingLog({ + level: 'verbose', + writeTo: process.stdout, +}) + +const config = OptimizerConfig.create({ + repoRoot: Path.resolve(__dirname, '../../..'), + watch: false, + oss: true, + dist: true +}); + +await runOptimizer(config) + .pipe(logOptimizerState(log, config)) + .toPromise(); +``` + +This is essentially what we're doing in [`script/build_kibana_platform_plugins`][Cli] and the new [build system task][BuildTask]. + +## Internals + +The optimizer runs webpack instances in worker processes. Each worker is configured via a [`WorkerConfig`][WorkerConfig] object and an array of [`Bundle`][Bundle] objects which are JSON serialized and passed to the worker as it's arguments. + +Plugins/bundles are assigned to workers based on the number of modules historically seen in each bundle in an effort to evenly distribute the load across the worker pool (see [`assignBundlesToWorkers`][AssignBundlesToWorkers]). + +The number of workers that will be started at any time is automatically chosen by dividing the number of cores available by 3 (minimum of 2). + +The [`WorkerConfig`][WorkerConfig] includes the location of the repo (it might be one of many builds, or the main repo), wether we are running in watch mode, wether we are building a distributable, and other global config items. + +The [`Bundle`][Bundle] objects which include the details necessary to create a webpack config for a specific plugin's bundle (created using [`webpack.config.ts`][WebpackConfig]). + +Each worker communicates state back to the main process by sending [`WorkerMsg`][WorkerMsg] and [`CompilerMsg`][CompilerMsg] objects using IPC. + +The Optimizer captures all of these messages and produces a stream of update objects. + +Optimizer phases: +
+
'initializing'
+
Initial phase, during this state the optimizer is validating caches and determining which builds should be built initially.
+
'initialized'
+
Emitted by the optimizer once it's don't initializing its internal state and determined which bundles are going to be built initially.
+
'running'
+
Emitted when any worker is in a running state. To determine which compilers are running, look for BundleState objects with type 'running'.
+
'issue'
+
Emitted when all workers are done running and any compiler completed with a 'compiler issue' status. Compiler issues include things like "unable to resolve module" or syntax errors in the source modules and can be fixed by users when running in watch mode.
+
'success'
+
Emitted when all workers are done running and all compilers completed with 'compiler success'.
+
'reallocating'
+
Emitted when the files referenced by a cached bundle have changed, before the worker has been started up to update that bundle.
+
+ +Workers have several error message they may emit which indicate unrecoverable errors. When any of those messages are received the stream will error and the workers will be torn down. + +For an example of how to handle these states checkout the [`logOptimizerState()`][LogOptimizerState] helper. + +[PostCss]: https://postcss.org/ +[Cli]: src/cli.ts +[Optimizer]: src/optimizer.ts +[ObserveWorker]: src/observe_worker.ts +[CompilerMsg]: src/common/compiler_messages.ts +[WorkerMsg]: src/common/worker_messages.ts +[Bundle]: src/common/bundle.ts +[WebpackConfig]: src/worker/webpack.config.ts +[BundleDefinition]: src/common/bundle_definition.ts +[WorkerConfig]: src/common/worker_config.ts +[OptimizerConfig]: src/optimizer_config.ts +[LogOptimizerState]: src/log_optimizer_state.ts +[AssignBundlesToWorkers]: src/assign_bundles_to_workers.ts +[BuildTask]: ../../src/dev/build/tasks/build_kibana_platform_plugins.js \ No newline at end of file diff --git a/src/cli/color.js b/packages/kbn-optimizer/babel.config.js similarity index 84% rename from src/cli/color.js rename to packages/kbn-optimizer/babel.config.js index a02fb551c4181..ff657603f4c8d 100644 --- a/src/cli/color.js +++ b/packages/kbn-optimizer/babel.config.js @@ -17,8 +17,7 @@ * under the License. */ -import chalk from 'chalk'; - -export const green = chalk.black.bgGreen; -export const red = chalk.white.bgRed; -export const yellow = chalk.black.bgYellow; +module.exports = { + presets: ['@kbn/babel-preset/node_preset'], + ignore: ['**/*.test.js'], +}; diff --git a/webpackShims/tinymath.js b/packages/kbn-optimizer/index.d.ts similarity index 93% rename from webpackShims/tinymath.js rename to packages/kbn-optimizer/index.d.ts index 45aa86a6ef64a..aa55df9215c2f 100644 --- a/webpackShims/tinymath.js +++ b/packages/kbn-optimizer/index.d.ts @@ -17,4 +17,4 @@ * under the License. */ -module.exports = require('tinymath/lib/tinymath.es5.js'); +export * from './src/index'; diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json new file mode 100644 index 0000000000000..e8bb31f1e365d --- /dev/null +++ b/packages/kbn-optimizer/package.json @@ -0,0 +1,44 @@ +{ + "name": "@kbn/optimizer", + "version": "1.0.0", + "private": true, + "license": "Apache-2.0", + "main": "./target/index.js", + "scripts": { + "build": "babel src --out-dir target --copy-files --delete-dir-on-start --extensions .ts --ignore *.test.ts --source-maps=inline", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@babel/cli": "^7.5.5", + "@kbn/babel-preset": "1.0.0", + "@kbn/dev-utils": "1.0.0", + "@kbn/ui-shared-deps": "1.0.0", + "@types/loader-utils": "^1.1.3", + "@types/watchpack": "^1.1.5", + "@types/webpack": "^4.41.3", + "autoprefixer": "^9.7.4", + "babel-loader": "^8.0.6", + "clean-webpack-plugin": "^3.0.0", + "cpy": "^8.0.0", + "css-loader": "^3.4.2", + "del": "^5.1.0", + "file-loader": "^4.2.0", + "istanbul-instrumenter-loader": "^3.0.1", + "jest-diff": "^25.1.0", + "json-stable-stringify": "^1.0.1", + "loader-utils": "^1.2.3", + "node-sass": "^4.13.0", + "postcss-loader": "^3.0.0", + "raw-loader": "^3.1.0", + "rxjs": "^6.5.3", + "sass-loader": "^8.0.2", + "style-loader": "^1.1.3", + "terser-webpack-plugin": "^2.1.2", + "tinymath": "1.2.1", + "url-loader": "^2.2.0", + "watchpack": "^1.6.0", + "webpack": "^4.41.5", + "webpack-merge": "^4.2.2" + } +} \ No newline at end of file diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json new file mode 100644 index 0000000000000..20c8046daa65e --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "bar", + "ui": true +} diff --git a/src/legacy/core_plugins/navigation/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts similarity index 79% rename from src/legacy/core_plugins/navigation/public/index.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 7ddb4819cdb3a..66fa55479f3b9 100644 --- a/src/legacy/core_plugins/navigation/public/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -17,7 +17,6 @@ * under the License. */ -// TODO these are imports from the old plugin world. -// Once the new platform is ready, they can get removed -// and handled by the platform itself in the setup method -// of the ExpressionExectorService +import { fooLibFn } from '../../foo/public/index'; +export * from './lib'; +export { fooLibFn }; diff --git a/src/legacy/core_plugins/inspector_views/index.js b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts similarity index 80% rename from src/legacy/core_plugins/inspector_views/index.js rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts index a37b6bb3db426..091fae72ad635 100644 --- a/src/legacy/core_plugins/inspector_views/index.js +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts @@ -17,12 +17,6 @@ * under the License. */ -import { resolve } from 'path'; - -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); +export function barLibFn() { + return 'bar'; } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json new file mode 100644 index 0000000000000..6e4e9c70a115c --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json @@ -0,0 +1,3 @@ +{ + "id": "baz" +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts new file mode 100644 index 0000000000000..12e580bbb76b3 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './lib'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts new file mode 100644 index 0000000000000..870e5a8045280 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function bazLibFn() { + return 'baz'; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json new file mode 100644 index 0000000000000..256856181ccd8 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "foo", + "ui": true +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts new file mode 100644 index 0000000000000..3064d6814e2b1 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ext = 'TRUE'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts new file mode 100644 index 0000000000000..9d3871df24739 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './lib'; +export * from './ext'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts new file mode 100644 index 0000000000000..04a8c7e5b1eec --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function fooLibFn() { + return 'foo'; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json new file mode 100644 index 0000000000000..b9e044523a6a5 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json @@ -0,0 +1,3 @@ +{ + "id": "test_baz" +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts new file mode 100644 index 0000000000000..12e580bbb76b3 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './lib'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts new file mode 100644 index 0000000000000..870e5a8045280 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function bazLibFn() { + return 'baz'; +} diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts new file mode 100644 index 0000000000000..dcb4dcd35698d --- /dev/null +++ b/packages/kbn-optimizer/src/cli.ts @@ -0,0 +1,118 @@ +/* + * 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 'source-map-support/register'; + +import Path from 'path'; + +import { run, REPO_ROOT, createFlagError } from '@kbn/dev-utils'; + +import { logOptimizerState } from './log_optimizer_state'; +import { OptimizerConfig } from './optimizer'; +import { runOptimizer } from './run_optimizer'; + +run( + async ({ log, flags }) => { + const watch = flags.watch ?? false; + if (typeof watch !== 'boolean') { + throw createFlagError('expected --watch to have no value'); + } + + const oss = flags.oss ?? false; + if (typeof oss !== 'boolean') { + throw createFlagError('expected --oss to have no value'); + } + + const cache = flags.cache ?? true; + if (typeof cache !== 'boolean') { + throw createFlagError('expected --cache to have no value'); + } + + const dist = flags.dist ?? false; + if (typeof dist !== 'boolean') { + throw createFlagError('expected --dist to have no value'); + } + + const examples = flags.examples ?? false; + if (typeof examples !== 'boolean') { + throw createFlagError('expected --no-examples to have no value'); + } + + const profileWebpack = flags.profile ?? false; + if (typeof profileWebpack !== 'boolean') { + throw createFlagError('expected --profile to have no value'); + } + + const inspectWorkers = flags['inspect-workers'] ?? false; + if (typeof inspectWorkers !== 'boolean') { + throw createFlagError('expected --no-inspect-workers to have no value'); + } + + const maxWorkerCount = flags.workers ? Number.parseInt(String(flags.workers), 10) : undefined; + if (maxWorkerCount !== undefined && (!Number.isFinite(maxWorkerCount) || maxWorkerCount < 1)) { + throw createFlagError('expected --workers to be a number greater than 0'); + } + + const extraPluginScanDirs = ([] as string[]) + .concat((flags['scan-dir'] as string | string[]) || []) + .map(p => Path.resolve(p)); + if (!extraPluginScanDirs.every(s => typeof s === 'string')) { + throw createFlagError('expected --scan-dir to be a string'); + } + + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + watch, + maxWorkerCount, + oss, + dist, + cache, + examples, + profileWebpack, + extraPluginScanDirs, + inspectWorkers, + }); + + await runOptimizer(config) + .pipe(logOptimizerState(log, config)) + .toPromise(); + }, + { + flags: { + boolean: ['watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], + string: ['workers', 'scan-dir'], + default: { + examples: true, + cache: true, + 'inspect-workers': true, + }, + help: ` + --watch run the optimizer in watch mode + --workers max number of workers to use + --oss only build oss plugins + --profile profile the webpack builds and write stats.json files to build outputs + --no-cache disable the cache + --no-examples don't build the example plugins + --dist create bundles that are suitable for inclusion in the Kibana distributable + --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) + --no-inspect-workers when inspecting the parent process, don't inspect the workers + `, + }, + } +); diff --git a/packages/kbn-optimizer/src/common/array_helpers.test.ts b/packages/kbn-optimizer/src/common/array_helpers.test.ts new file mode 100644 index 0000000000000..9d45217486ee8 --- /dev/null +++ b/packages/kbn-optimizer/src/common/array_helpers.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { ascending, descending } from './array_helpers'; + +describe('ascending/descending', () => { + interface Item { + a: number; + b: number | string; + c?: number; + } + + const a = (x: Item) => x.a; + const b = (x: Item) => x.b; + const c = (x: Item) => x.c; + const print = (x: Item) => `${x.a}/${x.b}/${x.c}`; + const values: Item[] = [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 2, c: 1 }, + { a: 9, b: 9, c: 9 }, + { a: 8, b: 5, c: 8 }, + { a: 8, b: 5 }, + { a: 8, b: 4 }, + { a: 8, b: 3, c: 8 }, + { a: 8, b: 2 }, + { a: 8, b: 1, c: 8 }, + { a: 8, b: 1 }, + { a: 8, b: 0 }, + { a: 8, b: -1, c: 8 }, + { a: 8, b: -2 }, + { a: 8, b: -3, c: 8 }, + { a: 8, b: -4 }, + { a: 8, b: 'foo', c: 8 }, + { a: 8, b: 'foo' }, + { a: 8, b: 'bar', c: 8 }, + { a: 8, b: 'bar' }, + ].sort(() => 0.5 - Math.random()); + + it('sorts items using getters', () => { + expect( + Array.from(values) + .sort(ascending(a, b, c)) + .map(print) + ).toMatchInlineSnapshot(` + Array [ + "1/2/3", + "3/2/1", + "8/-4/undefined", + "8/-3/8", + "8/-2/undefined", + "8/-1/8", + "8/0/undefined", + "8/1/undefined", + "8/1/8", + "8/2/undefined", + "8/3/8", + "8/4/undefined", + "8/5/undefined", + "8/5/8", + "8/bar/undefined", + "8/bar/8", + "8/foo/undefined", + "8/foo/8", + "9/9/9", + ] + `); + + expect( + Array.from(values) + .sort(descending(a, b, c)) + .map(print) + ).toMatchInlineSnapshot(` + Array [ + "9/9/9", + "8/foo/8", + "8/foo/undefined", + "8/bar/8", + "8/bar/undefined", + "8/5/8", + "8/5/undefined", + "8/4/undefined", + "8/3/8", + "8/2/undefined", + "8/1/8", + "8/1/undefined", + "8/0/undefined", + "8/-1/8", + "8/-2/undefined", + "8/-3/8", + "8/-4/undefined", + "3/2/1", + "1/2/3", + ] + `); + }); +}); diff --git a/packages/kbn-optimizer/src/common/array_helpers.ts b/packages/kbn-optimizer/src/common/array_helpers.ts new file mode 100644 index 0000000000000..740f018d19298 --- /dev/null +++ b/packages/kbn-optimizer/src/common/array_helpers.ts @@ -0,0 +1,84 @@ +/* + * 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. + */ + +type SortPropGetter = (x: T) => number | string | undefined; +type Comparator = (a: T, b: T) => number; + +/** + * create a sort comparator that sorts objects in ascending + * order based on the ...getters. getters are called for each + * item and return the value to compare against the other items. + * + * - if a getter returns undefined the item will be sorted + * before all other items + * - if a getter returns a string it will be compared using + * `String#localeCompare` + * - otherwise comparison is done using subtraction + * - If the values for a getter are equal the next getter is + * used to compare the items. + */ +export const ascending = (...getters: Array>): Comparator => (a, b) => { + for (const getter of getters) { + const valA = getter(a); + const valB = getter(b); + + if (valA === valB) { + continue; + } + if (valA === undefined) { + return -1; + } + if (valB === undefined) { + return 1; + } + + return typeof valA === 'string' || typeof valB === 'string' + ? String(valA).localeCompare(String(valB)) + : valA - valB; + } + + return 0; +}; + +/** + * create a sort comparator that sorts values in descending + * order based on the ...getters + * + * See docs for ascending() + */ +export const descending = (...getters: Array>): Comparator => { + const sorter = ascending(...getters); + return (a, b) => sorter(b, a); +}; + +/** + * Alternate Array#includes() implementation with sane types, functions as a type guard + */ +export const includes = (array: T[], value: any): value is T => array.includes(value); + +/** + * Ponyfill for Object.fromEntries() + */ +export const entriesToObject = (entries: Array): Record => { + const object: Record = {}; + for (const [key, value] of entries) { + object[key] = value; + } + return object; +}; diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts new file mode 100644 index 0000000000000..ec78a1bdf020e --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { Bundle, BundleSpec, parseBundles } from './bundle'; + +jest.mock('fs'); + +const SPEC: BundleSpec = { + contextDir: '/foo/bar', + entry: 'entry', + id: 'bar', + outputDir: '/foo/bar/target', + sourceRoot: '/foo', + type: 'plugin', +}; + +it('creates cache keys', () => { + const bundle = new Bundle(SPEC); + expect( + bundle.createCacheKey( + ['/foo/bar/a', '/foo/bar/c'], + new Map([ + ['/foo/bar/a', 123], + ['/foo/bar/b', 456], + ['/foo/bar/c', 789], + ]) + ) + ).toMatchInlineSnapshot(` + Object { + "mtimes": Object { + "/foo/bar/a": 123, + "/foo/bar/c": 789, + }, + "spec": Object { + "contextDir": "/foo/bar", + "entry": "entry", + "id": "bar", + "outputDir": "/foo/bar/target", + "sourceRoot": "/foo", + "type": "plugin", + }, + } + `); +}); + +it('provides serializable versions of itself', () => { + const bundle = new Bundle(SPEC); + expect(bundle.toSpec()).toEqual(SPEC); +}); + +it('provides the module count from the cache', () => { + const bundle = new Bundle(SPEC); + expect(bundle.cache.getModuleCount()).toBe(undefined); + bundle.cache.set({ moduleCount: 123 }); + expect(bundle.cache.getModuleCount()).toBe(123); +}); + +it('parses bundles from JSON specs', () => { + const bundles = parseBundles(JSON.stringify([SPEC])); + + expect(bundles).toMatchInlineSnapshot(` + Array [ + Bundle { + "cache": BundleCache { + "path": "/foo/bar/target/.kbn-optimizer-cache", + "state": undefined, + }, + "contextDir": "/foo/bar", + "entry": "entry", + "id": "bar", + "outputDir": "/foo/bar/target", + "sourceRoot": "/foo", + "type": "plugin", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts new file mode 100644 index 0000000000000..f1bc0965a46cc --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -0,0 +1,170 @@ +/* + * 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 Path from 'path'; + +import { BundleCache } from './bundle_cache'; +import { UnknownVals } from './ts_helpers'; +import { includes, ascending, entriesToObject } from './array_helpers'; + +const VALID_BUNDLE_TYPES = ['plugin' as const]; + +export interface BundleSpec { + readonly type: typeof VALID_BUNDLE_TYPES[0]; + /** Unique id for this bundle */ + readonly id: string; + /** Webpack entry request for this plugin, relative to the contextDir */ + readonly entry: string; + /** Absolute path to the plugin source directory */ + readonly contextDir: string; + /** Absolute path to the root of the repository */ + readonly sourceRoot: string; + /** Absolute path to the directory where output should be written */ + readonly outputDir: string; +} + +export class Bundle { + /** Bundle type, only "plugin" is supported for now */ + public readonly type: BundleSpec['type']; + /** Unique identifier for this bundle */ + public readonly id: BundleSpec['id']; + /** Path, relative to `contextDir`, to the entry file for the Webpack bundle */ + public readonly entry: BundleSpec['entry']; + /** + * Absolute path to the root of the bundle context (plugin directory) + * where the entry is resolved relative to and the default output paths + * are relative to + */ + public readonly contextDir: BundleSpec['contextDir']; + /** Absolute path to the root of the whole project source, repo root */ + public readonly sourceRoot: BundleSpec['sourceRoot']; + /** Absolute path to the output directory for this bundle */ + public readonly outputDir: BundleSpec['outputDir']; + + public readonly cache: BundleCache; + + constructor(spec: BundleSpec) { + this.type = spec.type; + this.id = spec.id; + this.entry = spec.entry; + this.contextDir = spec.contextDir; + this.sourceRoot = spec.sourceRoot; + this.outputDir = spec.outputDir; + + this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); + } + + /** + * Calculate the cache key for this bundle based from current + * mtime values. + * + * @param mtimes pre-fetched mtimes (ms || undefined) for all referenced files + */ + createCacheKey(files: string[], mtimes: Map): unknown { + return { + spec: this.toSpec(), + mtimes: entriesToObject( + files.map(p => [p, mtimes.get(p)] as const).sort(ascending(e => e[0])) + ), + }; + } + + /** + * Get the raw "specification" for the bundle, this object is JSON serialized + * in the cache key, passed to worker processes so they know what bundles + * to build, and passed to the Bundle constructor to rebuild the Bundle object. + */ + toSpec(): BundleSpec { + return { + type: this.type, + id: this.id, + entry: this.entry, + contextDir: this.contextDir, + sourceRoot: this.sourceRoot, + outputDir: this.outputDir, + }; + } +} + +/** + * Parse a JSON string containing an array of BundleSpec objects into an array + * of Bundle objects, validating everything. + */ +export function parseBundles(json: string) { + try { + if (typeof json !== 'string') { + throw new Error('must be a JSON string'); + } + + const specs: Array> = JSON.parse(json); + + if (!Array.isArray(specs)) { + throw new Error('must be an array'); + } + + return specs.map( + (spec: UnknownVals): Bundle => { + if (!(spec && typeof spec === 'object')) { + throw new Error('`bundles[]` must be an object'); + } + + const { type } = spec; + if (!includes(VALID_BUNDLE_TYPES, type)) { + throw new Error('`bundles[]` must have a valid `type`'); + } + + const { id } = spec; + if (!(typeof id === 'string')) { + throw new Error('`bundles[]` must have a string `id` property'); + } + + const { entry } = spec; + if (!(typeof entry === 'string')) { + throw new Error('`bundles[]` must have a string `entry` property'); + } + + const { contextDir } = spec; + if (!(typeof contextDir === 'string' && Path.isAbsolute(contextDir))) { + throw new Error('`bundles[]` must have an absolute path `contextDir` property'); + } + + const { sourceRoot } = spec; + if (!(typeof sourceRoot === 'string' && Path.isAbsolute(sourceRoot))) { + throw new Error('`bundles[]` must have an absolute path `sourceRoot` property'); + } + + const { outputDir } = spec; + if (!(typeof outputDir === 'string' && Path.isAbsolute(outputDir))) { + throw new Error('`bundles[]` must have an absolute path `outputDir` property'); + } + + return new Bundle({ + type, + id, + entry, + contextDir, + sourceRoot, + outputDir, + }); + } + ); + } catch (error) { + throw new Error(`unable to parse bundles: ${error.message}`); + } +} diff --git a/packages/kbn-optimizer/src/common/bundle_cache.test.ts b/packages/kbn-optimizer/src/common/bundle_cache.test.ts new file mode 100644 index 0000000000000..f6118739045ba --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle_cache.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { BundleCache, State } from './bundle_cache'; + +jest.mock('fs'); +const mockReadFileSync: jest.Mock = jest.requireMock('fs').readFileSync; +const mockMkdirSync: jest.Mock = jest.requireMock('fs').mkdirSync; +const mockWriteFileSync: jest.Mock = jest.requireMock('fs').writeFileSync; + +const SOME_STATE: State = { + cacheKey: 'abc', + files: ['123'], + moduleCount: 123, + optimizerCacheKey: 'abc', +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it(`doesn't complain if files are not on disk`, () => { + const cache = new BundleCache('/foo/bar.json'); + expect(cache.get()).toEqual({}); +}); + +it(`updates files on disk when calling set()`, () => { + const cache = new BundleCache('/foo/bar.json'); + cache.set(SOME_STATE); + expect(mockReadFileSync).not.toHaveBeenCalled(); + expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/foo", + Object { + "recursive": true, + }, + ], + ] + `); + expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/foo/bar.json", + "{ + \\"cacheKey\\": \\"abc\\", + \\"files\\": [ + \\"123\\" + ], + \\"moduleCount\\": 123, + \\"optimizerCacheKey\\": \\"abc\\" + }", + ], + ] + `); +}); + +it(`serves updated state from memory`, () => { + const cache = new BundleCache('/foo/bar.json'); + cache.set(SOME_STATE); + jest.clearAllMocks(); + + expect(cache.get()).toEqual(SOME_STATE); + expect(mockReadFileSync).not.toHaveBeenCalled(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(mockWriteFileSync).not.toHaveBeenCalled(); +}); + +it('reads state from disk on get() after refresh()', () => { + const cache = new BundleCache('/foo/bar.json'); + cache.set(SOME_STATE); + cache.refresh(); + jest.clearAllMocks(); + + cache.get(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(mockWriteFileSync).not.toHaveBeenCalled(); + expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/foo/bar.json", + "utf8", + ], + ] + `); +}); + +it('provides accessors to specific state properties', () => { + const cache = new BundleCache('/foo/bar.json'); + + expect(cache.getModuleCount()).toBe(undefined); + expect(cache.getReferencedFiles()).toEqual(undefined); + expect(cache.getCacheKey()).toEqual(undefined); + expect(cache.getOptimizerCacheKey()).toEqual(undefined); + + cache.set(SOME_STATE); + + expect(cache.getModuleCount()).toBe(123); + expect(cache.getReferencedFiles()).toEqual(['123']); + expect(cache.getCacheKey()).toEqual('abc'); + expect(cache.getOptimizerCacheKey()).toEqual('abc'); +}); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts new file mode 100644 index 0000000000000..1dbc7f1d1b6b0 --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -0,0 +1,97 @@ +/* + * 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 Path from 'path'; + +export interface State { + optimizerCacheKey?: unknown; + cacheKey?: unknown; + moduleCount?: number; + files?: string[]; +} + +const DEFAULT_STATE: State = {}; +const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE); + +/** + * Helper to read and update metadata for bundles. + */ +export class BundleCache { + private state: State | undefined = undefined; + constructor(private readonly path: string | false) {} + + refresh() { + this.state = undefined; + } + + get() { + if (!this.state) { + let json; + try { + if (this.path) { + json = Fs.readFileSync(this.path, 'utf8'); + } + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + let partialCache: Partial; + try { + partialCache = JSON.parse(json || DEFAULT_STATE_JSON); + } catch (error) { + partialCache = {}; + } + + this.state = { + ...DEFAULT_STATE, + ...partialCache, + }; + } + + return this.state; + } + + set(updated: State) { + this.state = updated; + if (this.path) { + const directory = Path.dirname(this.path); + Fs.mkdirSync(directory, { recursive: true }); + Fs.writeFileSync(this.path, JSON.stringify(this.state, null, 2)); + } + } + + public getModuleCount() { + return this.get().moduleCount; + } + + public getReferencedFiles() { + return this.get().files; + } + + public getCacheKey() { + return this.get().cacheKey; + } + + public getOptimizerCacheKey() { + return this.get().optimizerCacheKey; + } +} diff --git a/packages/kbn-optimizer/src/common/compiler_messages.ts b/packages/kbn-optimizer/src/common/compiler_messages.ts new file mode 100644 index 0000000000000..5f2e9d518bfa6 --- /dev/null +++ b/packages/kbn-optimizer/src/common/compiler_messages.ts @@ -0,0 +1,98 @@ +/* + * 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. + */ + +/** + * Message sent when a compiler encouters an unresolvable error. + * The worker will be shut down following this message. + */ +export interface CompilerErrorMsg { + type: 'compiler error'; + id: string; + errorMsg: string; + errorStack?: string; +} + +/** + * Message sent when a compiler starts running, either for the first + * time or because of changes detected when watching. + */ +export interface CompilerRunningMsg { + type: 'running'; + bundleId: string; +} + +/** + * Message sent when a compiler encounters an error that + * prevents the bundle from building correctly. When in + * watch mode these issues can be fixed by the user. + * (ie. unresolved import, syntax error, etc.) + */ +export interface CompilerIssueMsg { + type: 'compiler issue'; + bundleId: string; + failure: string; +} + +/** + * Message sent when a compiler completes successfully and + * the bundle has been written to disk or updated on disk. + */ +export interface CompilerSuccessMsg { + type: 'compiler success'; + bundleId: string; + moduleCount: number; +} + +export type CompilerMsg = CompilerRunningMsg | CompilerIssueMsg | CompilerSuccessMsg; + +export class CompilerMsgs { + constructor(private bundle: string) {} + + running(): CompilerRunningMsg { + return { + bundleId: this.bundle, + type: 'running', + }; + } + + compilerFailure(options: { failure: string }): CompilerIssueMsg { + return { + bundleId: this.bundle, + type: 'compiler issue', + failure: options.failure, + }; + } + + compilerSuccess(options: { moduleCount: number }): CompilerSuccessMsg { + return { + bundleId: this.bundle, + type: 'compiler success', + moduleCount: options.moduleCount, + }; + } + + error(error: Error): CompilerErrorMsg { + return { + id: this.bundle, + type: 'compiler error', + errorMsg: error.message, + errorStack: error.stack, + }; + } +} diff --git a/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts b/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts new file mode 100644 index 0000000000000..60982abff2d87 --- /dev/null +++ b/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts @@ -0,0 +1,69 @@ +/* + * 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 Rx from 'rxjs'; +import { toArray } from 'rxjs/operators'; + +import { summarizeEvent$ } from './event_stream_helpers'; + +it('emits each state with each event, ignoring events when reducer returns undefined', async () => { + const values = await summarizeEvent$( + Rx.of(1, 2, 3, 4, 5), + { + sum: 0, + }, + (state, event) => { + if (event % 2) { + return { + sum: state.sum + event, + }; + } + } + ) + .pipe(toArray()) + .toPromise(); + + expect(values).toMatchInlineSnapshot(` + Array [ + Object { + "state": Object { + "sum": 0, + }, + }, + Object { + "event": 1, + "state": Object { + "sum": 1, + }, + }, + Object { + "event": 3, + "state": Object { + "sum": 4, + }, + }, + Object { + "event": 5, + "state": Object { + "sum": 9, + }, + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/common/event_stream_helpers.ts b/packages/kbn-optimizer/src/common/event_stream_helpers.ts new file mode 100644 index 0000000000000..c1585f79ede6e --- /dev/null +++ b/packages/kbn-optimizer/src/common/event_stream_helpers.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 * as Rx from 'rxjs'; +import { scan, distinctUntilChanged, startWith } from 'rxjs/operators'; + +export interface Update { + event?: Event; + state: State; +} + +export type Summarizer = (prev: State, event: Event) => State | undefined; + +/** + * Transform an event stream into a state update stream which emits + * the events and individual states for each event. + */ +export const summarizeEvent$ = ( + event$: Rx.Observable, + initialState: State, + reducer: Summarizer +) => { + const initUpdate: Update = { + state: initialState, + }; + + return event$.pipe( + scan((prev, event): Update => { + const newState = reducer(prev.state, event); + return newState === undefined + ? prev + : { + event, + state: newState, + }; + }, initUpdate), + distinctUntilChanged(), + startWith(initUpdate) + ); +}; diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts new file mode 100644 index 0000000000000..ea0560f132153 --- /dev/null +++ b/packages/kbn-optimizer/src/common/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './bundle'; +export * from './bundle_cache'; +export * from './worker_config'; +export * from './worker_messages'; +export * from './compiler_messages'; +export * from './ts_helpers'; +export * from './rxjs_helpers'; +export * from './array_helpers'; +export * from './event_stream_helpers'; diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts new file mode 100644 index 0000000000000..72be71e6bf7ec --- /dev/null +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts @@ -0,0 +1,140 @@ +/* + * 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 Rx from 'rxjs'; +import { toArray, map } from 'rxjs/operators'; + +import { pipeClosure, debounceTimeBuffer, maybeMap, maybe } from './rxjs_helpers'; + +jest.useFakeTimers(); + +describe('pipeClosure()', () => { + it('calls closure on each subscription to setup unique state', async () => { + let counter = 0; + + const foo$ = Rx.of(1, 2, 3).pipe( + pipeClosure(source$ => { + const multiplier = ++counter; + return source$.pipe(map(i => i * multiplier)); + }), + toArray() + ); + + await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + Array [ + 1, + 2, + 3, + ] + `); + await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + Array [ + 2, + 4, + 6, + ] + `); + await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + Array [ + 3, + 6, + 9, + ] + `); + }); +}); + +describe('maybe()', () => { + it('filters out undefined values from the stream', async () => { + const foo$ = Rx.of(1, undefined, 2, undefined, 3).pipe(maybe(), toArray()); + + await expect(foo$.toPromise()).resolves.toEqual([1, 2, 3]); + }); +}); + +describe('maybeMap()', () => { + it('calls map fn and filters out undefined values returned', async () => { + const foo$ = Rx.of(1, 2, 3, 4, 5).pipe( + maybeMap(i => (i % 2 ? i : undefined)), + toArray() + ); + + await expect(foo$.toPromise()).resolves.toEqual([1, 3, 5]); + }); +}); + +describe('debounceTimeBuffer()', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('buffers items until there is n milliseconds of silence, then flushes buffer to stream', async () => { + const foo$ = new Rx.Subject(); + const dest = new Rx.BehaviorSubject(undefined); + foo$ + .pipe( + debounceTimeBuffer(100), + map(items => items.reduce((sum, n) => sum + n)) + ) + .subscribe(dest); + + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + + // only wait 99 milliseconds before sending the next value + jest.advanceTimersByTime(99); + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + + // only wait 99 milliseconds before sending the next value + jest.advanceTimersByTime(99); + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + + // send the next value after 100 milliseconds and observe that it was forwarded + jest.advanceTimersByTime(100); + foo$.next(1); + expect(dest.getValue()).toBe(3); + + foo$.complete(); + if (!dest.isStopped) { + throw new Error('Expected destination to stop as soon as the source is completed'); + } + }); + + it('clears queue as soon as source completes if source completes before time is up', () => { + const foo$ = new Rx.Subject(); + const dest = new Rx.BehaviorSubject(undefined); + foo$ + .pipe( + debounceTimeBuffer(100), + map(items => items.reduce((sum, n) => sum + n)) + ) + .subscribe(dest); + + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + foo$.complete(); + expect(dest.getValue()).toBe(1); + }); +}); diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.ts new file mode 100644 index 0000000000000..1114f65bacb19 --- /dev/null +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.ts @@ -0,0 +1,75 @@ +/* + * 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 Rx from 'rxjs'; +import { mergeMap, tap, debounceTime, map } from 'rxjs/operators'; + +type Operator = (source: Rx.Observable) => Rx.Observable; +type MapFn = (item: T1, index: number) => T2; + +/** + * Wrap an operator chain in a closure so that is can have some local + * state. The `fn` is called each time the final observable is + * subscribed so the pipeline/closure is setup for each subscription. + */ +export const pipeClosure = (fn: Operator): Operator => { + return (source: Rx.Observable) => { + return Rx.defer(() => fn(source)); + }; +}; + +/** + * An operator that filters out undefined values from the stream while + * supporting TypeScript + */ +export const maybe = (): Operator => { + return mergeMap(item => (item === undefined ? Rx.EMPTY : [item])); +}; + +/** + * An operator like map(), but undefined values are filered out automatically + * with TypeScript support. For some reason TS doesn't have great support for + * filter's without defining an explicit type assertion in the signature of + * the filter. + */ +export const maybeMap = (fn: MapFn): Operator => { + return mergeMap((item, index) => { + const result = fn(item, index); + return result === undefined ? Rx.EMPTY : [result]; + }); +}; + +/** + * Debounce received notifications and write them to a buffer. Once the source + * has been silent for `ms` milliseconds the buffer is flushed as a single array + * to the destination stream + */ +export const debounceTimeBuffer = (ms: number) => + pipeClosure((source$: Rx.Observable) => { + const buffer: T[] = []; + return source$.pipe( + tap(item => buffer.push(item)), + debounceTime(ms), + map(() => { + const items = Array.from(buffer); + buffer.length = 0; + return items; + }) + ); + }); diff --git a/packages/kbn-optimizer/src/common/ts_helpers.ts b/packages/kbn-optimizer/src/common/ts_helpers.ts new file mode 100644 index 0000000000000..8c0b857d212ac --- /dev/null +++ b/packages/kbn-optimizer/src/common/ts_helpers.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. + */ + +/** + * Convert an object type into an object with the same keys + * but with each value type replaced with `unknown` + */ +export type UnknownVals = { + [k in keyof T]: unknown; +}; diff --git a/packages/kbn-optimizer/src/common/worker_config.ts b/packages/kbn-optimizer/src/common/worker_config.ts new file mode 100644 index 0000000000000..c999260872d0f --- /dev/null +++ b/packages/kbn-optimizer/src/common/worker_config.ts @@ -0,0 +1,93 @@ +/* + * 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 Path from 'path'; + +import { UnknownVals } from './ts_helpers'; + +export interface WorkerConfig { + readonly repoRoot: string; + readonly watch: boolean; + readonly dist: boolean; + readonly cache: boolean; + readonly profileWebpack: boolean; + readonly browserslistEnv: string; + readonly optimizerCacheKey: unknown; +} + +export function parseWorkerConfig(json: string): WorkerConfig { + try { + if (typeof json !== 'string') { + throw new Error('expected worker config to be a JSON string'); + } + + const parsed: UnknownVals = JSON.parse(json); + + if (!(typeof parsed === 'object' && parsed)) { + throw new Error('config must be an object'); + } + + const repoRoot = parsed.repoRoot; + if (typeof repoRoot !== 'string' || !Path.isAbsolute(repoRoot)) { + throw new Error('`repoRoot` config must be an absolute path'); + } + + const cache = parsed.cache; + if (typeof cache !== 'boolean') { + throw new Error('`cache` config must be a boolean'); + } + + const watch = parsed.watch; + if (typeof watch !== 'boolean') { + throw new Error('`watch` config must be a boolean'); + } + + const dist = parsed.dist; + if (typeof dist !== 'boolean') { + throw new Error('`dist` config must be a boolean'); + } + + const profileWebpack = parsed.profileWebpack; + if (typeof profileWebpack !== 'boolean') { + throw new Error('`profileWebpack` must be a boolean'); + } + + const optimizerCacheKey = parsed.optimizerCacheKey; + if (optimizerCacheKey === undefined) { + throw new Error('`optimizerCacheKey` must be defined'); + } + + const browserslistEnv = parsed.browserslistEnv; + if (typeof browserslistEnv !== 'string') { + throw new Error('`browserslistEnv` must be a string'); + } + + return { + repoRoot, + cache, + watch, + dist, + profileWebpack, + optimizerCacheKey, + browserslistEnv, + }; + } catch (error) { + throw new Error(`unable to parse worker config: ${error.message}`); + } +} diff --git a/packages/kbn-optimizer/src/common/worker_messages.ts b/packages/kbn-optimizer/src/common/worker_messages.ts new file mode 100644 index 0000000000000..d3c03f483d7e8 --- /dev/null +++ b/packages/kbn-optimizer/src/common/worker_messages.ts @@ -0,0 +1,64 @@ +/* + * 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 { + CompilerRunningMsg, + CompilerIssueMsg, + CompilerSuccessMsg, + CompilerErrorMsg, +} from './compiler_messages'; + +export type WorkerMsg = + | CompilerRunningMsg + | CompilerIssueMsg + | CompilerSuccessMsg + | CompilerErrorMsg + | WorkerErrorMsg; + +/** + * Message sent when the worker encounters an error that it can't + * recover from, no more messages will be sent and the worker + * will exit after this message. + */ +export interface WorkerErrorMsg { + type: 'worker error'; + errorMsg: string; + errorStack?: string; +} + +const WORKER_STATE_TYPES: ReadonlyArray = [ + 'running', + 'compiler issue', + 'compiler success', + 'compiler error', + 'worker error', +]; + +export const isWorkerMsg = (value: any): value is WorkerMsg => + typeof value === 'object' && value && WORKER_STATE_TYPES.includes(value.type); + +export class WorkerMsgs { + error(error: Error): WorkerErrorMsg { + return { + type: 'worker error', + errorMsg: error.message, + errorStack: error.stack, + }; + } +} diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts new file mode 100644 index 0000000000000..48777f1d54aaf --- /dev/null +++ b/packages/kbn-optimizer/src/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { OptimizerConfig } from './optimizer'; +export * from './run_optimizer'; +export * from './log_optimizer_state'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap new file mode 100644 index 0000000000000..706f79978beee --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -0,0 +1,557 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` +OptimizerConfig { + "bundles": Array [ + Bundle { + "cache": BundleCache { + "path": /plugins/bar/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /plugins/bar, + "entry": "./public/index", + "id": "bar", + "outputDir": /plugins/bar/target/public, + "sourceRoot": , + "type": "plugin", + }, + Bundle { + "cache": BundleCache { + "path": /plugins/foo/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /plugins/foo, + "entry": "./public/index", + "id": "foo", + "outputDir": /plugins/foo/target/public, + "sourceRoot": , + "type": "plugin", + }, + ], + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 1, + "plugins": Array [ + Object { + "directory": /plugins/bar, + "id": "bar", + "isUiPlugin": true, + }, + Object { + "directory": /plugins/baz, + "id": "baz", + "isUiPlugin": false, + }, + Object { + "directory": /plugins/foo, + "id": "foo", + "isUiPlugin": true, + }, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, +} +`; + +exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = ` +"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/bar\\"] = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ \\"../foo/public/ext.ts\\": +/*!****************************!*\\\\ + !*** ../foo/public/ext.ts ***! + \\\\****************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.ext = void 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. + */ +const ext = 'TRUE'; +exports.ext = ext; + +/***/ }), + +/***/ \\"../foo/public/index.ts\\": +/*!******************************!*\\\\ + !*** ../foo/public/index.ts ***! + \\\\******************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); + +var _lib = __webpack_require__(/*! ./lib */ \\"../foo/public/lib.ts\\"); + +Object.keys(_lib).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _lib[key]; + } + }); +}); + +var _ext = __webpack_require__(/*! ./ext */ \\"../foo/public/ext.ts\\"); + +Object.keys(_ext).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _ext[key]; + } + }); +}); + +/***/ }), + +/***/ \\"../foo/public/lib.ts\\": +/*!****************************!*\\\\ + !*** ../foo/public/lib.ts ***! + \\\\****************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.fooLibFn = fooLibFn; + +/* + * 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. + */ +function fooLibFn() { + return 'foo'; +} + +/***/ }), + +/***/ \\"./public/index.ts\\": +/*!*************************!*\\\\ + !*** ./public/index.ts ***! + \\\\*************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +var _exportNames = { + fooLibFn: true +}; +Object.defineProperty(exports, \\"fooLibFn\\", { + enumerable: true, + get: function () { + return _index.fooLibFn; + } +}); + +var _index = __webpack_require__(/*! ../../foo/public/index */ \\"../foo/public/index.ts\\"); + +var _lib = __webpack_require__(/*! ./lib */ \\"./public/lib.ts\\"); + +Object.keys(_lib).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _lib[key]; + } + }); +}); + +/***/ }), + +/***/ \\"./public/lib.ts\\": +/*!***********************!*\\\\ + !*** ./public/lib.ts ***! + \\\\***********************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.barLibFn = barLibFn; + +/* + * 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. + */ +function barLibFn() { + return 'bar'; +} + +/***/ }) + +/******/ })[\\"plugin\\"]; +//# sourceMappingURL=bar.plugin.js.map" +`; + +exports[`builds expected bundles, saves bundle counts to metadata: foo bundle 1`] = ` +"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/foo\\"] = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ \\"./public/ext.ts\\": +/*!***********************!*\\\\ + !*** ./public/ext.ts ***! + \\\\***********************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.ext = void 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. + */ +const ext = 'TRUE'; +exports.ext = ext; + +/***/ }), + +/***/ \\"./public/index.ts\\": +/*!*************************!*\\\\ + !*** ./public/index.ts ***! + \\\\*************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); + +var _lib = __webpack_require__(/*! ./lib */ \\"./public/lib.ts\\"); + +Object.keys(_lib).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _lib[key]; + } + }); +}); + +var _ext = __webpack_require__(/*! ./ext */ \\"./public/ext.ts\\"); + +Object.keys(_ext).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _ext[key]; + } + }); +}); + +/***/ }), + +/***/ \\"./public/lib.ts\\": +/*!***********************!*\\\\ + !*** ./public/lib.ts ***! + \\\\***********************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.fooLibFn = fooLibFn; + +/* + * 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. + */ +function fooLibFn() { + return 'foo'; +} + +/***/ }) + +/******/ })[\\"plugin\\"]; +//# sourceMappingURL=foo.plugin.js.map" +`; diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts new file mode 100644 index 0000000000000..dda818875db23 --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -0,0 +1,155 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import { inspect } from 'util'; + +import cpy from 'cpy'; +import del from 'del'; +import { toArray, tap } from 'rxjs/operators'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, OptimizerUpdate } from '@kbn/optimizer'; + +const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); +const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); +const MOCK_REPO_DIR = Path.resolve(TMP_DIR, 'mock_repo'); + +expect.addSnapshotSerializer(createAbsolutePathSerializer(MOCK_REPO_DIR)); + +beforeEach(async () => { + await del(TMP_DIR); + await cpy('**/*', MOCK_REPO_DIR, { + cwd: MOCK_REPO_SRC, + parents: true, + deep: true, + }); +}); + +afterEach(async () => { + await del(TMP_DIR); +}); + +it('builds expected bundles, saves bundle counts to metadata', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + maxWorkerCount: 1, + }); + + expect(config).toMatchSnapshot('OptimizerConfig'); + + const msgs = await runOptimizer(config) + .pipe( + tap(state => { + if (state.event?.type === 'worker stdio') { + // eslint-disable-next-line no-console + console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + } + }), + toArray() + ) + .toPromise(); + + const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { + if (!truth) { + throw new Error( + `expected optimizer to ${statement}, states: ${inspect(altStates || msgs, { + colors: true, + depth: Infinity, + })}` + ); + } + }; + + const initializingStates = msgs.filter(msg => msg.state.phase === 'initializing'); + assert('produce at least one initializing event', initializingStates.length >= 1); + + const bundleCacheStates = msgs.filter( + msg => + (msg.event?.type === 'bundle cached' || msg.event?.type === 'bundle not cached') && + msg.state.phase === 'initializing' + ); + assert('produce two bundle cache events while initializing', bundleCacheStates.length === 2); + + const initializedStates = msgs.filter(msg => msg.state.phase === 'initialized'); + assert('produce at least one initialized event', initializedStates.length >= 1); + + const workerStarted = msgs.filter(msg => msg.event?.type === 'worker started'); + assert('produce one worker started event', workerStarted.length === 1); + + const runningStates = msgs.filter(msg => msg.state.phase === 'running'); + assert( + 'produce two or three "running" states', + runningStates.length === 2 || runningStates.length === 3 + ); + + const bundleNotCachedEvents = msgs.filter(msg => msg.event?.type === 'bundle not cached'); + assert('produce two "bundle not cached" events', bundleNotCachedEvents.length === 2); + + const successStates = msgs.filter(msg => msg.state.phase === 'success'); + assert( + 'produce one or two "compiler success" states', + successStates.length === 1 || successStates.length === 2 + ); + + const otherStates = msgs.filter( + msg => + msg.state.phase !== 'initializing' && + msg.state.phase !== 'success' && + msg.state.phase !== 'running' && + msg.state.phase !== 'initialized' && + msg.event?.type !== 'bundle not cached' + ); + assert('produce zero unexpected states', otherStates.length === 0, otherStates); + + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/foo.plugin.js'), 'utf8') + ).toMatchSnapshot('foo bundle'); + + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/bar/target/public/bar.plugin.js'), 'utf8') + ).toMatchSnapshot('bar bundle'); + + const foo = config.bundles.find(b => b.id === 'foo')!; + expect(foo).toBeTruthy(); + foo.cache.refresh(); + expect(foo.cache.getModuleCount()).toBe(3); + expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /plugins/foo/public/ext.ts, + /plugins/foo/public/index.ts, + /plugins/foo/public/lib.ts, + ] + `); + + const bar = config.bundles.find(b => b.id === 'bar')!; + expect(bar).toBeTruthy(); + bar.cache.refresh(); + expect(bar.cache.getModuleCount()).toBe(5); + expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /plugins/foo/public/ext.ts, + /plugins/foo/public/index.ts, + /plugins/foo/public/lib.ts, + /plugins/bar/public/index.ts, + /plugins/bar/public/lib.ts, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts new file mode 100644 index 0000000000000..1bfd8d3fd073a --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts @@ -0,0 +1,301 @@ +/* + * 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 Path from 'path'; + +import cpy from 'cpy'; +import del from 'del'; +import { toArray } from 'rxjs/operators'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { getMtimes } from '../optimizer/get_mtimes'; +import { OptimizerConfig } from '../optimizer/optimizer_config'; +import { Bundle } from '../common/bundle'; +import { getBundleCacheEvent$ } from '../optimizer/bundle_cache'; + +const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); +const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); +const MOCK_REPO_DIR = Path.resolve(TMP_DIR, 'mock_repo'); + +expect.addSnapshotSerializer({ + print: () => '', + test: v => v instanceof Bundle, +}); +expect.addSnapshotSerializer(createAbsolutePathSerializer(MOCK_REPO_DIR)); + +beforeEach(async () => { + await del(TMP_DIR); + await cpy('**/*', MOCK_REPO_DIR, { + cwd: MOCK_REPO_SRC, + parents: true, + deep: true, + }); +}); + +afterEach(async () => { + await del(TMP_DIR); +}); + +it('emits "bundle cached" event when everything is updated', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "type": "bundle cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when cacheKey is up to date but caching is disabled in config', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + cache: false, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "reason": "cache disabled", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when optimizerCacheKey is missing', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey: undefined, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "reason": "missing optimizer cache key", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when optimizerCacheKey is outdated, includes diff', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey: 'old', + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "diff": "- Expected + + Received + + - old + + optimizerCacheKey", + "reason": "optimizer cache key mismatch", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when cacheKey is missing', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + + bundle.cache.set({ + cacheKey: undefined, + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "reason": "missing cache key", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when cacheKey is outdated', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + + bundle.cache.set({ + cacheKey: 'old', + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + jest.spyOn(bundle, 'createCacheKey').mockImplementation(() => 'new'); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "diff": "- Expected + + Received + + - old + + new", + "reason": "cache key mismatch", + "type": "bundle not cached", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts new file mode 100644 index 0000000000000..c02a857883a98 --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts @@ -0,0 +1,143 @@ +/* + * 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 Rx from 'rxjs'; +import { map } from 'rxjs/operators'; +import ActualWatchpack from 'watchpack'; + +import { Bundle, ascending } from '../common'; +import { watchBundlesForChanges$ } from '../optimizer/watch_bundles_for_changes'; +import { BundleCacheEvent } from '../optimizer'; + +jest.mock('fs'); +jest.mock('watchpack'); + +const MockWatchPack: jest.MockedClass = jest.requireMock('watchpack'); +const bundleEntryPath = (bundle: Bundle) => `${bundle.contextDir}/${bundle.entry}`; + +const makeTestBundle = (id: string) => { + const bundle = new Bundle({ + type: 'plugin', + id, + contextDir: `/repo/plugins/${id}/public`, + entry: 'index.ts', + outputDir: `/repo/plugins/${id}/target/public`, + sourceRoot: `/repo`, + }); + + bundle.cache.set({ + cacheKey: 'abc', + moduleCount: 1, + optimizerCacheKey: 'abc', + files: [bundleEntryPath(bundle)], + }); + + return bundle; +}; + +const FOO_BUNDLE = makeTestBundle('foo'); +const BAR_BUNDLE = makeTestBundle('bar'); +const BAZ_BUNDLE = makeTestBundle('baz'); +const BOX_BUNDLE = makeTestBundle('box'); +const CAR_BUNDLE = makeTestBundle('car'); +const BUNDLES = [FOO_BUNDLE, BAR_BUNDLE, BAZ_BUNDLE, BOX_BUNDLE, CAR_BUNDLE]; + +const bundleCacheEvent$ = Rx.from(BUNDLES).pipe( + map( + (bundle): BundleCacheEvent => ({ + type: 'bundle cached', + bundle, + }) + ) +); + +beforeEach(async () => { + jest.useFakeTimers(); +}); + +afterEach(async () => { + jest.useRealTimers(); +}); + +it('notifies of changes and completes once all bundles have changed', async () => { + expect.assertions(18); + + const promise = watchBundlesForChanges$(bundleCacheEvent$, Date.now()) + .pipe( + map((event, i) => { + // each time we trigger a change event we get a 'changed detected' event + if (i === 0 || i === 2 || i === 4 || i === 6) { + expect(event).toHaveProperty('type', 'changes detected'); + return; + } + + expect(event).toHaveProperty('type', 'changes'); + // to teach TS what we're doing + if (event.type !== 'changes') { + return; + } + + // first we change foo and bar, and after 1 second get that change comes though + if (i === 1) { + expect(event.bundles).toHaveLength(2); + const [bar, foo] = event.bundles.sort(ascending(b => b.id)); + expect(bar).toHaveProperty('id', 'bar'); + expect(foo).toHaveProperty('id', 'foo'); + } + + // next we change just the baz package and it's represented on its own + if (i === 3) { + expect(event.bundles).toHaveLength(1); + expect(event.bundles[0]).toHaveProperty('id', 'baz'); + } + + // finally we change box and car together + if (i === 5) { + expect(event.bundles).toHaveLength(2); + const [bar, foo] = event.bundles.sort(ascending(b => b.id)); + expect(bar).toHaveProperty('id', 'box'); + expect(foo).toHaveProperty('id', 'car'); + } + }) + ) + .toPromise(); + + expect(MockWatchPack.mock.instances).toHaveLength(1); + const [watcher] = (MockWatchPack.mock.instances as any) as Array>; + expect(watcher.on).toHaveBeenCalledTimes(1); + expect(watcher.on).toHaveBeenCalledWith('change', expect.any(Function)); + const [, changeListener] = watcher.on.mock.calls[0]; + + // foo and bar are changes without 1sec so they are batched + changeListener(bundleEntryPath(FOO_BUNDLE), 'modified'); + jest.advanceTimersByTime(900); + changeListener(bundleEntryPath(BAR_BUNDLE), 'modified'); + jest.advanceTimersByTime(1000); + + // baz is the only change in 1sec so it is on its own + changeListener(bundleEntryPath(BAZ_BUNDLE), 'modified'); + jest.advanceTimersByTime(1000); + + // finish by changing box and car + changeListener(bundleEntryPath(BOX_BUNDLE), 'deleted'); + changeListener(bundleEntryPath(CAR_BUNDLE), 'deleted'); + jest.advanceTimersByTime(1000); + + await expect(promise).resolves.toEqual(undefined); +}); diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts new file mode 100644 index 0000000000000..1ee4e47bfd9ee --- /dev/null +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -0,0 +1,137 @@ +/* + * 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 { inspect } from 'util'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { tap } from 'rxjs/operators'; + +import { OptimizerConfig } from './optimizer'; +import { OptimizerUpdate$ } from './run_optimizer'; +import { CompilerMsg, pipeClosure } from './common'; + +export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { + return pipeClosure((update$: OptimizerUpdate$) => { + const bundleStates = new Map(); + const bundlesThatWereBuilt = new Set(); + let loggedInit = false; + + return update$.pipe( + tap(update => { + const { event, state } = update; + + if (event?.type === 'worker stdio') { + const chunk = event.chunk.toString('utf8'); + log.warning( + `worker`, + event.stream, + chunk.slice(0, chunk.length - (chunk.endsWith('\n') ? 1 : 0)) + ); + } + + if (event?.type === 'bundle not cached') { + log.debug( + `[${event.bundle.id}] bundle not cached because [${event.reason}]${ + event.diff ? `, diff:\n${event.diff}` : '' + }` + ); + } + + if (event?.type === 'bundle cached') { + log.debug(`[${event.bundle.id}] bundle cached`); + } + + if (event?.type === 'worker started') { + let moduleCount = 0; + for (const bundle of event.bundles) { + moduleCount += bundle.cache.getModuleCount() ?? NaN; + } + const mcString = isFinite(moduleCount) ? String(moduleCount) : '?'; + const bcString = String(event.bundles.length); + log.info(`starting worker [${bcString} bundles, ${mcString} modules]`); + } + + if (state.phase === 'reallocating') { + log.debug(`changes detected...`); + return; + } + + if (state.phase === 'initialized') { + if (!loggedInit) { + loggedInit = true; + log.info(`initialized, ${state.offlineBundles.length} bundles cached`); + } + + if (state.onlineBundles.length === 0) { + log.success(`all bundles cached, success after ${state.durSec}`); + } + return; + } + + for (const { bundleId: id, type } of state.compilerStates) { + const prevBundleState = bundleStates.get(id); + + if (type === prevBundleState) { + continue; + } + + if (type === 'running') { + bundlesThatWereBuilt.add(id); + } + + bundleStates.set(id, type); + log.debug( + `[${id}] state = "${type}"${type !== 'running' ? ` after ${state.durSec} sec` : ''}` + ); + } + + if (state.phase === 'running' || state.phase === 'initializing') { + return true; + } + + if (state.phase === 'issue') { + log.error(`webpack compile errors`); + log.indent(4); + for (const b of state.compilerStates) { + if (b.type === 'compiler issue') { + log.error(`[${b.bundleId}] build`); + log.indent(4); + log.error(b.failure); + log.indent(-4); + } + } + log.indent(-4); + return true; + } + + if (state.phase === 'success') { + const buildCount = bundlesThatWereBuilt.size; + bundlesThatWereBuilt.clear(); + log.success( + `${buildCount} bundles compiled successfully after ${state.durSec} sec` + + (config.watch ? ', watching for changes' : '') + ); + return true; + } + + throw new Error(`unhandled optimizer message: ${inspect(update)}`); + }) + ); + }); +} diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts new file mode 100644 index 0000000000000..dd4d5c294dfc8 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts @@ -0,0 +1,226 @@ +/* + * 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. + */ + +jest.mock('fs'); + +import { Bundle } from '../common'; + +import { assignBundlesToWorkers, Assignments } from './assign_bundles_to_workers'; + +const hasModuleCount = (b: Bundle) => b.cache.getModuleCount() !== undefined; +const noModuleCount = (b: Bundle) => b.cache.getModuleCount() === undefined; +const summarizeBundles = (w: Assignments) => + [ + w.moduleCount ? `${w.moduleCount} known modules` : '', + w.newBundles ? `${w.newBundles} new bundles` : '', + ] + .filter(Boolean) + .join(', '); + +const readConfigs = (workers: Assignments[]) => + workers.map( + (w, i) => `worker ${i} (${summarizeBundles(w)}) => ${w.bundles.map(b => b.id).join(',')}` + ); + +const assertReturnVal = (workers: Assignments[]) => { + expect(workers).toBeInstanceOf(Array); + for (const worker of workers) { + expect(worker).toEqual({ + moduleCount: expect.any(Number), + newBundles: expect.any(Number), + bundles: expect.any(Array), + }); + + expect(worker.bundles.filter(noModuleCount).length).toBe(worker.newBundles); + expect( + worker.bundles.filter(hasModuleCount).reduce((sum, b) => sum + b.cache.getModuleCount()!, 0) + ).toBe(worker.moduleCount); + } +}; + +const testBundle = (id: string) => + new Bundle({ + contextDir: `/repo/plugin/${id}/public`, + entry: 'index.ts', + id, + outputDir: `/repo/plugins/${id}/target/public`, + sourceRoot: `/repo`, + type: 'plugin', + }); + +const getBundles = ({ + withCounts = 0, + withoutCounts = 0, +}: { + withCounts?: number; + withoutCounts?: number; +}) => { + const bundles: Bundle[] = []; + + for (let i = 1; i <= withCounts; i++) { + const id = `foo${i}`; + const bundle = testBundle(id); + bundle.cache.set({ moduleCount: i % 5 === 0 ? i * 10 : i }); + bundles.push(bundle); + } + + for (let i = 0; i < withoutCounts; i++) { + const id = `bar${i}`; + bundles.push(testBundle(id)); + } + + return bundles; +}; + +it('creates less workers if maxWorkersCount is larger than bundle count', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 2 }), 10); + + assertReturnVal(workers); + expect(workers.length).toBe(2); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (1 known modules) => foo1", + "worker 1 (2 known modules) => foo2", + ] + `); +}); + +it('assigns unknown plugin counts as evenly as possible', () => { + const workers = assignBundlesToWorkers(getBundles({ withoutCounts: 10 }), 3); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (4 new bundles) => bar9,bar6,bar3,bar0", + "worker 1 (3 new bundles) => bar8,bar5,bar2", + "worker 2 (3 new bundles) => bar7,bar4,bar1", + ] + `); +}); + +it('distributes bundles without module counts evenly after assigning modules with known counts evenly', () => { + const bundles = getBundles({ withCounts: 16, withoutCounts: 10 }); + const workers = assignBundlesToWorkers(bundles, 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (78 known modules, 3 new bundles) => foo5,foo11,foo8,foo6,foo2,foo1,bar9,bar5,bar1", + "worker 1 (78 known modules, 3 new bundles) => foo16,foo14,foo13,foo12,foo9,foo7,foo4,foo3,bar8,bar4,bar0", + "worker 2 (100 known modules, 2 new bundles) => foo10,bar7,bar3", + "worker 3 (150 known modules, 2 new bundles) => foo15,bar6,bar2", + ] + `); +}); + +it('distributes 2 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 2 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (1 known modules) => foo1", + "worker 1 (2 known modules) => foo2", + ] + `); +}); + +it('distributes 5 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 5 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (3 known modules) => foo2,foo1", + "worker 1 (3 known modules) => foo3", + "worker 2 (4 known modules) => foo4", + "worker 3 (50 known modules) => foo5", + ] + `); +}); + +it('distributes 10 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 10 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (20 known modules) => foo9,foo6,foo4,foo1", + "worker 1 (20 known modules) => foo8,foo7,foo3,foo2", + "worker 2 (50 known modules) => foo5", + "worker 3 (100 known modules) => foo10", + ] + `); +}); + +it('distributes 15 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 15 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (70 known modules) => foo14,foo13,foo12,foo11,foo9,foo6,foo4,foo1", + "worker 1 (70 known modules) => foo5,foo8,foo7,foo3,foo2", + "worker 2 (100 known modules) => foo10", + "worker 3 (150 known modules) => foo15", + ] + `); +}); + +it('distributes 20 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 20 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (153 known modules) => foo15,foo3", + "worker 1 (153 known modules) => foo10,foo16,foo13,foo11,foo7,foo6", + "worker 2 (154 known modules) => foo5,foo19,foo18,foo17,foo14,foo12,foo9,foo8,foo4,foo2,foo1", + "worker 3 (200 known modules) => foo20", + ] + `); +}); + +it('distributes 25 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 25 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (250 known modules) => foo20,foo17,foo13,foo9,foo8,foo2,foo1", + "worker 1 (250 known modules) => foo15,foo23,foo22,foo18,foo16,foo11,foo7,foo3", + "worker 2 (250 known modules) => foo10,foo5,foo24,foo21,foo19,foo14,foo12,foo6,foo4", + "worker 3 (250 known modules) => foo25", + ] + `); +}); + +it('distributes 30 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 30 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (352 known modules) => foo30,foo22,foo14,foo11,foo4,foo1", + "worker 1 (352 known modules) => foo15,foo10,foo28,foo24,foo19,foo16,foo9,foo6", + "worker 2 (353 known modules) => foo20,foo5,foo29,foo23,foo21,foo13,foo12,foo3,foo2", + "worker 3 (353 known modules) => foo25,foo27,foo26,foo18,foo17,foo8,foo7", + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts new file mode 100644 index 0000000000000..001783b167c7a --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts @@ -0,0 +1,121 @@ +/* + * 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 { Bundle, descending, ascending } from '../common'; + +// helper types used inside getWorkerConfigs so we don't have +// to calculate moduleCounts over and over + +export interface Assignments { + moduleCount: number; + newBundles: number; + bundles: Bundle[]; +} + +/** assign a wrapped bundle to a worker */ +const assignBundle = (worker: Assignments, bundle: Bundle) => { + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount !== undefined) { + worker.moduleCount += moduleCount; + } else { + worker.newBundles += 1; + } + + worker.bundles.push(bundle); +}; + +/** + * Create WorkerConfig objects for each worker we will use to build the bundles. + * + * We need to evenly assign bundles to workers so that each worker will have + * about the same amount of work to do. We do this by tracking the module count + * of each bundle in the OptimizerCache and determining the overall workload + * of a worker by the sum of modules it will have to compile for all of its + * bundles. + * + * We only know the module counts after the first build of a new bundle, so + * when we encounter a bundle without a module count in the cache we just + * assign them to workers round-robin, starting with the workers which have + * the smallest number of modules to build. + */ +export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number) { + const workerCount = Math.min(bundles.length, maxWorkerCount); + const workers: Assignments[] = []; + for (let i = 0; i < workerCount; i++) { + workers.push({ + moduleCount: 0, + newBundles: 0, + bundles: [], + }); + } + + /** + * separate the bundles which do and don't have module + * counts and sort them by [moduleCount, id] + */ + const bundlesWithCountsDesc = bundles + .filter(b => b.cache.getModuleCount() !== undefined) + .sort( + descending( + b => b.cache.getModuleCount(), + b => b.id + ) + ); + const bundlesWithoutModuleCounts = bundles + .filter(b => b.cache.getModuleCount() === undefined) + .sort(descending(b => b.id)); + + /** + * assign largest bundles to the smallest worker until it is + * no longer the smallest worker and repeat until all bundles + * with module counts are assigned + */ + while (bundlesWithCountsDesc.length) { + const [smallestWorker, nextSmallestWorker] = workers.sort(ascending(w => w.moduleCount)); + + while (!nextSmallestWorker || smallestWorker.moduleCount <= nextSmallestWorker.moduleCount) { + const bundle = bundlesWithCountsDesc.shift(); + + if (!bundle) { + break; + } + + assignBundle(smallestWorker, bundle); + } + } + + /** + * assign bundles without module counts to workers round-robin + * starting with the smallest workers + */ + workers.sort(ascending(w => w.moduleCount)); + while (bundlesWithoutModuleCounts.length) { + for (const worker of workers) { + const bundle = bundlesWithoutModuleCounts.shift(); + + if (!bundle) { + break; + } + + assignBundle(worker, bundle); + } + } + + return workers; +} diff --git a/packages/kbn-optimizer/src/optimizer/bundle_cache.ts b/packages/kbn-optimizer/src/optimizer/bundle_cache.ts new file mode 100644 index 0000000000000..55e8e1d3fd084 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/bundle_cache.ts @@ -0,0 +1,132 @@ +/* + * 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 Rx from 'rxjs'; +import { mergeAll } from 'rxjs/operators'; + +import { Bundle } from '../common'; + +import { OptimizerConfig } from './optimizer_config'; +import { getMtimes } from './get_mtimes'; +import { diffCacheKey } from './cache_keys'; + +export type BundleCacheEvent = BundleNotCachedEvent | BundleCachedEvent; + +export interface BundleNotCachedEvent { + type: 'bundle not cached'; + reason: + | 'missing optimizer cache key' + | 'optimizer cache key mismatch' + | 'missing cache key' + | 'cache key mismatch' + | 'cache disabled'; + diff?: string; + bundle: Bundle; +} + +export interface BundleCachedEvent { + type: 'bundle cached'; + bundle: Bundle; +} + +export function getBundleCacheEvent$( + config: OptimizerConfig, + optimizerCacheKey: unknown +): Rx.Observable { + return Rx.defer(async () => { + const events: BundleCacheEvent[] = []; + const eligibleBundles: Bundle[] = []; + + for (const bundle of config.bundles) { + if (!config.cache) { + events.push({ + type: 'bundle not cached', + reason: 'cache disabled', + bundle, + }); + continue; + } + + const cachedOptimizerCacheKeys = bundle.cache.getOptimizerCacheKey(); + if (!cachedOptimizerCacheKeys) { + events.push({ + type: 'bundle not cached', + reason: 'missing optimizer cache key', + bundle, + }); + continue; + } + + const optimizerCacheKeyDiff = diffCacheKey(cachedOptimizerCacheKeys, optimizerCacheKey); + if (optimizerCacheKeyDiff !== undefined) { + events.push({ + type: 'bundle not cached', + reason: 'optimizer cache key mismatch', + diff: optimizerCacheKeyDiff, + bundle, + }); + continue; + } + + if (!bundle.cache.getCacheKey()) { + events.push({ + type: 'bundle not cached', + reason: 'missing cache key', + bundle, + }); + continue; + } + + eligibleBundles.push(bundle); + } + + const mtimes = await getMtimes( + new Set( + eligibleBundles.reduce( + (acc: string[], bundle) => [...acc, ...(bundle.cache.getReferencedFiles() || [])], + [] + ) + ) + ); + + for (const bundle of eligibleBundles) { + const diff = diffCacheKey( + bundle.cache.getCacheKey(), + bundle.createCacheKey(bundle.cache.getReferencedFiles() || [], mtimes) + ); + + if (diff) { + events.push({ + type: 'bundle not cached', + reason: 'cache key mismatch', + diff, + bundle, + }); + continue; + } + + events.push({ + type: 'bundle cached', + bundle, + }); + } + + return events; + }).pipe(mergeAll()); +} diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts new file mode 100644 index 0000000000000..44234acd897dc --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -0,0 +1,178 @@ +/* + * 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 jestDiff from 'jest-diff'; +import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; +import { OptimizerConfig } from './optimizer_config'; + +jest.mock('./get_changes.ts'); +jest.mock('execa'); +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { + expect(cmd).toBe('git'); + expect(args).toEqual([ + 'log', + '-n', + '1', + '--pretty=format:%H', + '--', + expect.stringContaining('kbn-optimizer'), + ]); + expect(opts).toEqual({ + cwd: REPO_ROOT, + }); + + return { + stdout: '', + }; +}); + +jest.requireMock('./get_changes.ts').getChanges.mockImplementation( + async () => + new Map([ + ['/foo/bar/a', 'modified'], + ['/foo/bar/b', 'modified'], + ['/foo/bar/c', 'deleted'], + ]) +); + +describe('getOptimizerCacheKey()', () => { + it('uses latest commit and changes files to create unique value', async () => { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + }); + + await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` + Object { + "deletedPaths": Array [ + "/foo/bar/c", + ], + "lastCommit": "", + "modifiedPaths": Object {}, + "workerConfig": Object { + "browserslistEnv": "dev", + "cache": true, + "dist": false, + "optimizerCacheKey": "♻", + "profileWebpack": false, + "repoRoot": , + "watch": false, + }, + } + `); + }); +}); + +describe('diffCacheKey()', () => { + it('returns undefined if values are equal', () => { + expect(diffCacheKey('1', '1')).toBe(undefined); + expect(diffCacheKey(1, 1)).toBe(undefined); + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { a: 'b' }])).toBe(undefined); + expect( + diffCacheKey( + { + a: '1', + b: '2', + }, + { + b: '2', + a: '1', + } + ) + ).toBe(undefined); + }); + + it('returns a diff if the values are different', () => { + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { b: 'a' }])).toMatchInlineSnapshot(` + "- Expected + + Received + +  Array [ +  \\"1\\", +  \\"2\\", +  Object { + - \\"a\\": \\"b\\", + + \\"b\\": \\"a\\", +  }, +  ]" + `); + expect( + diffCacheKey( + { + a: '1', + b: '1', + }, + { + b: '2', + a: '2', + } + ) + ).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": \\"1\\", + - \\"b\\": \\"1\\", + + \\"a\\": \\"2\\", + + \\"b\\": \\"2\\", +  }" + `); + }); +}); + +describe('reformatJestDiff()', () => { + it('reformats large jestDiff output to focus on the changed lines', () => { + const diff = jestDiff( + { + a: ['1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1', '1', '1', '1', '1', '1'], + }, + { + b: ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1'], + } + ); + + expect(reformatJestDiff(diff)).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": Array [ + + \\"b\\": Array [ +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + - \\"2\\", +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + + \\"2\\", +  \\"1\\", +  \\"1\\", +  ..." + `); + }); +}); diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.ts new file mode 100644 index 0000000000000..3529ffa587f16 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.ts @@ -0,0 +1,155 @@ +/* + * 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 Path from 'path'; + +import Chalk from 'chalk'; +import execa from 'execa'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import stripAnsi from 'strip-ansi'; + +import jestDiff from 'jest-diff'; +import jsonStable from 'json-stable-stringify'; +import { ascending, WorkerConfig } from '../common'; + +import { getMtimes } from './get_mtimes'; +import { getChanges } from './get_changes'; +import { OptimizerConfig } from './optimizer_config'; + +const OPTIMIZER_DIR = Path.dirname(require.resolve('../../package.json')); +const RELATIVE_DIR = Path.relative(REPO_ROOT, OPTIMIZER_DIR); + +export function diffCacheKey(expected?: unknown, actual?: unknown) { + if (jsonStable(expected) === jsonStable(actual)) { + return; + } + + return reformatJestDiff(jestDiff(expected, actual)); +} + +export function reformatJestDiff(diff: string | null) { + const diffLines = diff?.split('\n') || []; + + if ( + diffLines.length < 4 || + stripAnsi(diffLines[0]) !== '- Expected' || + stripAnsi(diffLines[1]) !== '+ Received' + ) { + throw new Error(`unexpected diff format: ${diff}`); + } + + const outputLines = [diffLines.shift(), diffLines.shift(), diffLines.shift()]; + + /** + * buffer which contains between 0 and 5 lines from the diff which aren't additions or + * deletions. The first three are the first three lines seen since the buffer was cleared + * and the last two lines are the last two lines seen. + * + * When flushContext() is called we write the first two lines to output, an elipses if there + * are five lines, and then the last two lines. + * + * At the very end we will write the last two lines of context if they're defined + */ + const contextBuffer: string[] = []; + + /** + * Convert a line to an empty line with elipses placed where the text on that line starts + */ + const toElipses = (line: string) => { + return stripAnsi(line).replace(/^(\s*).*/, '$1...'); + }; + + while (diffLines.length) { + const line = diffLines.shift()!; + const plainLine = stripAnsi(line); + if (plainLine.startsWith('+ ') || plainLine.startsWith('- ')) { + // write contextBuffer to the outputLines + if (contextBuffer.length) { + outputLines.push( + ...contextBuffer.slice(0, 2), + ...(contextBuffer.length === 5 + ? [Chalk.dim(toElipses(contextBuffer[2])), ...contextBuffer.slice(3, 5)] + : contextBuffer.slice(2, 4)) + ); + + contextBuffer.length = 0; + } + + // add this line to the outputLines + outputLines.push(line); + } else { + // update the contextBuffer with this line which doesn't represent a change + if (contextBuffer.length === 5) { + contextBuffer[3] = contextBuffer[4]; + contextBuffer[4] = line; + } else { + contextBuffer.push(line); + } + } + } + + if (contextBuffer.length) { + outputLines.push( + ...contextBuffer.slice(0, 2), + ...(contextBuffer.length > 2 ? [Chalk.dim(toElipses(contextBuffer[2]))] : []) + ); + } + + return outputLines.join('\n'); +} + +export interface OptimizerCacheKey { + readonly lastCommit: string | undefined; + readonly workerConfig: WorkerConfig; + readonly deletedPaths: string[]; + readonly modifiedPaths: Record; +} + +async function getLastCommit() { + const { stdout } = await execa( + 'git', + ['log', '-n', '1', '--pretty=format:%H', '--', RELATIVE_DIR], + { + cwd: REPO_ROOT, + } + ); + + return stdout.trim() || undefined; +} + +export async function getOptimizerCacheKey(config: OptimizerConfig) { + const changes = Array.from((await getChanges(OPTIMIZER_DIR)).entries()); + + const cacheKeys: OptimizerCacheKey = { + lastCommit: await getLastCommit(), + workerConfig: config.getWorkerConfig('♻'), + deletedPaths: changes.filter(e => e[1] === 'deleted').map(e => e[0]), + modifiedPaths: {} as Record, + }; + + const modified = changes.filter(e => e[1] === 'modified').map(e => e[0]); + const mtimes = await getMtimes(modified); + for (const [path, mtime] of Array.from(mtimes.entries()).sort(ascending(e => e[0]))) { + if (typeof mtime === 'number') { + cacheKeys.modifiedPaths[path] = mtime; + } + } + + return cacheKeys; +} diff --git a/packages/kbn-optimizer/src/optimizer/get_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_bundles.test.ts new file mode 100644 index 0000000000000..9d95d883d605c --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_bundles.test.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 { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { getBundles } from './get_bundles'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); + +it('returns a bundle for each plugin', () => { + expect( + getBundles( + [ + { + directory: '/repo/plugins/foo', + id: 'foo', + isUiPlugin: true, + }, + { + directory: '/repo/plugins/bar', + id: 'bar', + isUiPlugin: false, + }, + { + directory: '/outside/of/repo/plugins/baz', + id: 'baz', + isUiPlugin: true, + }, + ], + '/repo' + ).map(b => b.toSpec()) + ).toMatchInlineSnapshot(` + Array [ + Object { + "contextDir": /plugins/foo, + "entry": "./public/index", + "id": "foo", + "outputDir": /plugins/foo/target/public, + "sourceRoot": , + "type": "plugin", + }, + Object { + "contextDir": "/outside/of/repo/plugins/baz", + "entry": "./public/index", + "id": "baz", + "outputDir": "/outside/of/repo/plugins/baz/target/public", + "sourceRoot": , + "type": "plugin", + }, + ] + `); +}); diff --git a/src/cli/log.js b/packages/kbn-optimizer/src/optimizer/get_bundles.ts similarity index 60% rename from src/cli/log.js rename to packages/kbn-optimizer/src/optimizer/get_bundles.ts index 917d06c42c7ca..7cd7bf15317e0 100644 --- a/src/cli/log.js +++ b/packages/kbn-optimizer/src/optimizer/get_bundles.ts @@ -17,18 +17,24 @@ * under the License. */ -import _ from 'lodash'; +import Path from 'path'; -const log = _.restParam(function(color, label, rest1) { - console.log.apply(console, [color(` ${_.trim(label)} `)].concat(rest1)); -}); +import { Bundle } from '../common'; -import { green, yellow, red } from './color'; +import { KibanaPlatformPlugin } from './kibana_platform_plugins'; -export default class Log { - constructor(quiet, silent) { - this.good = quiet || silent ? _.noop : _.partial(log, green); - this.warn = quiet || silent ? _.noop : _.partial(log, yellow); - this.bad = silent ? _.noop : _.partial(log, red); - } +export function getBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + return plugins + .filter(p => p.isUiPlugin) + .map( + p => + new Bundle({ + type: 'plugin', + id: p.id, + entry: './public/index', + sourceRoot: repoRoot, + contextDir: p.directory, + outputDir: Path.resolve(p.directory, 'target/public'), + }) + ); } diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.test.ts b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts new file mode 100644 index 0000000000000..04a6dfb3e3625 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_changes.test.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. + */ + +jest.mock('execa'); + +import { getChanges } from './get_changes'; + +const execa: jest.Mock = jest.requireMock('execa'); + +it('parses git ls-files output', async () => { + expect.assertions(4); + + execa.mockImplementation((cmd, args, options) => { + expect(cmd).toBe('git'); + expect(args).toEqual(['ls-files', '-dmt', '--', '/foo/bar/x']); + expect(options).toEqual({ + cwd: '/foo/bar/x', + }); + + return { + stdout: [ + 'C kbn-optimizer/package.json', + 'C kbn-optimizer/src/common/bundle.ts', + 'R kbn-optimizer/src/common/bundles.ts', + 'C kbn-optimizer/src/common/bundles.ts', + 'R kbn-optimizer/src/get_bundle_definitions.test.ts', + 'C kbn-optimizer/src/get_bundle_definitions.test.ts', + ].join('\n'), + }; + }); + + await expect(getChanges('/foo/bar/x')).resolves.toMatchInlineSnapshot(` + Map { + "/foo/bar/x/kbn-optimizer/package.json" => "modified", + "/foo/bar/x/kbn-optimizer/src/common/bundle.ts" => "modified", + "/foo/bar/x/kbn-optimizer/src/common/bundles.ts" => "deleted", + "/foo/bar/x/kbn-optimizer/src/get_bundle_definitions.test.ts" => "deleted", + } + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.ts b/packages/kbn-optimizer/src/optimizer/get_changes.ts new file mode 100644 index 0000000000000..0c03b029c0dc4 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_changes.ts @@ -0,0 +1,63 @@ +/* + * 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 Path from 'path'; + +import execa from 'execa'; + +export type Changes = Map; + +/** + * get the changes in all the context directories (plugin public paths) + */ +export async function getChanges(dir: string) { + const { stdout } = await execa('git', ['ls-files', '-dmt', '--', dir], { + cwd: dir, + }); + + const changes: Changes = new Map(); + const output = stdout.trim(); + + if (output) { + for (const line of output.split('\n')) { + const [tag, ...pathParts] = line.trim().split(' '); + const path = Path.resolve(dir, pathParts.join(' ')); + switch (tag) { + case 'M': + case 'C': + // for some reason ls-files returns deleted files as both deleted + // and modified, so make sure not to overwrite changes already + // tracked as "deleted" + if (changes.get(path) !== 'deleted') { + changes.set(path, 'modified'); + } + break; + + case 'R': + changes.set(path, 'deleted'); + break; + + default: + throw new Error(`unexpected path status ${tag} for path ${path}`); + } + } + } + + return changes; +} diff --git a/packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts b/packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts new file mode 100644 index 0000000000000..e1ecd3f1078ad --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts @@ -0,0 +1,46 @@ +/* + * 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. + */ + +jest.mock('fs'); + +import { getMtimes } from './get_mtimes'; + +const { stat }: { stat: jest.Mock } = jest.requireMock('fs'); + +it('returns mtimes Map', async () => { + stat.mockImplementation((path, cb) => { + if (path.includes('missing')) { + const error = new Error('file not found'); + (error as any).code = 'ENOENT'; + cb(error); + } else { + cb(null, { + mtimeMs: 1234, + }); + } + }); + + await expect(getMtimes(['/foo/bar', '/foo/missing', '/foo/baz', '/foo/bar'])).resolves + .toMatchInlineSnapshot(` + Map { + "/foo/bar" => 1234, + "/foo/baz" => 1234, + } + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/get_mtimes.ts b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts new file mode 100644 index 0000000000000..9ac156cb5b8de --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts @@ -0,0 +1,47 @@ +/* + * 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 * as Rx from 'rxjs'; +import { mergeMap, toArray, map, catchError } from 'rxjs/operators'; + +const stat$ = Rx.bindNodeCallback(Fs.stat); + +/** + * get mtimes of referenced paths concurrently, limit concurrency to 100 + */ +export async function getMtimes(paths: Iterable) { + return await Rx.from(paths) + .pipe( + // map paths to [path, mtimeMs] entries with concurrency of + // 100 at a time, ignoring missing paths + mergeMap( + path => + stat$(path).pipe( + map(stat => [path, stat.mtimeMs] as const), + catchError((error: any) => (error?.code === 'ENOENT' ? Rx.EMPTY : Rx.throwError(error))) + ), + 100 + ), + toArray(), + map(entries => new Map(entries)) + ) + .toPromise(); +} diff --git a/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts new file mode 100644 index 0000000000000..b7f14cf3c517f --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/index.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. + */ + +export * from './optimizer_config'; +export { WorkerStdio } from './observe_worker'; +export * from './optimizer_reducer'; +export * from './cache_keys'; +export * from './watch_bundles_for_changes'; +export * from './run_workers'; +export * from './bundle_cache'; diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts new file mode 100644 index 0000000000000..e047b6d1e44cf --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts @@ -0,0 +1,60 @@ +/* + * 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 Path from 'path'; + +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { findKibanaPlatformPlugins } from './kibana_platform_plugins'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +const FIXTURES_PATH = Path.resolve(__dirname, '../__fixtures__'); + +it('parses kibana.json files of plugins found in pluginDirs', () => { + expect( + findKibanaPlatformPlugins( + [Path.resolve(FIXTURES_PATH, 'mock_repo/plugins')], + [Path.resolve(FIXTURES_PATH, 'mock_repo/test_plugins/test_baz')] + ) + ).toMatchInlineSnapshot(` + Array [ + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar, + "id": "bar", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz, + "id": "baz", + "isUiPlugin": false, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo, + "id": "foo", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz, + "id": "test_baz", + "isUiPlugin": false, + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts new file mode 100644 index 0000000000000..b7e5e12f46a7f --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -0,0 +1,69 @@ +/* + * 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 Path from 'path'; + +import globby from 'globby'; +import loadJsonFile from 'load-json-file'; + +export interface KibanaPlatformPlugin { + readonly directory: string; + readonly id: string; + readonly isUiPlugin: boolean; +} + +/** + * Helper to find the new platform plugins. + */ +export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { + return globby + .sync( + Array.from( + new Set([ + ...scanDirs.map(dir => `${dir}/*/kibana.json`), + ...paths.map(path => `${path}/kibana.json`), + ]) + ), + { + absolute: true, + } + ) + .map(path => readKibanaPlatformPlugin(path)); +} + +function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { + if (!Path.isAbsolute(manifestPath)) { + throw new TypeError('expected new platform manifest path to be absolute'); + } + + const manifest = loadJsonFile.sync(manifestPath); + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); + } + + if (typeof manifest.id !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string id'); + } + + return { + directory: Path.dirname(manifestPath), + id: manifest.id, + isUiPlugin: !!manifest.ui, + }; +} diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts new file mode 100644 index 0000000000000..bfc853e5a6b75 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -0,0 +1,199 @@ +/* + * 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 { fork, ChildProcess } from 'child_process'; +import { Readable } from 'stream'; +import { inspect } from 'util'; + +import * as Rx from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; + +import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle } from '../common'; + +import { OptimizerConfig } from './optimizer_config'; + +export interface WorkerStdio { + type: 'worker stdio'; + stream: 'stdout' | 'stderr'; + chunk: Buffer; +} + +export interface WorkerStarted { + type: 'worker started'; + bundles: Bundle[]; +} + +export type WorkerStatus = WorkerStdio | WorkerStarted; + +interface ProcResource extends Rx.Unsubscribable { + proc: ChildProcess; +} +const isNumeric = (input: any) => String(input).match(/^[0-9]+$/); + +let inspectPortCounter = 9230; +const inspectFlagIndex = process.execArgv.findIndex(flag => flag.startsWith('--inspect')); +let inspectFlag: string | undefined; +if (inspectFlagIndex !== -1) { + const argv = process.execArgv[inspectFlagIndex]; + if (argv.includes('=')) { + // --inspect=port + const [flag, port] = argv.split('='); + inspectFlag = flag; + inspectPortCounter = Number.parseInt(port, 10) + 1; + } else { + // --inspect + inspectFlag = argv; + if (isNumeric(process.execArgv[inspectFlagIndex + 1])) { + // --inspect port + inspectPortCounter = Number.parseInt(process.execArgv[inspectFlagIndex + 1], 10) + 1; + } + } +} + +function usingWorkerProc( + config: OptimizerConfig, + workerConfig: WorkerConfig, + bundles: Bundle[], + fn: (proc: ChildProcess) => Rx.Observable +) { + return Rx.using( + (): ProcResource => { + const args = [JSON.stringify(workerConfig), JSON.stringify(bundles.map(b => b.toSpec()))]; + + const proc = fork(require.resolve('../worker/run_worker'), args, { + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + execArgv: [ + ...(inspectFlag && config.inspectWorkers + ? [`${inspectFlag}=${inspectPortCounter++}`] + : []), + ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), + ], + }); + + return { + proc, + unsubscribe() { + proc.kill('SIGKILL'); + }, + }; + }, + + resource => { + const { proc } = resource as ProcResource; + return fn(proc); + } + ); +} + +function observeStdio$(stream: Readable, name: WorkerStdio['stream']) { + return Rx.fromEvent(stream, 'data').pipe( + takeUntil( + Rx.race( + Rx.fromEvent(stream, 'end'), + Rx.fromEvent(stream, 'error').pipe( + map(error => { + throw error; + }) + ) + ) + ), + map( + (chunk): WorkerStdio => ({ + type: 'worker stdio', + chunk, + stream: name, + }) + ) + ); +} + +/** + * Start a worker process with the specified `workerConfig` and + * `bundles` and return an observable of the events related to + * that worker, including the messages sent to us by that worker + * and the status of the process (stdio, started). + */ +export function observeWorker( + config: OptimizerConfig, + workerConfig: WorkerConfig, + bundles: Bundle[] +): Rx.Observable { + return usingWorkerProc(config, workerConfig, bundles, proc => { + let lastMsg: WorkerMsg; + + return Rx.merge( + Rx.of({ + type: 'worker started', + bundles, + }), + observeStdio$(proc.stdout, 'stdout'), + observeStdio$(proc.stderr, 'stderr'), + Rx.fromEvent<[unknown]>(proc, 'message') + .pipe( + // validate the messages from the process + map(([msg]) => { + if (!isWorkerMsg(msg)) { + throw new Error(`unexpected message from worker: ${JSON.stringify(msg)}`); + } + + lastMsg = msg; + return msg; + }) + ) + .pipe( + takeUntil( + Rx.race( + // throw into stream on error events + Rx.fromEvent(proc, 'error').pipe( + map(error => { + throw new Error(`worker failed to spawn: ${error.message}`); + }) + ), + + // throw into stream on unexpected exits, or emit to trigger the stream to close + Rx.fromEvent<[number | void]>(proc, 'exit').pipe( + map(([code]) => { + const terminalMsgTypes: Array = [ + 'compiler error', + 'worker error', + ]; + + if (!config.watch) { + terminalMsgTypes.push('compiler issue', 'compiler success'); + } + + // verify that this is an expected exit state + if (code === 0 && lastMsg && terminalMsgTypes.includes(lastMsg.type)) { + // emit undefined so that takeUntil completes the observable + return; + } + + throw new Error( + `worker exitted unexpectedly with code ${code} [last message: ${inspect( + lastMsg + )}]` + ); + }) + ) + ) + ) + ) + ); + }); +} diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts new file mode 100644 index 0000000000000..d67b957416753 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -0,0 +1,408 @@ +/* + * 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. + */ + +jest.mock('./assign_bundles_to_workers.ts'); +jest.mock('./kibana_platform_plugins.ts'); +jest.mock('./get_bundles.ts'); + +import Path from 'path'; +import Os from 'os'; + +import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { OptimizerConfig } from './optimizer_config'; + +jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any); + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +beforeEach(() => { + delete process.env.KBN_OPTIMIZER_MAX_WORKERS; + delete process.env.KBN_OPTIMIZER_NO_CACHE; + jest.clearAllMocks(); +}); + +describe('OptimizerConfig::parseOptions()', () => { + it('validates that repoRoot is absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ repoRoot: 'foo/bar' }) + ).toThrowErrorMatchingInlineSnapshot(`"repoRoot must be an absolute path"`); + }); + + it('validates that pluginScanDirs are absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: ['foo/bar'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"pluginScanDirs must all be absolute paths"`); + }); + + it('validates that pluginPaths are absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginPaths: ['foo/bar'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"pluginPaths must all be absolute paths"`); + }); + + it('validates that extraPluginScanDirs are absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + extraPluginScanDirs: ['foo/bar'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"extraPluginScanDirs must all be absolute paths"`); + }); + + it('validates that maxWorkerCount is a number', () => { + expect(() => { + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + maxWorkerCount: NaN, + }); + }).toThrowErrorMatchingInlineSnapshot(`"worker count must be a number"`); + }); + + it('applies defaults', () => { + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /x-pack/plugins, + /plugins, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + cache: false, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /x-pack/plugins, + /plugins, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + examples: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /x-pack/plugins, + /plugins, + /examples, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + oss: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /plugins, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [Path.resolve(REPO_ROOT, 'x/y/z'), '/outside/of/repo'], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /x/y/z, + "/outside/of/repo", + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_MAX_WORKERS = '100'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_NO_CACHE = '0'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_NO_CACHE = '1'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_NO_CACHE = '1'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + cache: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + delete process.env.KBN_OPTIMIZER_NO_CACHE; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + cache: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + }); +}); + +/** + * NOTE: this method is basically just calling others, so we're mocking out the return values + * of each function with a Symbol, including the return values of OptimizerConfig.parseOptions + * and just making sure that the arguments are coming from where we expect + */ +describe('OptimizerConfig::create()', () => { + const assignBundlesToWorkers: jest.Mock = jest.requireMock('./assign_bundles_to_workers.ts') + .assignBundlesToWorkers; + const findKibanaPlatformPlugins: jest.Mock = jest.requireMock('./kibana_platform_plugins.ts') + .findKibanaPlatformPlugins; + const getBundles: jest.Mock = jest.requireMock('./get_bundles.ts').getBundles; + + beforeEach(() => { + if ('mock' in OptimizerConfig.parseOptions) { + (OptimizerConfig.parseOptions as jest.Mock).mockRestore(); + } + + assignBundlesToWorkers.mockReturnValue([ + { config: Symbol('worker config 1') }, + { config: Symbol('worker config 2') }, + ]); + findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); + getBundles.mockReturnValue(Symbol('bundles')); + + jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ + cache: Symbol('parsed cache'), + dist: Symbol('parsed dist'), + maxWorkerCount: Symbol('parsed max worker count'), + pluginPaths: Symbol('parsed plugin paths'), + pluginScanDirs: Symbol('parsed plugin scan dirs'), + repoRoot: Symbol('parsed repo root'), + watch: Symbol('parsed watch'), + inspectWorkers: Symbol('parsed inspect workers'), + profileWebpack: Symbol('parsed profile webpack'), + })); + }); + + it('passes parsed options to findKibanaPlatformPlugins, getBundles, and assignBundlesToWorkers', () => { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + }); + + expect(config).toMatchInlineSnapshot(` + OptimizerConfig { + "bundles": Symbol(bundles), + "cache": Symbol(parsed cache), + "dist": Symbol(parsed dist), + "inspectWorkers": Symbol(parsed inspect workers), + "maxWorkerCount": Symbol(parsed max worker count), + "plugins": Symbol(new platform plugins), + "profileWebpack": Symbol(parsed profile webpack), + "repoRoot": Symbol(parsed repo root), + "watch": Symbol(parsed watch), + } + `); + + expect(findKibanaPlatformPlugins.mock).toMatchInlineSnapshot(` + Object { + "calls": Array [ + Array [ + Symbol(parsed plugin scan dirs), + Symbol(parsed plugin paths), + ], + ], + "instances": Array [ + [Window], + ], + "invocationCallOrder": Array [ + 7, + ], + "results": Array [ + Object { + "type": "return", + "value": Symbol(new platform plugins), + }, + ], + } + `); + + expect(getBundles.mock).toMatchInlineSnapshot(` + Object { + "calls": Array [ + Array [ + Symbol(new platform plugins), + Symbol(parsed repo root), + ], + ], + "instances": Array [ + [Window], + ], + "invocationCallOrder": Array [ + 8, + ], + "results": Array [ + Object { + "type": "return", + "value": Symbol(bundles), + }, + ], + } + `); + }); +}); diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts new file mode 100644 index 0000000000000..a258e1010fce3 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -0,0 +1,172 @@ +/* + * 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 Path from 'path'; +import Os from 'os'; + +import { Bundle, WorkerConfig } from '../common'; + +import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; +import { getBundles } from './get_bundles'; + +interface Options { + /** absolute path to root of the repo/build */ + repoRoot: string; + /** enable to run the optimizer in watch mode */ + watch?: boolean; + /** the maximum number of workers that will be created */ + maxWorkerCount?: number; + /** set to false to disabling writing/reading of caches */ + cache?: boolean; + /** build assets suitable for use in the distributable */ + dist?: boolean; + /** enable webpack profiling, writes stats.json files to the root of each plugin's output dir */ + profileWebpack?: boolean; + /** set to true to inspecting workers when the parent process is being inspected */ + inspectWorkers?: boolean; + + /** include only oss plugins in default scan dirs */ + oss?: boolean; + /** include examples in default scan dirs */ + examples?: boolean; + /** absolute paths to specific plugins that should be built */ + pluginPaths?: string[]; + /** absolute paths to directories that should be built, overrides the default scan dirs */ + pluginScanDirs?: string[]; + /** absolute paths that should be added to the default scan dirs */ + extraPluginScanDirs?: string[]; +} + +interface ParsedOptions { + repoRoot: string; + watch: boolean; + maxWorkerCount: number; + profileWebpack: boolean; + cache: boolean; + dist: boolean; + pluginPaths: string[]; + pluginScanDirs: string[]; + inspectWorkers: boolean; +} + +export class OptimizerConfig { + static parseOptions(options: Options): ParsedOptions { + const watch = !!options.watch; + const oss = !!options.oss; + const dist = !!options.dist; + const examples = !!options.examples; + const profileWebpack = !!options.profileWebpack; + const inspectWorkers = !!options.inspectWorkers; + const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; + + const repoRoot = options.repoRoot; + if (!Path.isAbsolute(repoRoot)) { + throw new TypeError('repoRoot must be an absolute path'); + } + + /** + * BEWARE: this needs to stay roughly synchronized with + * `src/core/server/config/env.ts` which determins which paths + * should be searched for plugins to load + */ + const pluginScanDirs = options.pluginScanDirs || [ + Path.resolve(repoRoot, 'src/plugins'), + ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), + Path.resolve(repoRoot, 'plugins'), + ...(examples ? [Path.resolve('examples')] : []), + Path.resolve(repoRoot, '../kibana-extra'), + ]; + if (!pluginScanDirs.every(p => Path.isAbsolute(p))) { + throw new TypeError('pluginScanDirs must all be absolute paths'); + } + + for (const extraPluginScanDir of options.extraPluginScanDirs || []) { + if (!Path.isAbsolute(extraPluginScanDir)) { + throw new TypeError('extraPluginScanDirs must all be absolute paths'); + } + pluginScanDirs.push(extraPluginScanDir); + } + + const pluginPaths = options.pluginPaths || []; + if (!pluginPaths.every(s => Path.isAbsolute(s))) { + throw new TypeError('pluginPaths must all be absolute paths'); + } + + const maxWorkerCount = process.env.KBN_OPTIMIZER_MAX_WORKERS + ? parseInt(process.env.KBN_OPTIMIZER_MAX_WORKERS, 10) + : options.maxWorkerCount ?? Math.max(Math.ceil(Math.max(Os.cpus()?.length, 1) / 3), 2); + if (typeof maxWorkerCount !== 'number' || !Number.isFinite(maxWorkerCount)) { + throw new TypeError('worker count must be a number'); + } + + return { + watch, + dist, + repoRoot, + maxWorkerCount, + profileWebpack, + cache, + pluginScanDirs, + pluginPaths, + inspectWorkers, + }; + } + + static create(inputOptions: Options) { + const options = OptimizerConfig.parseOptions(inputOptions); + const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); + const bundles = getBundles(plugins, options.repoRoot); + + return new OptimizerConfig( + bundles, + options.cache, + options.watch, + options.inspectWorkers, + plugins, + options.repoRoot, + options.maxWorkerCount, + options.dist, + options.profileWebpack + ); + } + + constructor( + public readonly bundles: Bundle[], + public readonly cache: boolean, + public readonly watch: boolean, + public readonly inspectWorkers: boolean, + public readonly plugins: KibanaPlatformPlugin[], + public readonly repoRoot: string, + public readonly maxWorkerCount: number, + public readonly dist: boolean, + public readonly profileWebpack: boolean + ) {} + + getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { + return { + cache: this.cache, + dist: this.dist, + profileWebpack: this.profileWebpack, + repoRoot: this.repoRoot, + watch: this.watch, + optimizerCacheKey, + browserslistEnv: this.dist ? 'production' : process.env.BROWSERSLIST_ENV || 'dev', + }; + } +} diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts b/packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts new file mode 100644 index 0000000000000..c1e6572bd7e75 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts @@ -0,0 +1,170 @@ +/* + * 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 { inspect } from 'util'; + +import { WorkerMsg, CompilerMsg, Bundle, Summarizer } from '../common'; + +import { ChangeEvent } from './watcher'; +import { WorkerStatus } from './observe_worker'; +import { BundleCacheEvent } from './bundle_cache'; +import { OptimizerConfig } from './optimizer_config'; + +export interface OptimizerInitializedEvent { + type: 'optimizer initialized'; +} + +export type OptimizerEvent = + | OptimizerInitializedEvent + | ChangeEvent + | WorkerMsg + | WorkerStatus + | BundleCacheEvent; + +export interface OptimizerState { + phase: 'initializing' | 'initialized' | 'running' | 'issue' | 'success' | 'reallocating'; + startTime: number; + durSec: number; + compilerStates: CompilerMsg[]; + onlineBundles: Bundle[]; + offlineBundles: Bundle[]; +} + +const msToSec = (ms: number) => Math.round(ms / 100) / 10; + +/** + * merge a state and some updates into a new optimizer state, apply some + * standard updates related to timing + */ +function createOptimizerState( + prevState: OptimizerState, + update?: Partial> +): OptimizerState { + // reset start time if we are transitioning into running + const startTime = + (prevState.phase === 'success' || prevState.phase === 'issue') && + (update?.phase === 'running' || update?.phase === 'reallocating') + ? Date.now() + : prevState.startTime; + + return { + ...prevState, + ...update, + startTime, + durSec: msToSec(Date.now() - startTime), + }; +} + +/** + * calculate the total state, given a set of compiler messages + */ +function getStatePhase(states: CompilerMsg[]) { + const types = states.map(s => s.type); + + if (types.includes('running')) { + return 'running'; + } + + if (types.includes('compiler issue')) { + return 'issue'; + } + + if (types.every(s => s === 'compiler success')) { + return 'success'; + } + + throw new Error(`unable to summarize bundle states: ${JSON.stringify(states)}`); +} + +export function createOptimizerReducer( + config: OptimizerConfig +): Summarizer { + return (state, event) => { + if (event.type === 'optimizer initialized') { + return createOptimizerState(state, { + phase: 'initialized', + }); + } + + if (event.type === 'worker error' || event.type === 'compiler error') { + // unrecoverable error states + const error = new Error(event.errorMsg); + error.stack = event.errorStack; + throw error; + } + + if (event.type === 'worker stdio' || event.type === 'worker started') { + // same state, but updated to the event is shared externally + return createOptimizerState(state); + } + + if (event.type === 'changes detected') { + // switch to running early, before workers are started, so that + // base path proxy can prevent requests in the delay between changes + // and workers started + return createOptimizerState(state, { + phase: 'reallocating', + }); + } + + if ( + event.type === 'changes' || + event.type === 'bundle cached' || + event.type === 'bundle not cached' + ) { + const onlineBundles: Bundle[] = [...state.onlineBundles]; + if (event.type === 'changes') { + onlineBundles.push(...event.bundles); + } + if (event.type === 'bundle not cached') { + onlineBundles.push(event.bundle); + } + + const offlineBundles: Bundle[] = []; + for (const bundle of config.bundles) { + if (!onlineBundles.includes(bundle)) { + offlineBundles.push(bundle); + } + } + + return createOptimizerState(state, { + phase: state.phase === 'initializing' ? 'initializing' : 'running', + onlineBundles, + offlineBundles, + }); + } + + if ( + event.type === 'compiler issue' || + event.type === 'compiler success' || + event.type === 'running' + ) { + const compilerStates: CompilerMsg[] = [ + ...state.compilerStates.filter(c => c.bundleId !== event.bundleId), + event, + ]; + return createOptimizerState(state, { + phase: getStatePhase(compilerStates), + compilerStates, + }); + } + + throw new Error(`unexpected optimizer event ${inspect(event)}`); + }; +} diff --git a/packages/kbn-optimizer/src/optimizer/run_workers.ts b/packages/kbn-optimizer/src/optimizer/run_workers.ts new file mode 100644 index 0000000000000..e91b0d25fd72b --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/run_workers.ts @@ -0,0 +1,67 @@ +/* + * 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 Rx from 'rxjs'; +import { mergeMap, toArray } from 'rxjs/operators'; + +import { maybeMap } from '../common'; + +import { OptimizerConfig } from './optimizer_config'; +import { BundleCacheEvent } from './bundle_cache'; +import { ChangeEvent } from './watcher'; +import { assignBundlesToWorkers } from './assign_bundles_to_workers'; +import { observeWorker } from './observe_worker'; + +/** + * Create a stream of all worker events, these include messages + * from workers and events about the status of workers. To get + * these events we assign the bundles to workers via + * `assignBundlesToWorkers()` and then start a worler for each + * assignment with `observeWorker()`. + * + * Subscribes to `changeEvent$` in order to determine when more + * bundles should be assigned to workers. + * + * Completes when all workers have exitted. If we are running in + * watch mode this observable will never exit. + */ +export function runWorkers( + config: OptimizerConfig, + optimizerCacheKey: unknown, + bundleCache$: Rx.Observable, + changeEvent$: Rx.Observable +) { + return Rx.concat( + // first batch of bundles are based on how up-to-date the cache is + bundleCache$.pipe( + maybeMap(event => (event.type === 'bundle not cached' ? event.bundle : undefined)), + toArray() + ), + // subsequent batches are defined by changeEvent$ + changeEvent$.pipe(maybeMap(c => (c.type === 'changes' ? c.bundles : undefined))) + ).pipe( + mergeMap(bundles => + Rx.from(assignBundlesToWorkers(bundles, config.maxWorkerCount)).pipe( + mergeMap(assignment => + observeWorker(config, config.getWorkerConfig(optimizerCacheKey), assignment.bundles) + ) + ) + ) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts b/packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts new file mode 100644 index 0000000000000..9149c483786fc --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts @@ -0,0 +1,85 @@ +/* + * 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 Rx from 'rxjs'; +import { mergeMap, toArray } from 'rxjs/operators'; + +import { Bundle, maybeMap } from '../common'; + +import { BundleCacheEvent } from './bundle_cache'; +import { Watcher } from './watcher'; + +/** + * Recursively call watcher.getNextChange$, passing it + * just the bundles that haven't been changed yet until + * all bundles have changed, then exit + */ +function recursiveGetNextChange$( + watcher: Watcher, + bundles: Bundle[], + startTime: number +): ReturnType { + return !bundles.length + ? Rx.EMPTY + : watcher.getNextChange$(bundles, startTime).pipe( + mergeMap(event => { + if (event.type === 'changes detected') { + return Rx.of(event); + } + + return Rx.concat( + Rx.of(event), + + recursiveGetNextChange$( + watcher, + bundles.filter(b => !event.bundles.includes(b)), + Date.now() + ) + ); + }) + ); +} + +/** + * Create an observable that emits change events for offline + * bundles. + * + * Once changes are seen in a bundle that bundles + * files will no longer be watched. + * + * Once changes have been seen in all bundles changeEvent$ + * will complete. + * + * If there are no bundles to watch or we config.watch === false + * the observable completes without sending any notifications. + */ +export function watchBundlesForChanges$( + bundleCacheEvent$: Rx.Observable, + initialStartTime: number +) { + return bundleCacheEvent$.pipe( + maybeMap(event => (event.type === 'bundle cached' ? event.bundle : undefined)), + toArray(), + mergeMap(bundles => + bundles.length + ? Watcher.using(watcher => recursiveGetNextChange$(watcher, bundles, initialStartTime)) + : Rx.EMPTY + ) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/watcher.ts b/packages/kbn-optimizer/src/optimizer/watcher.ts new file mode 100644 index 0000000000000..343f391921383 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/watcher.ts @@ -0,0 +1,109 @@ +/* + * 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 Rx from 'rxjs'; +import { take, map, share } from 'rxjs/operators'; +import Watchpack from 'watchpack'; + +import { debounceTimeBuffer, Bundle } from '../common'; + +export interface ChangesStarted { + type: 'changes detected'; +} + +export interface Changes { + type: 'changes'; + bundles: Bundle[]; +} + +export type ChangeEvent = ChangesStarted | Changes; + +export class Watcher { + /** + * Use watcher as an RxJS Resource, which is a special type of observable + * that calls unsubscribe on the resource (the Watcher instance in this case) + * when the observable is unsubscribed. + */ + static using(fn: (watcher: Watcher) => Rx.Observable) { + return Rx.using( + () => new Watcher(), + resource => fn(resource as Watcher) + ); + } + + private readonly watchpack = new Watchpack({ + aggregateTimeout: 0, + ignored: /node_modules\/([^\/]+[\/])*(?!package.json)([^\/]+)$/, + }); + + private readonly change$ = Rx.fromEvent<[string]>(this.watchpack, 'change').pipe(share()); + + public getNextChange$(bundles: Bundle[], startTime: number) { + return Rx.merge( + // emit ChangesStarted as soon as we have been triggered + this.change$.pipe( + take(1), + map( + (): ChangesStarted => ({ + type: 'changes detected', + }) + ) + ), + + // debounce and bufffer change events for 1 second to create + // final change notification + this.change$.pipe( + map(event => event[0]), + debounceTimeBuffer(1000), + map( + (changes): Changes => ({ + type: 'changes', + bundles: bundles.filter(bundle => { + const referencedFiles = bundle.cache.getReferencedFiles(); + return changes.some(change => referencedFiles?.includes(change)); + }), + }) + ), + take(1) + ), + + // call watchpack.watch after listerners are setup + Rx.defer(() => { + const watchPaths: string[] = []; + + for (const bundle of bundles) { + for (const path of bundle.cache.getReferencedFiles() || []) { + watchPaths.push(path); + } + } + + this.watchpack.watch(watchPaths, [], startTime); + return Rx.EMPTY; + }) + ); + } + + /** + * Called automatically by RxJS when Watcher instances + * are used as resources + */ + unsubscribe() { + this.watchpack.close(); + } +} diff --git a/packages/kbn-optimizer/src/run_optimizer.ts b/packages/kbn-optimizer/src/run_optimizer.ts new file mode 100644 index 0000000000000..e6cce8d306e35 --- /dev/null +++ b/packages/kbn-optimizer/src/run_optimizer.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 * as Rx from 'rxjs'; +import { mergeMap, share, observeOn } from 'rxjs/operators'; + +import { summarizeEvent$, Update } from './common'; + +import { + OptimizerConfig, + OptimizerEvent, + OptimizerState, + getBundleCacheEvent$, + getOptimizerCacheKey, + watchBundlesForChanges$, + runWorkers, + OptimizerInitializedEvent, + createOptimizerReducer, +} from './optimizer'; + +export type OptimizerUpdate = Update; +export type OptimizerUpdate$ = Rx.Observable; + +export function runOptimizer(config: OptimizerConfig) { + return Rx.defer(async () => ({ + startTime: Date.now(), + cacheKey: await getOptimizerCacheKey(config), + })).pipe( + mergeMap(({ startTime, cacheKey }) => { + const bundleCacheEvent$ = getBundleCacheEvent$(config, cacheKey).pipe( + observeOn(Rx.asyncScheduler), + share() + ); + + // initialization completes once all bundle caches have been resolved + const init$ = Rx.concat( + bundleCacheEvent$, + Rx.of({ + type: 'optimizer initialized', + }) + ); + + // watch the offline bundles for changes, turning them online... + const changeEvent$ = config.watch + ? watchBundlesForChanges$(bundleCacheEvent$, startTime).pipe(share()) + : Rx.EMPTY; + + // run workers to build all the online bundles, including the bundles turned online by changeEvent$ + const workerEvent$ = runWorkers(config, cacheKey, bundleCacheEvent$, changeEvent$); + + // create the stream that summarized all the events into specific states + return summarizeEvent$( + Rx.merge(init$, changeEvent$, workerEvent$), + { + phase: 'initializing', + compilerStates: [], + offlineBundles: [], + onlineBundles: [], + startTime, + durSec: 0, + }, + createOptimizerReducer(config) + ); + }) + ); +} diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/src/worker/postcss.config.js new file mode 100644 index 0000000000000..571bae86dee37 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/postcss.config.js @@ -0,0 +1,22 @@ +/* + * 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. + */ + +module.exports = { + plugins: [require('autoprefixer')()], +}; diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts new file mode 100644 index 0000000000000..7dcce8a0fae8d --- /dev/null +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -0,0 +1,210 @@ +/* + * 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 'source-map-support/register'; + +import Fs from 'fs'; +import Path from 'path'; +import { inspect } from 'util'; + +import webpack, { Stats } from 'webpack'; +import * as Rx from 'rxjs'; +import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; + +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig } from '../common'; +import { getWebpackConfig } from './webpack.config'; +import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; +import { + isExternalModule, + isNormalModule, + isIgnoredModule, + isConcatenatedModule, + WebpackNormalModule, + getModulePath, +} from './webpack_helpers'; + +const PLUGIN_NAME = '@kbn/optimizer'; + +/** + * Create an Observable for a specific child compiler + bundle + */ +const observeCompiler = ( + workerConfig: WorkerConfig, + bundle: Bundle, + compiler: webpack.Compiler +): Rx.Observable => { + const compilerMsgs = new CompilerMsgs(bundle.id); + const done$ = new Rx.Subject(); + const { beforeRun, watchRun, done } = compiler.hooks; + + /** + * Called by webpack as a single run compilation is starting + */ + const started$ = Rx.merge( + Rx.fromEventPattern(cb => beforeRun.tap(PLUGIN_NAME, cb)), + Rx.fromEventPattern(cb => watchRun.tap(PLUGIN_NAME, cb)) + ).pipe(mapTo(compilerMsgs.running())); + + /** + * Called by webpack as any compilation is complete. If the + * needAdditionalPass property is set then another compilation + * is about to be started, so we shouldn't send complete quite yet + */ + const complete$ = Rx.fromEventPattern(cb => done.tap(PLUGIN_NAME, cb)).pipe( + maybeMap(stats => { + // @ts-ignore not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58 + if (stats.compilation.needAdditionalPass) { + return undefined; + } + + if (workerConfig.profileWebpack) { + Fs.writeFileSync( + Path.resolve(bundle.outputDir, 'stats.json'), + JSON.stringify(stats.toJson()) + ); + } + + if (!workerConfig.watch) { + process.nextTick(() => done$.next()); + } + + if (isFailureStats(stats)) { + return compilerMsgs.compilerFailure({ + failure: failedStatsToErrorMessage(stats), + }); + } + + const normalModules = stats.compilation.modules.filter( + (module): module is WebpackNormalModule => { + if (isNormalModule(module)) { + return true; + } + + if (isExternalModule(module) || isIgnoredModule(module) || isConcatenatedModule(module)) { + return false; + } + + throw new Error(`Unexpected module type: ${inspect(module)}`); + } + ); + + const referencedFiles = new Set(); + + for (const module of normalModules) { + const path = getModulePath(module); + + const parsedPath = Path.parse(path); + const dirSegments = parsedPath.dir.split(Path.sep); + if (!dirSegments.includes('node_modules')) { + referencedFiles.add(path); + continue; + } + + const nmIndex = dirSegments.lastIndexOf('node_modules'); + const isScoped = dirSegments[nmIndex + 1].startsWith('@'); + referencedFiles.add( + Path.join( + parsedPath.root, + ...dirSegments.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' + ) + ); + } + + const files = Array.from(referencedFiles); + const mtimes = new Map( + files.map((path): [string, number | undefined] => { + try { + return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; + } catch (error) { + if (error?.code === 'ENOENT') { + return [path, undefined]; + } + + throw error; + } + }) + ); + + bundle.cache.set({ + optimizerCacheKey: workerConfig.optimizerCacheKey, + cacheKey: bundle.createCacheKey(files, mtimes), + moduleCount: normalModules.length, + files, + }); + + return compilerMsgs.compilerSuccess({ + moduleCount: normalModules.length, + }); + }) + ); + + /** + * Called whenever the compilation results in an error that + * prevets assets from being emitted, and prevents watching + * from continuing. + */ + const error$ = Rx.fromEventPattern(cb => compiler.hooks.failed.tap(PLUGIN_NAME, cb)).pipe( + map(error => { + throw compilerMsgs.error(error); + }) + ); + + /** + * Merge events into a single stream, if we're not watching + * complete the stream after our first complete$ event + */ + return Rx.merge(started$, complete$, error$).pipe(takeUntil(done$)); +}; + +/** + * Run webpack compilers + */ +export const runCompilers = (workerConfig: WorkerConfig, bundles: Bundle[]) => { + const multiCompiler = webpack(bundles.map(def => getWebpackConfig(def, workerConfig))); + + return Rx.merge( + /** + * convert each compiler into an event stream that represents + * the status of each compiler, if we aren't watching the streams + * will complete after the compilers are complete. + * + * If a significant error occurs the stream will error + */ + Rx.from(multiCompiler.compilers.entries()).pipe( + mergeMap(([compilerIndex, compiler]) => { + const bundle = bundles[compilerIndex]; + return observeCompiler(workerConfig, bundle, compiler); + }) + ), + + /** + * compilers have been hooked up for their events, trigger run()/watch() + */ + Rx.defer(() => { + if (!workerConfig.watch) { + multiCompiler.run(() => {}); + } else { + multiCompiler.watch({}, () => {}); + } + + return []; + }) + ); +}; diff --git a/packages/kbn-optimizer/src/worker/run_worker.ts b/packages/kbn-optimizer/src/worker/run_worker.ts new file mode 100644 index 0000000000000..d6ca2aa94fb1a --- /dev/null +++ b/packages/kbn-optimizer/src/worker/run_worker.ts @@ -0,0 +1,107 @@ +/* + * 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 Rx from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { parseBundles, parseWorkerConfig, WorkerMsg, isWorkerMsg, WorkerMsgs } from '../common'; + +import { runCompilers } from './run_compilers'; + +/** + ** + ** + ** Entry file for optimizer workers, this hooks into the process, handles + ** sending messages to the parent, makes sure the worker exits properly + ** and triggers all the compilers by calling runCompilers() + ** + ** + **/ + +const workerMsgs = new WorkerMsgs(); + +if (!process.send) { + throw new Error('worker process was not started with an IPC channel'); +} + +const send = (msg: WorkerMsg) => { + if (!process.send) { + // parent is gone + process.exit(0); + } else { + process.send(msg); + } +}; + +/** + * set the exitCode and wait for the process to exit, if it + * doesn't exit naturally do so forcibly and fail. + */ +const exit = (code: number) => { + process.exitCode = code; + setTimeout(() => { + send( + workerMsgs.error( + new Error('process did not automatically exit within 5 seconds, forcing exit') + ) + ); + process.exit(1); + }, 5000).unref(); +}; + +// check for connected parent on an unref'd timer rather than listening +// to "disconnect" since that listner prevents the process from exiting +setInterval(() => { + if (!process.connected) { + // parent is gone + process.exit(0); + } +}, 1000).unref(); + +Rx.defer(() => { + return Rx.of({ + workerConfig: parseWorkerConfig(process.argv[2]), + bundles: parseBundles(process.argv[3]), + }); +}) + .pipe( + mergeMap(({ workerConfig, bundles }) => { + // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers + process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; + + return runCompilers(workerConfig, bundles); + }) + ) + .subscribe( + msg => { + send(msg); + }, + error => { + if (isWorkerMsg(error)) { + send(error); + } else { + send(workerMsgs.error(error)); + } + + exit(1); + }, + () => { + exit(0); + } + ); diff --git a/packages/kbn-optimizer/src/worker/theme_loader.ts b/packages/kbn-optimizer/src/worker/theme_loader.ts new file mode 100644 index 0000000000000..6d6686a5bde1b --- /dev/null +++ b/packages/kbn-optimizer/src/worker/theme_loader.ts @@ -0,0 +1,32 @@ +/* + * 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 webpack from 'webpack'; +import { stringifyRequest } from 'loader-utils'; + +// eslint-disable-next-line import/no-default-export +export default function(this: webpack.loader.LoaderContext) { + return ` +if (window.__kbnDarkMode__) { + require(${stringifyRequest(this, `${this.resourcePath}?dark`)}) +} else { + require(${stringifyRequest(this, `${this.resourcePath}?light`)}); +} + `; +} diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts new file mode 100644 index 0000000000000..1e87b8a5a7f7b --- /dev/null +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -0,0 +1,244 @@ +/* + * 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 Path from 'path'; + +import { stringifyRequest } from 'loader-utils'; +import webpack from 'webpack'; +// @ts-ignore +import TerserPlugin from 'terser-webpack-plugin'; +// @ts-ignore +import webpackMerge from 'webpack-merge'; +// @ts-ignore +import { CleanWebpackPlugin } from 'clean-webpack-plugin'; +import * as SharedDeps from '@kbn/ui-shared-deps'; + +import { Bundle, WorkerConfig } from '../common'; + +const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; +const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); +const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; +const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); + +export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { + const commonConfig: webpack.Configuration = { + node: { fs: 'empty' }, + context: bundle.contextDir, + cache: true, + entry: { + [bundle.id]: bundle.entry, + }, + + devtool: worker.dist ? false : '#cheap-source-map', + profile: worker.profileWebpack, + + output: { + path: bundle.outputDir, + filename: '[name].plugin.js', + publicPath: PUBLIC_PATH_PLACEHOLDER, + devtoolModuleFilenameTemplate: info => + `/${bundle.type}:${bundle.id}/${Path.relative( + bundle.sourceRoot, + info.absoluteResourcePath + )}${info.query}`, + jsonpFunction: `${bundle.id}_bundle_jsonpfunction`, + ...(bundle.type === 'plugin' + ? { + // When the entry point is loaded, assign it's exported `plugin` + // value to a key on the global `__kbnBundles__` object. + library: ['__kbnBundles__', `plugin/${bundle.id}`], + libraryExport: 'plugin', + } + : {}), + }, + + optimization: { + noEmitOnErrors: true, + }, + + externals: { + ...SharedDeps.externals, + }, + + plugins: [new CleanWebpackPlugin()], + + module: { + // no parse rules for a few known large packages which have no require() statements + noParse: [ + /[\///]node_modules[\///]elasticsearch-browser[\///]/, + /[\///]node_modules[\///]lodash[\///]index\.js/, + ], + + rules: [ + { + test: /\.css$/, + include: /node_modules/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: !worker.dist, + }, + }, + ], + }, + { + test: /\.scss$/, + exclude: /node_modules/, + oneOf: [ + { + resourceQuery: /dark|light/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: !worker.dist, + }, + }, + { + loader: 'postcss-loader', + options: { + sourceMap: !worker.dist, + config: { + path: require.resolve('./postcss.config'), + }, + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: !worker.dist, + prependData(loaderContext: webpack.loader.LoaderContext) { + return `@import ${stringifyRequest( + loaderContext, + Path.resolve( + worker.repoRoot, + 'src/legacy/ui/public/styles/_styling_constants.scss' + ) + )};\n`; + }, + webpackImporter: false, + implementation: require('node-sass'), + sassOptions(loaderContext: webpack.loader.LoaderContext) { + const darkMode = loaderContext.resourceQuery === '?dark'; + + return { + outputStyle: 'nested', + includePaths: [Path.resolve(worker.repoRoot, 'node_modules')], + sourceMapRoot: `/${bundle.type}:${bundle.id}`, + importer: (url: string) => { + if (darkMode && url.includes('eui_colors_light')) { + return { file: url.replace('eui_colors_light', 'eui_colors_dark') }; + } + + return { file: url }; + }, + }; + }, + }, + }, + ], + }, + { + loader: require.resolve('./theme_loader'), + }, + ], + }, + { + test: /\.(woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg)(\?|$)/, + loader: 'url-loader', + options: { + limit: 8192, + }, + }, + { + test: /\.(js|tsx?)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: IS_CODE_COVERAGE + ? [ISTANBUL_PRESET_PATH, BABEL_PRESET_PATH] + : [BABEL_PRESET_PATH], + }, + }, + }, + { + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', + }, + }, + ], + }, + + resolve: { + extensions: ['.js', '.ts', '.tsx', '.json'], + alias: { + tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), + }, + }, + + performance: { + // NOTE: we are disabling this as those hints + // are more tailored for the final bundles result + // and not for the webpack compilations performance itself + hints: false, + }, + }; + + const nonDistributableConfig: webpack.Configuration = { + mode: 'development', + }; + + const distributableConfig: webpack.Configuration = { + mode: 'production', + + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + IS_KIBANA_DISTRIBUTABLE: `"true"`, + }, + }), + ], + + optimization: { + minimizer: [ + new TerserPlugin({ + cache: false, + sourceMap: false, + extractComments: false, + terserOptions: { + compress: false, + mangle: false, + }, + }), + ], + }, + }; + + return webpackMerge(commonConfig, worker.dist ? distributableConfig : nonDistributableConfig); +} diff --git a/packages/kbn-optimizer/src/worker/webpack_helpers.ts b/packages/kbn-optimizer/src/worker/webpack_helpers.ts new file mode 100644 index 0000000000000..a11c85c64198e --- /dev/null +++ b/packages/kbn-optimizer/src/worker/webpack_helpers.ts @@ -0,0 +1,166 @@ +/* + * 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 webpack from 'webpack'; +import { defaults } from 'lodash'; +// @ts-ignore +import Stats from 'webpack/lib/Stats'; + +export function isFailureStats(stats: webpack.Stats) { + if (stats.hasErrors()) { + return true; + } + + const { warnings } = stats.toJson({ all: false, warnings: true }); + + // 1 - when typescript doesn't do a full type check, as we have the ts-loader + // configured here, it does not have enough information to determine + // whether an imported name is a type or not, so when the name is then + // exported, typescript has no choice but to emit the export. Fortunately, + // the extraneous export should not be harmful, so we just suppress these warnings + // https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse + // + // 2 - Mini Css Extract plugin tracks the order for each css import we have + // through the project (and it's successive imports) since version 0.4.2. + // In case we have the same imports more than one time with different + // sequences, this plugin will throw a warning. This should not be harmful, + // but the an issue was opened and can be followed on: + // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/250#issuecomment-415345126 + const filteredWarnings = Stats.filterWarnings(warnings, STATS_WARNINGS_FILTER); + + return filteredWarnings.length > 0; +} + +const STATS_WARNINGS_FILTER = new RegExp( + [ + '(export .* was not found in)', + '|(chunk .* \\[mini-css-extract-plugin\\]\\\nConflicting order between:)', + ].join('') +); + +export function failedStatsToErrorMessage(stats: webpack.Stats) { + const details = stats.toString( + defaults( + { colors: true, warningsFilter: STATS_WARNINGS_FILTER }, + Stats.presetToOptions('minimal') + ) + ); + + return `Optimizations failure.\n${details.split('\n').join('\n ')}`; +} + +export interface WebpackResolveData { + /** compilation context */ + context: string; + /** full request (with loaders) */ + request: string; + dependencies: [ + { + module: unknown; + weak: boolean; + optional: boolean; + loc: unknown; + request: string; + userRequest: string; + } + ]; + /** absolute path, but probably includes loaders in some cases */ + userRequest: string; + /** string from source code */ + rawRequest: string; + loaders: unknown; + /** absolute path to file, but probablt includes loaders in some cases */ + resource: string; + /** module type */ + type: string | 'javascript/auto'; + + resourceResolveData: { + context: { + /** absolute path to the file that issued the request */ + issuer: string; + }; + /** absolute path to the resolved file */ + path: string; + }; +} + +interface Dependency { + type: 'null' | 'cjs require'; + module: unknown; +} + +/** used for standard js/ts modules */ +export interface WebpackNormalModule { + type: string; + /** absolute path to file on disk */ + resource: string; + buildInfo: { + cacheable: boolean; + fileDependencies: Set; + }; + dependencies: Dependency[]; +} + +export function isNormalModule(module: any): module is WebpackNormalModule { + return module?.constructor?.name === 'NormalModule'; +} + +/** module used for ignored code */ +export interface WebpackIgnoredModule { + type: string; + /** unique string to identify this module with (starts with `ignored`) */ + identifierStr: string; + /** human readable identifier */ + readableIdentifierStr: string; +} + +export function isIgnoredModule(module: any): module is WebpackIgnoredModule { + return module?.constructor?.name === 'RawModule' && module.identifierStr?.startsWith('ignored '); +} + +/** module replacing imports for webpack externals */ +export interface WebpackExternalModule { + type: string; + id: string; + /** JS used to get instance of External */ + request: string; + /** module name that is handled by externals */ + userRequest: string; +} + +export function isExternalModule(module: any): module is WebpackExternalModule { + return module?.constructor?.name === 'ExternalModule'; +} + +/** module replacing imports for webpack externals */ +export interface WebpackConcatenatedModule { + type: string; + id: number; + dependencies: Dependency[]; + usedExports: string[]; +} + +export function isConcatenatedModule(module: any): module is WebpackConcatenatedModule { + return module?.constructor?.name === 'ConcatenatedModule'; +} + +export function getModulePath(module: WebpackNormalModule) { + const queryIndex = module.resource.indexOf('?'); + return queryIndex === -1 ? module.resource : module.resource.slice(0, queryIndex); +} diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json new file mode 100644 index 0000000000000..e2994f4d02414 --- /dev/null +++ b/packages/kbn-optimizer/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "index.d.ts", + "src/**/*" + ] +} diff --git a/packages/kbn-optimizer/yarn.lock b/packages/kbn-optimizer/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-optimizer/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 129125c4583d5..51a404379fedb 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -24,7 +24,7 @@ import util from 'util'; import { stat, readFileSync } from 'fs'; import { snakeCase } from 'lodash'; import del from 'del'; -import { withProcRunner, ToolingLog } from '@kbn/dev-utils'; +import { ProcRunner, ToolingLog } from '@kbn/dev-utils'; import { createLegacyEsTestCluster } from '@kbn/test'; import execa from 'execa'; @@ -84,27 +84,30 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug }); describe('with es instance', () => { - const log = new ToolingLog(); + const log = new ToolingLog({ + level: 'verbose', + writeTo: process.stdout, + }); + const pr = new ProcRunner(log); const es = createLegacyEsTestCluster({ license: 'basic', log }); beforeAll(es.start); afterAll(es.stop); + afterAll(() => pr.teardown()); it(`'yarn start' should result in the spec plugin being initialized on kibana's stdout`, async () => { - await withProcRunner(log, async proc => { - await proc.run('kibana', { - cmd: 'yarn', - args: [ - 'start', - '--optimize.enabled=false', - '--logging.json=false', - '--migrations.skip=true', - ], - cwd: generatedPath, - wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), - }); - await proc.stop('kibana'); + await pr.run('kibana', { + cmd: 'yarn', + args: [ + 'start', + '--optimize.enabled=false', + '--logging.json=false', + '--migrations.skip=true', + ], + cwd: generatedPath, + wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), }); + await pr.stop('kibana'); }); }); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index e3df1ab585ee4..15ea3b68f1182 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -4490,14 +4490,10 @@ const tslib_1 = __webpack_require__(36); var proc_runner_1 = __webpack_require__(37); exports.withProcRunner = proc_runner_1.withProcRunner; exports.ProcRunner = proc_runner_1.ProcRunner; -var tooling_log_1 = __webpack_require__(415); -exports.ToolingLog = tooling_log_1.ToolingLog; -exports.ToolingLogTextWriter = tooling_log_1.ToolingLogTextWriter; -exports.pickLevelFromFlags = tooling_log_1.pickLevelFromFlags; -exports.ToolingLogCollectingWriter = tooling_log_1.ToolingLogCollectingWriter; +tslib_1.__exportStar(__webpack_require__(415), exports); var serializers_1 = __webpack_require__(420); exports.createAbsolutePathSerializer = serializers_1.createAbsolutePathSerializer; -var certs_1 = __webpack_require__(422); +var certs_1 = __webpack_require__(445); exports.CA_CERT_PATH = certs_1.CA_CERT_PATH; exports.ES_KEY_PATH = certs_1.ES_KEY_PATH; exports.ES_CERT_PATH = certs_1.ES_CERT_PATH; @@ -4509,13 +4505,13 @@ exports.KBN_KEY_PATH = certs_1.KBN_KEY_PATH; exports.KBN_CERT_PATH = certs_1.KBN_CERT_PATH; exports.KBN_P12_PATH = certs_1.KBN_P12_PATH; exports.KBN_P12_PASSWORD = certs_1.KBN_P12_PASSWORD; -var run_1 = __webpack_require__(423); +var run_1 = __webpack_require__(446); exports.run = run_1.run; exports.createFailError = run_1.createFailError; exports.createFlagError = run_1.createFlagError; exports.combineErrors = run_1.combineErrors; exports.isFailError = run_1.isFailError; -var repo_root_1 = __webpack_require__(428); +var repo_root_1 = __webpack_require__(422); exports.REPO_ROOT = repo_root_1.REPO_ROOT; var kbn_client_1 = __webpack_require__(451); exports.KbnClient = kbn_client_1.KbnClient; @@ -36634,6 +36630,7 @@ var tooling_log_text_writer_1 = __webpack_require__(417); exports.ToolingLogTextWriter = tooling_log_text_writer_1.ToolingLogTextWriter; var log_levels_1 = __webpack_require__(418); exports.pickLevelFromFlags = log_levels_1.pickLevelFromFlags; +exports.parseLogLevel = log_levels_1.parseLogLevel; var tooling_log_collecting_writer_1 = __webpack_require__(419); exports.ToolingLogCollectingWriter = tooling_log_collecting_writer_1.ToolingLogCollectingWriter; @@ -36789,17 +36786,23 @@ class ToolingLogTextWriter { throw new Error('ToolingLogTextWriter requires the `writeTo` option be set to a stream (like process.stdout)'); } } - write({ type, indent, args }) { - if (!shouldWriteType(this.level, type)) { + write(msg) { + if (!shouldWriteType(this.level, msg.type)) { return false; } - const txt = type === 'error' ? stringifyError(args[0]) : util_1.format(args[0], ...args.slice(1)); - const prefix = has(MSG_PREFIXES, type) ? MSG_PREFIXES[type] : ''; + const prefix = has(MSG_PREFIXES, msg.type) ? MSG_PREFIXES[msg.type] : ''; + ToolingLogTextWriter.write(this.writeTo, prefix, msg); + return true; + } + static write(writeTo, prefix, msg) { + const txt = msg.type === 'error' + ? stringifyError(msg.args[0]) + : util_1.format(msg.args[0], ...msg.args.slice(1)); (prefix + txt).split('\n').forEach((line, i) => { let lineIndent = ''; - if (indent > 0) { + if (msg.indent > 0) { // if we are indenting write some spaces followed by a symbol - lineIndent += ' '.repeat(indent - 1); + lineIndent += ' '.repeat(msg.indent - 1); lineIndent += line.startsWith('-') ? '└' : '│'; } if (line && prefix && i > 0) { @@ -36807,9 +36810,8 @@ class ToolingLogTextWriter { // the first if this message gets a prefix lineIndent += PREFIX_INDENT; } - this.writeTo.write(`${lineIndent}${line}\n`); + writeTo.write(`${lineIndent}${line}\n`); }); - return true; } } exports.ToolingLogTextWriter = ToolingLogTextWriter; @@ -36968,7 +36970,8 @@ exports.createAbsolutePathSerializer = absolute_path_serializer_1.createAbsolute * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -function createAbsolutePathSerializer(rootPath) { +const repo_root_1 = __webpack_require__(422); +function createAbsolutePathSerializer(rootPath = repo_root_1.REPO_ROOT) { return { print: (value) => value.replace(rootPath, '').replace(/\\/g, '/'), test: (value) => typeof value === 'string' && value.startsWith(rootPath), @@ -36983,79 +36986,6 @@ exports.createAbsolutePathSerializer = createAbsolutePathSerializer; "use strict"; -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = __webpack_require__(16); -exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt'); -exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key'); -exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt'); -exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12'); -exports.ES_P12_PASSWORD = 'storepass'; -exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12'); -exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); -exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key'); -exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt'); -exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12'); -exports.KBN_P12_PASSWORD = 'storepass'; - - -/***/ }), -/* 423 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -var run_1 = __webpack_require__(424); -exports.run = run_1.run; -var fail_1 = __webpack_require__(425); -exports.createFailError = fail_1.createFailError; -exports.createFlagError = fail_1.createFlagError; -exports.combineErrors = fail_1.combineErrors; -exports.isFailError = fail_1.isFailError; - - -/***/ }), -/* 424 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37076,688 +37006,176 @@ exports.isFailError = fail_1.isFailError; */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -// @ts-ignore @types are outdated and module is super simple -const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); -const tooling_log_1 = __webpack_require__(415); -const fail_1 = __webpack_require__(425); -const flags_1 = __webpack_require__(426); -const proc_runner_1 = __webpack_require__(37); -async function run(fn, options = {}) { - var _a; - const flags = flags_1.getFlags(process.argv.slice(2), options); - if (flags.help) { - process.stderr.write(flags_1.getHelp(options)); - process.exit(1); - } - const log = new tooling_log_1.ToolingLog({ - level: tooling_log_1.pickLevelFromFlags(flags), - writeTo: process.stdout, - }); - process.on('unhandledRejection', error => { - log.error('UNHANDLED PROMISE REJECTION'); - log.error(error); - process.exit(1); - }); - const handleErrorWithoutExit = (error) => { - if (fail_1.isFailError(error)) { - log.error(error.message); - if (error.showHelp) { - log.write(flags_1.getHelp(options)); - } - process.exitCode = error.exitCode; - } - else { - log.error('UNHANDLED ERROR'); - log.error(error); - process.exitCode = 1; - } - }; - const doCleanup = () => { - const tasks = cleanupTasks.slice(0); - cleanupTasks.length = 0; - for (const task of tasks) { - try { - task(); - } - catch (error) { - handleErrorWithoutExit(error); - } - } - }; - const unhookExit = exit_hook_1.default(doCleanup); - const cleanupTasks = [unhookExit]; +const path_1 = tslib_1.__importDefault(__webpack_require__(16)); +const fs_1 = tslib_1.__importDefault(__webpack_require__(23)); +const load_json_file_1 = tslib_1.__importDefault(__webpack_require__(423)); +const isKibanaDir = (dir) => { try { - if (!((_a = options.flags) === null || _a === void 0 ? void 0 : _a.allowUnexpected) && flags.unexpected.length) { - throw fail_1.createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); - } - try { - await proc_runner_1.withProcRunner(log, async (procRunner) => { - await fn({ - log, - flags, - procRunner, - addCleanupTask: (task) => cleanupTasks.push(task), - }); - }); - } - finally { - doCleanup(); + const path = path_1.default.resolve(dir, 'package.json'); + const json = load_json_file_1.default.sync(path); + if (json && typeof json === 'object' && 'name' in json && json.name === 'kibana') { + return true; } } catch (error) { - handleErrorWithoutExit(error); - process.exit(); + if (error && error.code === 'ENOENT') { + return false; + } + throw error; + } +}; +// search for the kibana directory, since this file is moved around it might +// not be where we think but should always be a relatively close parent +// of this directory +const startDir = fs_1.default.realpathSync(__dirname); +const { root: rootDir } = path_1.default.parse(startDir); +let cursor = startDir; +while (true) { + if (isKibanaDir(cursor)) { + break; + } + const parent = path_1.default.dirname(cursor); + if (parent === rootDir) { + throw new Error(`unable to find kibana directory from ${startDir}`); } + cursor = parent; } -exports.run = run; +exports.REPO_ROOT = cursor; /***/ }), -/* 425 */ +/* 423 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const util_1 = __webpack_require__(29); -const FAIL_TAG = Symbol('fail error'); -function createFailError(reason, options = {}) { - const { exitCode = 1, showHelp = false } = options; - return Object.assign(new Error(reason), { - exitCode, - showHelp, - [FAIL_TAG]: true, - }); -} -exports.createFailError = createFailError; -function createFlagError(reason) { - return createFailError(reason, { - showHelp: true, - }); -} -exports.createFlagError = createFlagError; -function isFailError(error) { - return Boolean(error && error[FAIL_TAG]); -} -exports.isFailError = isFailError; -function combineErrors(errors) { - if (errors.length === 1) { - return errors[0]; - } - const exitCode = errors - .filter(isFailError) - .reduce((acc, error) => Math.max(acc, error.exitCode), 1); - const showHelp = errors.some(error => isFailError(error) && error.showHelp); - const message = errors.reduce((acc, error) => { - if (isFailError(error)) { - return acc + '\n' + error.message; - } - return acc + `\nUNHANDLED ERROR\n${util_1.inspect(error)}`; - }, ''); - return createFailError(`${errors.length} errors:\n${message}`, { - exitCode, - showHelp, - }); -} -exports.combineErrors = combineErrors; - - -/***/ }), -/* 426 */ -/***/ (function(module, exports, __webpack_require__) { +const path = __webpack_require__(16); +const {promisify} = __webpack_require__(29); +const fs = __webpack_require__(424); +const stripBom = __webpack_require__(428); +const parseJson = __webpack_require__(429); -"use strict"; +const parse = (data, filePath, options = {}) => { + data = stripBom(data); -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = __webpack_require__(36); -const path_1 = __webpack_require__(16); -const dedent_1 = tslib_1.__importDefault(__webpack_require__(14)); -const getopts_1 = tslib_1.__importDefault(__webpack_require__(427)); -function getFlags(argv, options) { - const unexpectedNames = new Set(); - const flagOpts = options.flags || {}; - const { verbose, quiet, silent, debug, help, _, ...others } = getopts_1.default(argv, { - string: flagOpts.string, - boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], - alias: { - ...(flagOpts.alias || {}), - v: 'verbose', - }, - default: flagOpts.default, - unknown: (name) => { - unexpectedNames.add(name); - return flagOpts.guessTypesForUnexpectedFlags; - }, - }); - const unexpected = []; - for (const unexpectedName of unexpectedNames) { - const matchingArgv = []; - iterArgv: for (const [i, v] of argv.entries()) { - for (const prefix of ['--', '-']) { - if (v.startsWith(prefix)) { - // -/--name=value - if (v.startsWith(`${prefix}${unexpectedName}=`)) { - matchingArgv.push(v); - continue iterArgv; - } - // -/--name (value possibly follows) - if (v === `${prefix}${unexpectedName}`) { - matchingArgv.push(v); - // value follows -/--name - if (argv.length > i + 1 && !argv[i + 1].startsWith('-')) { - matchingArgv.push(argv[i + 1]); - } - continue iterArgv; - } - } - } - // special case for `--no-{flag}` disabling of boolean flags - if (v === `--no-${unexpectedName}`) { - matchingArgv.push(v); - continue iterArgv; - } - // special case for shortcut flags formatted as `-abc` where `a`, `b`, - // and `c` will be three separate unexpected flags - if (unexpectedName.length === 1 && - v[0] === '-' && - v[1] !== '-' && - !v.includes('=') && - v.includes(unexpectedName)) { - matchingArgv.push(`-${unexpectedName}`); - continue iterArgv; - } - } - if (matchingArgv.length) { - unexpected.push(...matchingArgv); - } - else { - throw new Error(`unable to find unexpected flag named "${unexpectedName}"`); - } - } - return { - verbose, - quiet, - silent, - debug, - help, - _, - unexpected, - ...others, - }; -} -exports.getFlags = getFlags; -function getHelp(options) { - var _a, _b; - const usage = options.usage || `node ${path_1.relative(process.cwd(), process.argv[1])}`; - const optionHelp = (dedent_1.default(((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.flags) === null || _b === void 0 ? void 0 : _b.help) || '') + - '\n' + - dedent_1.default ` - --verbose, -v Log verbosely - --debug Log debug messages (less than verbose) - --quiet Only log errors - --silent Don't log anything - --help Show this message - `) - .split('\n') - .filter(Boolean) - .join('\n '); - return ` - ${usage} + if (typeof options.beforeParse === 'function') { + data = options.beforeParse(data); + } - ${dedent_1.default(options.description || 'Runs a dev task') - .split('\n') - .join('\n ')} + return parseJson(data, options.reviver, path.relative(process.cwd(), filePath)); +}; - Options: - ${optionHelp + '\n\n'}`; -} -exports.getHelp = getHelp; +module.exports = async (filePath, options) => parse(await promisify(fs.readFile)(filePath, 'utf8'), filePath, options); +module.exports.sync = (filePath, options) => parse(fs.readFileSync(filePath, 'utf8'), filePath, options); /***/ }), -/* 427 */ +/* 424 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; +var fs = __webpack_require__(23) +var polyfills = __webpack_require__(425) +var legacy = __webpack_require__(426) +var clone = __webpack_require__(427) +var queue = [] -const EMPTYARR = [] -const SHORTSPLIT = /$|[!-@[-`{-~][\s\S]*/g -const isArray = Array.isArray +var util = __webpack_require__(29) -const parseValue = function(any) { - if (any === "") return "" - if (any === "false") return false - const maybe = Number(any) - return maybe * 0 === 0 ? maybe : any +function noop () {} + +var debug = noop +if (util.debuglog) + debug = util.debuglog('gfs4') +else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) + debug = function() { + var m = util.format.apply(util, arguments) + m = 'GFS4: ' + m.split(/\n/).join('\nGFS4: ') + console.error(m) + } + +if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { + process.on('exit', function() { + debug(queue) + __webpack_require__(30).equal(queue.length, 0) + }) } -const parseAlias = function(aliases) { - let out = {}, - key, - alias, - prev, - len, - any, - i, - k +module.exports = patch(clone(fs)) +if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { + module.exports = patch(fs) + fs.__patched = true; +} - for (key in aliases) { - any = aliases[key] - alias = out[key] = isArray(any) ? any : [any] +// Always patch fs.close/closeSync, because we want to +// retry() whenever a close happens *anywhere* in the program. +// This is essential when multiple graceful-fs instances are +// in play at the same time. +module.exports.close = (function (fs$close) { return function (fd, cb) { + return fs$close.call(fs, fd, function (err) { + if (!err) + retry() - for (i = 0, len = alias.length; i < len; i++) { - prev = out[alias[i]] = [key] + if (typeof cb === 'function') + cb.apply(this, arguments) + }) +}})(fs.close) - for (k = 0; k < len; k++) { - if (i !== k) prev.push(alias[k]) - } - } - } +module.exports.closeSync = (function (fs$closeSync) { return function (fd) { + // Note that graceful-fs also retries when fs.closeSync() fails. + // Looks like a bug to me, although it's probably a harmless one. + var rval = fs$closeSync.apply(fs, arguments) + retry() + return rval +}})(fs.closeSync) - return out +// Only patch fs once, otherwise we'll run into a memory leak if +// graceful-fs is loaded multiple times, such as in test environments that +// reset the loaded modules between tests. +// We look for the string `graceful-fs` from the comment above. This +// way we are not adding any extra properties and it will detect if older +// versions of graceful-fs are installed. +if (!/\bgraceful-fs\b/.test(fs.closeSync.toString())) { + fs.closeSync = module.exports.closeSync; + fs.close = module.exports.close; } -const parseDefault = function(aliases, defaults) { - let out = {}, - key, - alias, - value, - len, - i - - for (key in defaults) { - value = defaults[key] - alias = aliases[key] +function patch (fs) { + // Everything that references the open() function needs to be in here + polyfills(fs) + fs.gracefulify = patch + fs.FileReadStream = ReadStream; // Legacy name. + fs.FileWriteStream = WriteStream; // Legacy name. + fs.createReadStream = createReadStream + fs.createWriteStream = createWriteStream + var fs$readFile = fs.readFile + fs.readFile = readFile + function readFile (path, options, cb) { + if (typeof options === 'function') + cb = options, options = null - out[key] = value + return go$readFile(path, options, cb) - if (alias === undefined) { - aliases[key] = EMPTYARR - } else { - for (i = 0, len = alias.length; i < len; i++) { - out[alias[i]] = value - } + function go$readFile (path, options, cb) { + return fs$readFile(path, options, function (err) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$readFile, [path, options, cb]]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + retry() + } + }) } } - return out -} + var fs$writeFile = fs.writeFile + fs.writeFile = writeFile + function writeFile (path, data, options, cb) { + if (typeof options === 'function') + cb = options, options = null -const parseOptions = function(aliases, options, value) { - let out = {}, - key, - alias, - len, - end, - i, - k - - if (options !== undefined) { - for (i = 0, len = options.length; i < len; i++) { - key = options[i] - alias = aliases[key] - - out[key] = value - - if (alias === undefined) { - aliases[key] = EMPTYARR - } else { - for (k = 0, end = alias.length; k < end; k++) { - out[alias[k]] = value - } - } - } - } - - return out -} - -const write = function(out, key, value, aliases, unknown) { - let i, - prev, - alias = aliases[key], - len = alias === undefined ? -1 : alias.length - - if (len >= 0 || unknown === undefined || unknown(key)) { - prev = out[key] - - if (prev === undefined) { - out[key] = value - } else { - if (isArray(prev)) { - prev.push(value) - } else { - out[key] = [prev, value] - } - } - - for (i = 0; i < len; i++) { - out[alias[i]] = out[key] - } - } -} - -const getopts = function(argv, opts) { - let unknown = (opts = opts || {}).unknown, - aliases = parseAlias(opts.alias), - strings = parseOptions(aliases, opts.string, ""), - values = parseDefault(aliases, opts.default), - bools = parseOptions(aliases, opts.boolean, false), - stopEarly = opts.stopEarly, - _ = [], - out = { _ }, - i = 0, - k = 0, - len = argv.length, - key, - arg, - end, - match, - value - - for (; i < len; i++) { - arg = argv[i] - - if (arg[0] !== "-" || arg === "-") { - if (stopEarly) while (i < len) _.push(argv[i++]) - else _.push(arg) - } else if (arg === "--") { - while (++i < len) _.push(argv[i]) - } else if (arg[1] === "-") { - end = arg.indexOf("=", 2) - if (arg[2] === "n" && arg[3] === "o" && arg[4] === "-") { - key = arg.slice(5, end >= 0 ? end : undefined) - value = false - } else if (end >= 0) { - key = arg.slice(2, end) - value = - bools[key] !== undefined || - (strings[key] === undefined - ? parseValue(arg.slice(end + 1)) - : arg.slice(end + 1)) - } else { - key = arg.slice(2) - value = - bools[key] !== undefined || - (len === i + 1 || argv[i + 1][0] === "-" - ? strings[key] === undefined - ? true - : "" - : strings[key] === undefined - ? parseValue(argv[++i]) - : argv[++i]) - } - write(out, key, value, aliases, unknown) - } else { - SHORTSPLIT.lastIndex = 2 - match = SHORTSPLIT.exec(arg) - end = match.index - value = match[0] - - for (k = 1; k < end; k++) { - write( - out, - (key = arg[k]), - k + 1 < end - ? strings[key] === undefined || - arg.substring(k + 1, (k = end)) + value - : value === "" - ? len === i + 1 || argv[i + 1][0] === "-" - ? strings[key] === undefined || "" - : bools[key] !== undefined || - (strings[key] === undefined ? parseValue(argv[++i]) : argv[++i]) - : bools[key] !== undefined || - (strings[key] === undefined ? parseValue(value) : value), - aliases, - unknown - ) - } - } - } - - for (key in values) if (out[key] === undefined) out[key] = values[key] - for (key in bools) if (out[key] === undefined) out[key] = false - for (key in strings) if (out[key] === undefined) out[key] = "" - - return out -} - -module.exports = getopts - - -/***/ }), -/* 428 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = __webpack_require__(36); -const path_1 = tslib_1.__importDefault(__webpack_require__(16)); -const fs_1 = tslib_1.__importDefault(__webpack_require__(23)); -const load_json_file_1 = tslib_1.__importDefault(__webpack_require__(429)); -const isKibanaDir = (dir) => { - try { - const path = path_1.default.resolve(dir, 'package.json'); - const json = load_json_file_1.default.sync(path); - if (json && typeof json === 'object' && 'name' in json && json.name === 'kibana') { - return true; - } - } - catch (error) { - if (error && error.code === 'ENOENT') { - return false; - } - throw error; - } -}; -// search for the kibana directory, since this file is moved around it might -// not be where we think but should always be a relatively close parent -// of this directory -const startDir = fs_1.default.realpathSync(__dirname); -const { root: rootDir } = path_1.default.parse(startDir); -let cursor = startDir; -while (true) { - if (isKibanaDir(cursor)) { - break; - } - const parent = path_1.default.dirname(cursor); - if (parent === rootDir) { - throw new Error(`unable to find kibana directory from ${startDir}`); - } - cursor = parent; -} -exports.REPO_ROOT = cursor; - - -/***/ }), -/* 429 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -const path = __webpack_require__(16); -const {promisify} = __webpack_require__(29); -const fs = __webpack_require__(430); -const stripBom = __webpack_require__(434); -const parseJson = __webpack_require__(435); - -const parse = (data, filePath, options = {}) => { - data = stripBom(data); - - if (typeof options.beforeParse === 'function') { - data = options.beforeParse(data); - } - - return parseJson(data, options.reviver, path.relative(process.cwd(), filePath)); -}; - -module.exports = async (filePath, options) => parse(await promisify(fs.readFile)(filePath, 'utf8'), filePath, options); -module.exports.sync = (filePath, options) => parse(fs.readFileSync(filePath, 'utf8'), filePath, options); - - -/***/ }), -/* 430 */ -/***/ (function(module, exports, __webpack_require__) { - -var fs = __webpack_require__(23) -var polyfills = __webpack_require__(431) -var legacy = __webpack_require__(432) -var clone = __webpack_require__(433) - -var queue = [] - -var util = __webpack_require__(29) - -function noop () {} - -var debug = noop -if (util.debuglog) - debug = util.debuglog('gfs4') -else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) - debug = function() { - var m = util.format.apply(util, arguments) - m = 'GFS4: ' + m.split(/\n/).join('\nGFS4: ') - console.error(m) - } - -if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { - process.on('exit', function() { - debug(queue) - __webpack_require__(30).equal(queue.length, 0) - }) -} - -module.exports = patch(clone(fs)) -if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { - module.exports = patch(fs) - fs.__patched = true; -} - -// Always patch fs.close/closeSync, because we want to -// retry() whenever a close happens *anywhere* in the program. -// This is essential when multiple graceful-fs instances are -// in play at the same time. -module.exports.close = (function (fs$close) { return function (fd, cb) { - return fs$close.call(fs, fd, function (err) { - if (!err) - retry() - - if (typeof cb === 'function') - cb.apply(this, arguments) - }) -}})(fs.close) - -module.exports.closeSync = (function (fs$closeSync) { return function (fd) { - // Note that graceful-fs also retries when fs.closeSync() fails. - // Looks like a bug to me, although it's probably a harmless one. - var rval = fs$closeSync.apply(fs, arguments) - retry() - return rval -}})(fs.closeSync) - -// Only patch fs once, otherwise we'll run into a memory leak if -// graceful-fs is loaded multiple times, such as in test environments that -// reset the loaded modules between tests. -// We look for the string `graceful-fs` from the comment above. This -// way we are not adding any extra properties and it will detect if older -// versions of graceful-fs are installed. -if (!/\bgraceful-fs\b/.test(fs.closeSync.toString())) { - fs.closeSync = module.exports.closeSync; - fs.close = module.exports.close; -} - -function patch (fs) { - // Everything that references the open() function needs to be in here - polyfills(fs) - fs.gracefulify = patch - fs.FileReadStream = ReadStream; // Legacy name. - fs.FileWriteStream = WriteStream; // Legacy name. - fs.createReadStream = createReadStream - fs.createWriteStream = createWriteStream - var fs$readFile = fs.readFile - fs.readFile = readFile - function readFile (path, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$readFile(path, options, cb) - - function go$readFile (path, options, cb) { - return fs$readFile(path, options, function (err) { - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$readFile, [path, options, cb]]) - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() - } - }) - } - } - - var fs$writeFile = fs.writeFile - fs.writeFile = writeFile - function writeFile (path, data, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$writeFile(path, data, options, cb) + return go$writeFile(path, data, options, cb) function go$writeFile (path, data, options, cb) { return fs$writeFile(path, data, options, function (err) { @@ -37937,7 +37355,7 @@ function retry () { /***/ }), -/* 431 */ +/* 425 */ /***/ (function(module, exports, __webpack_require__) { var constants = __webpack_require__(25) @@ -38272,7 +37690,7 @@ function patch (fs) { /***/ }), -/* 432 */ +/* 426 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -38396,7 +37814,7 @@ function legacy (fs) { /***/ }), -/* 433 */ +/* 427 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38422,7 +37840,7 @@ function clone (obj) { /***/ }), -/* 434 */ +/* 428 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38444,15 +37862,15 @@ module.exports = string => { /***/ }), -/* 435 */ +/* 429 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const errorEx = __webpack_require__(436); -const fallback = __webpack_require__(438); -const {default: LinesAndColumns} = __webpack_require__(439); -const {codeFrameColumns} = __webpack_require__(440); +const errorEx = __webpack_require__(430); +const fallback = __webpack_require__(432); +const {default: LinesAndColumns} = __webpack_require__(433); +const {codeFrameColumns} = __webpack_require__(434); const JSONError = errorEx('JSONError', { fileName: errorEx.append('in %s'), @@ -38501,14 +37919,14 @@ module.exports = (string, reviver, filename) => { /***/ }), -/* 436 */ +/* 430 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var isArrayish = __webpack_require__(437); +var isArrayish = __webpack_require__(431); var errorEx = function errorEx(name, properties) { if (!name || name.constructor !== String) { @@ -38641,7 +38059,7 @@ module.exports = errorEx; /***/ }), -/* 437 */ +/* 431 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38658,7 +38076,7 @@ module.exports = function isArrayish(obj) { /***/ }), -/* 438 */ +/* 432 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38697,7 +38115,7 @@ function parseJson (txt, reviver, context) { /***/ }), -/* 439 */ +/* 433 */ /***/ (function(__webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; @@ -38761,7 +38179,7 @@ var LinesAndColumns = (function () { /***/ }), -/* 440 */ +/* 434 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38774,7 +38192,7 @@ exports.codeFrameColumns = codeFrameColumns; exports.default = _default; function _highlight() { - const data = _interopRequireWildcard(__webpack_require__(441)); + const data = _interopRequireWildcard(__webpack_require__(435)); _highlight = function () { return data; @@ -38940,7 +38358,7 @@ function _default(rawLines, lineNumber, colNumber, opts = {}) { } /***/ }), -/* 441 */ +/* 435 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38954,7 +38372,7 @@ exports.getChalk = getChalk; exports.default = highlight; function _jsTokens() { - const data = _interopRequireWildcard(__webpack_require__(442)); + const data = _interopRequireWildcard(__webpack_require__(436)); _jsTokens = function () { return data; @@ -38964,7 +38382,7 @@ function _jsTokens() { } function _esutils() { - const data = _interopRequireDefault(__webpack_require__(443)); + const data = _interopRequireDefault(__webpack_require__(437)); _esutils = function () { return data; @@ -38974,7 +38392,7 @@ function _esutils() { } function _chalk() { - const data = _interopRequireDefault(__webpack_require__(447)); + const data = _interopRequireDefault(__webpack_require__(441)); _chalk = function () { return data; @@ -39075,7 +38493,7 @@ function highlight(code, options = {}) { } /***/ }), -/* 442 */ +/* 436 */ /***/ (function(module, exports) { // Copyright 2014, 2015, 2016, 2017, 2018 Simon Lydell @@ -39104,7 +38522,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 443 */ +/* 437 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -39135,15 +38553,15 @@ exports.matchToToken = function(match) { (function () { 'use strict'; - exports.ast = __webpack_require__(444); - exports.code = __webpack_require__(445); - exports.keyword = __webpack_require__(446); + exports.ast = __webpack_require__(438); + exports.code = __webpack_require__(439); + exports.keyword = __webpack_require__(440); }()); /* vim: set sw=4 ts=4 et tw=80 : */ /***/ }), -/* 444 */ +/* 438 */ /***/ (function(module, exports) { /* @@ -39293,7 +38711,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 445 */ +/* 439 */ /***/ (function(module, exports) { /* @@ -39434,7 +38852,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 446 */ +/* 440 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -39464,7 +38882,7 @@ exports.matchToToken = function(match) { (function () { 'use strict'; - var code = __webpack_require__(445); + var code = __webpack_require__(439); function isStrictModeReservedWordES6(id) { switch (id) { @@ -39605,16 +39023,16 @@ exports.matchToToken = function(match) { /***/ }), -/* 447 */ +/* 441 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(448); -const stdoutColor = __webpack_require__(449).stdout; +const ansiStyles = __webpack_require__(442); +const stdoutColor = __webpack_require__(443).stdout; -const template = __webpack_require__(450); +const template = __webpack_require__(444); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -39840,7 +39258,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 448 */ +/* 442 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40013,7 +39431,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 449 */ +/* 443 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40155,7 +39573,7 @@ module.exports = { /***/ }), -/* 450 */ +/* 444 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40290,7 +39708,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 451 */ +/* 445 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40314,14 +39732,22 @@ module.exports = (chalk, tmp) => { * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -var kbn_client_1 = __webpack_require__(452); -exports.KbnClient = kbn_client_1.KbnClient; -var kbn_client_requester_1 = __webpack_require__(453); -exports.uriencode = kbn_client_requester_1.uriencode; +const path_1 = __webpack_require__(16); +exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt'); +exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key'); +exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt'); +exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12'); +exports.ES_P12_PASSWORD = 'storepass'; +exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12'); +exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); +exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key'); +exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt'); +exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12'); +exports.KBN_P12_PASSWORD = 'storepass'; /***/ }), -/* 452 */ +/* 446 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40345,50 +39771,627 @@ exports.uriencode = kbn_client_requester_1.uriencode; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(453); -const kbn_client_status_1 = __webpack_require__(495); -const kbn_client_plugins_1 = __webpack_require__(496); -const kbn_client_version_1 = __webpack_require__(497); -const kbn_client_saved_objects_1 = __webpack_require__(498); -const kbn_client_ui_settings_1 = __webpack_require__(499); -class KbnClient { - /** - * Basic Kibana server client that implements common behaviors for talking - * to the Kibana server from dev tooling. - * - * @param log ToolingLog - * @param kibanaUrls Array of kibana server urls to send requests to - * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets - */ - constructor(log, kibanaUrls, uiSettingDefaults) { - this.log = log; - this.kibanaUrls = kibanaUrls; - this.uiSettingDefaults = uiSettingDefaults; - this.requester = new kbn_client_requester_1.KbnClientRequester(this.log, this.kibanaUrls); - this.status = new kbn_client_status_1.KbnClientStatus(this.requester); - this.plugins = new kbn_client_plugins_1.KbnClientPlugins(this.status); - this.version = new kbn_client_version_1.KbnClientVersion(this.status); - this.savedObjects = new kbn_client_saved_objects_1.KbnClientSavedObjects(this.log, this.requester); - this.uiSettings = new kbn_client_ui_settings_1.KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); - if (!kibanaUrls.length) { - throw new Error('missing Kibana urls'); +var run_1 = __webpack_require__(447); +exports.run = run_1.run; +var fail_1 = __webpack_require__(448); +exports.createFailError = fail_1.createFailError; +exports.createFlagError = fail_1.createFlagError; +exports.combineErrors = fail_1.combineErrors; +exports.isFailError = fail_1.isFailError; + + +/***/ }), +/* 447 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +// @ts-ignore @types are outdated and module is super simple +const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); +const tooling_log_1 = __webpack_require__(415); +const fail_1 = __webpack_require__(448); +const flags_1 = __webpack_require__(449); +const proc_runner_1 = __webpack_require__(37); +async function run(fn, options = {}) { + var _a; + const flags = flags_1.getFlags(process.argv.slice(2), options); + if (flags.help) { + process.stderr.write(flags_1.getHelp(options)); + process.exit(1); + } + const log = new tooling_log_1.ToolingLog({ + level: tooling_log_1.pickLevelFromFlags(flags), + writeTo: process.stdout, + }); + process.on('unhandledRejection', error => { + log.error('UNHANDLED PROMISE REJECTION'); + log.error(error); + process.exit(1); + }); + const handleErrorWithoutExit = (error) => { + if (fail_1.isFailError(error)) { + log.error(error.message); + if (error.showHelp) { + log.write(flags_1.getHelp(options)); + } + process.exitCode = error.exitCode; + } + else { + log.error('UNHANDLED ERROR'); + log.error(error); + process.exitCode = 1; + } + }; + const doCleanup = () => { + const tasks = cleanupTasks.slice(0); + cleanupTasks.length = 0; + for (const task of tasks) { + try { + task(); + } + catch (error) { + handleErrorWithoutExit(error); + } + } + }; + const unhookExit = exit_hook_1.default(doCleanup); + const cleanupTasks = [unhookExit]; + try { + if (!((_a = options.flags) === null || _a === void 0 ? void 0 : _a.allowUnexpected) && flags.unexpected.length) { + throw fail_1.createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); + } + try { + await proc_runner_1.withProcRunner(log, async (procRunner) => { + await fn({ + log, + flags, + procRunner, + addCleanupTask: (task) => cleanupTasks.push(task), + }); + }); + } + finally { + doCleanup(); } } - /** - * Make a direct request to the Kibana server - */ - async request(options) { - return await this.requester.request(options); + catch (error) { + handleErrorWithoutExit(error); + process.exit(); } - resolveUrl(relativeUrl) { - return this.requester.resolveUrl(relativeUrl); +} +exports.run = run; + + +/***/ }), +/* 448 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const util_1 = __webpack_require__(29); +const FAIL_TAG = Symbol('fail error'); +function createFailError(reason, options = {}) { + const { exitCode = 1, showHelp = false } = options; + return Object.assign(new Error(reason), { + exitCode, + showHelp, + [FAIL_TAG]: true, + }); +} +exports.createFailError = createFailError; +function createFlagError(reason) { + return createFailError(reason, { + showHelp: true, + }); +} +exports.createFlagError = createFlagError; +function isFailError(error) { + return Boolean(error && error[FAIL_TAG]); +} +exports.isFailError = isFailError; +function combineErrors(errors) { + if (errors.length === 1) { + return errors[0]; } + const exitCode = errors + .filter(isFailError) + .reduce((acc, error) => Math.max(acc, error.exitCode), 1); + const showHelp = errors.some(error => isFailError(error) && error.showHelp); + const message = errors.reduce((acc, error) => { + if (isFailError(error)) { + return acc + '\n' + error.message; + } + return acc + `\nUNHANDLED ERROR\n${util_1.inspect(error)}`; + }, ''); + return createFailError(`${errors.length} errors:\n${message}`, { + exitCode, + showHelp, + }); } -exports.KbnClient = KbnClient; +exports.combineErrors = combineErrors; /***/ }), -/* 453 */ +/* 449 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +const path_1 = __webpack_require__(16); +const dedent_1 = tslib_1.__importDefault(__webpack_require__(14)); +const getopts_1 = tslib_1.__importDefault(__webpack_require__(450)); +function getFlags(argv, options) { + const unexpectedNames = new Set(); + const flagOpts = options.flags || {}; + const { verbose, quiet, silent, debug, help, _, ...others } = getopts_1.default(argv, { + string: flagOpts.string, + boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], + alias: { + ...(flagOpts.alias || {}), + v: 'verbose', + }, + default: flagOpts.default, + unknown: (name) => { + unexpectedNames.add(name); + return flagOpts.guessTypesForUnexpectedFlags; + }, + }); + const unexpected = []; + for (const unexpectedName of unexpectedNames) { + const matchingArgv = []; + iterArgv: for (const [i, v] of argv.entries()) { + for (const prefix of ['--', '-']) { + if (v.startsWith(prefix)) { + // -/--name=value + if (v.startsWith(`${prefix}${unexpectedName}=`)) { + matchingArgv.push(v); + continue iterArgv; + } + // -/--name (value possibly follows) + if (v === `${prefix}${unexpectedName}`) { + matchingArgv.push(v); + // value follows -/--name + if (argv.length > i + 1 && !argv[i + 1].startsWith('-')) { + matchingArgv.push(argv[i + 1]); + } + continue iterArgv; + } + } + } + // special case for `--no-{flag}` disabling of boolean flags + if (v === `--no-${unexpectedName}`) { + matchingArgv.push(v); + continue iterArgv; + } + // special case for shortcut flags formatted as `-abc` where `a`, `b`, + // and `c` will be three separate unexpected flags + if (unexpectedName.length === 1 && + v[0] === '-' && + v[1] !== '-' && + !v.includes('=') && + v.includes(unexpectedName)) { + matchingArgv.push(`-${unexpectedName}`); + continue iterArgv; + } + } + if (matchingArgv.length) { + unexpected.push(...matchingArgv); + } + else { + throw new Error(`unable to find unexpected flag named "${unexpectedName}"`); + } + } + return { + verbose, + quiet, + silent, + debug, + help, + _, + unexpected, + ...others, + }; +} +exports.getFlags = getFlags; +function getHelp(options) { + var _a, _b; + const usage = options.usage || `node ${path_1.relative(process.cwd(), process.argv[1])}`; + const optionHelp = (dedent_1.default(((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.flags) === null || _b === void 0 ? void 0 : _b.help) || '') + + '\n' + + dedent_1.default ` + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + `) + .split('\n') + .filter(Boolean) + .join('\n '); + return ` + ${usage} + + ${dedent_1.default(options.description || 'Runs a dev task') + .split('\n') + .join('\n ')} + + Options: + ${optionHelp + '\n\n'}`; +} +exports.getHelp = getHelp; + + +/***/ }), +/* 450 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const EMPTYARR = [] +const SHORTSPLIT = /$|[!-@[-`{-~][\s\S]*/g +const isArray = Array.isArray + +const parseValue = function(any) { + if (any === "") return "" + if (any === "false") return false + const maybe = Number(any) + return maybe * 0 === 0 ? maybe : any +} + +const parseAlias = function(aliases) { + let out = {}, + key, + alias, + prev, + len, + any, + i, + k + + for (key in aliases) { + any = aliases[key] + alias = out[key] = isArray(any) ? any : [any] + + for (i = 0, len = alias.length; i < len; i++) { + prev = out[alias[i]] = [key] + + for (k = 0; k < len; k++) { + if (i !== k) prev.push(alias[k]) + } + } + } + + return out +} + +const parseDefault = function(aliases, defaults) { + let out = {}, + key, + alias, + value, + len, + i + + for (key in defaults) { + value = defaults[key] + alias = aliases[key] + + out[key] = value + + if (alias === undefined) { + aliases[key] = EMPTYARR + } else { + for (i = 0, len = alias.length; i < len; i++) { + out[alias[i]] = value + } + } + } + + return out +} + +const parseOptions = function(aliases, options, value) { + let out = {}, + key, + alias, + len, + end, + i, + k + + if (options !== undefined) { + for (i = 0, len = options.length; i < len; i++) { + key = options[i] + alias = aliases[key] + + out[key] = value + + if (alias === undefined) { + aliases[key] = EMPTYARR + } else { + for (k = 0, end = alias.length; k < end; k++) { + out[alias[k]] = value + } + } + } + } + + return out +} + +const write = function(out, key, value, aliases, unknown) { + let i, + prev, + alias = aliases[key], + len = alias === undefined ? -1 : alias.length + + if (len >= 0 || unknown === undefined || unknown(key)) { + prev = out[key] + + if (prev === undefined) { + out[key] = value + } else { + if (isArray(prev)) { + prev.push(value) + } else { + out[key] = [prev, value] + } + } + + for (i = 0; i < len; i++) { + out[alias[i]] = out[key] + } + } +} + +const getopts = function(argv, opts) { + let unknown = (opts = opts || {}).unknown, + aliases = parseAlias(opts.alias), + strings = parseOptions(aliases, opts.string, ""), + values = parseDefault(aliases, opts.default), + bools = parseOptions(aliases, opts.boolean, false), + stopEarly = opts.stopEarly, + _ = [], + out = { _ }, + i = 0, + k = 0, + len = argv.length, + key, + arg, + end, + match, + value + + for (; i < len; i++) { + arg = argv[i] + + if (arg[0] !== "-" || arg === "-") { + if (stopEarly) while (i < len) _.push(argv[i++]) + else _.push(arg) + } else if (arg === "--") { + while (++i < len) _.push(argv[i]) + } else if (arg[1] === "-") { + end = arg.indexOf("=", 2) + if (arg[2] === "n" && arg[3] === "o" && arg[4] === "-") { + key = arg.slice(5, end >= 0 ? end : undefined) + value = false + } else if (end >= 0) { + key = arg.slice(2, end) + value = + bools[key] !== undefined || + (strings[key] === undefined + ? parseValue(arg.slice(end + 1)) + : arg.slice(end + 1)) + } else { + key = arg.slice(2) + value = + bools[key] !== undefined || + (len === i + 1 || argv[i + 1][0] === "-" + ? strings[key] === undefined + ? true + : "" + : strings[key] === undefined + ? parseValue(argv[++i]) + : argv[++i]) + } + write(out, key, value, aliases, unknown) + } else { + SHORTSPLIT.lastIndex = 2 + match = SHORTSPLIT.exec(arg) + end = match.index + value = match[0] + + for (k = 1; k < end; k++) { + write( + out, + (key = arg[k]), + k + 1 < end + ? strings[key] === undefined || + arg.substring(k + 1, (k = end)) + value + : value === "" + ? len === i + 1 || argv[i + 1][0] === "-" + ? strings[key] === undefined || "" + : bools[key] !== undefined || + (strings[key] === undefined ? parseValue(argv[++i]) : argv[++i]) + : bools[key] !== undefined || + (strings[key] === undefined ? parseValue(value) : value), + aliases, + unknown + ) + } + } + } + + for (key in values) if (out[key] === undefined) out[key] = values[key] + for (key in bools) if (out[key] === undefined) out[key] = false + for (key in strings) if (out[key] === undefined) out[key] = "" + + return out +} + +module.exports = getopts + + +/***/ }), +/* 451 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +var kbn_client_1 = __webpack_require__(452); +exports.KbnClient = kbn_client_1.KbnClient; +var kbn_client_requester_1 = __webpack_require__(453); +exports.uriencode = kbn_client_requester_1.uriencode; + + +/***/ }), +/* 452 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const kbn_client_requester_1 = __webpack_require__(453); +const kbn_client_status_1 = __webpack_require__(495); +const kbn_client_plugins_1 = __webpack_require__(496); +const kbn_client_version_1 = __webpack_require__(497); +const kbn_client_saved_objects_1 = __webpack_require__(498); +const kbn_client_ui_settings_1 = __webpack_require__(499); +class KbnClient { + /** + * Basic Kibana server client that implements common behaviors for talking + * to the Kibana server from dev tooling. + * + * @param log ToolingLog + * @param kibanaUrls Array of kibana server urls to send requests to + * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets + */ + constructor(log, kibanaUrls, uiSettingDefaults) { + this.log = log; + this.kibanaUrls = kibanaUrls; + this.uiSettingDefaults = uiSettingDefaults; + this.requester = new kbn_client_requester_1.KbnClientRequester(this.log, this.kibanaUrls); + this.status = new kbn_client_status_1.KbnClientStatus(this.requester); + this.plugins = new kbn_client_plugins_1.KbnClientPlugins(this.status); + this.version = new kbn_client_version_1.KbnClientVersion(this.status); + this.savedObjects = new kbn_client_saved_objects_1.KbnClientSavedObjects(this.log, this.requester); + this.uiSettings = new kbn_client_ui_settings_1.KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + if (!kibanaUrls.length) { + throw new Error('missing Kibana urls'); + } + } + /** + * Make a direct request to the Kibana server + */ + async request(options) { + return await this.requester.request(options); + } + resolveUrl(relativeUrl) { + return this.requester.resolveUrl(relativeUrl); + } +} +exports.KbnClient = KbnClient; + + +/***/ }), +/* 453 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43043,28 +43046,21 @@ module.exports = require("tty"); const os = __webpack_require__(11); const hasFlag = __webpack_require__(12); -const {env} = process; +const env = process.env; let forceColor; if (hasFlag('no-color') || hasFlag('no-colors') || - hasFlag('color=false') || - hasFlag('color=never')) { - forceColor = 0; + hasFlag('color=false')) { + forceColor = false; } else if (hasFlag('color') || hasFlag('colors') || hasFlag('color=true') || hasFlag('color=always')) { - forceColor = 1; + forceColor = true; } if ('FORCE_COLOR' in env) { - if (env.FORCE_COLOR === true || env.FORCE_COLOR === 'true') { - forceColor = 1; - } else if (env.FORCE_COLOR === false || env.FORCE_COLOR === 'false') { - forceColor = 0; - } else { - forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3); - } + forceColor = env.FORCE_COLOR.length === 0 || parseInt(env.FORCE_COLOR, 10) !== 0; } function translateLevel(level) { @@ -43081,7 +43077,7 @@ function translateLevel(level) { } function supportsColor(stream) { - if (forceColor === 0) { + if (forceColor === false) { return 0; } @@ -43095,15 +43091,11 @@ function supportsColor(stream) { return 2; } - if (stream && !stream.isTTY && forceColor === undefined) { + if (stream && !stream.isTTY && forceColor !== true) { return 0; } - const min = forceColor || 0; - - if (env.TERM === 'dumb') { - return min; - } + const min = forceColor ? 1 : 0; if (process.platform === 'win32') { // Node.js 7.5.0 is the first version of Node.js to include a patch to @@ -43164,6 +43156,10 @@ function supportsColor(stream) { return 1; } + if (env.TERM === 'dumb') { + return min; + } + return min; } @@ -47879,10 +47875,10 @@ module.exports.sync = options => { "use strict"; -const errorEx = __webpack_require__(436); -const fallback = __webpack_require__(438); -const {default: LinesAndColumns} = __webpack_require__(439); -const {codeFrameColumns} = __webpack_require__(440); +const errorEx = __webpack_require__(430); +const fallback = __webpack_require__(432); +const {default: LinesAndColumns} = __webpack_require__(433); +const {codeFrameColumns} = __webpack_require__(434); const JSONError = errorEx('JSONError', { fileName: errorEx.append('in %s'), @@ -80679,7 +80675,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(923); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(929); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -80859,16 +80855,24 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); -const arrify = __webpack_require__(708); -const globby = __webpack_require__(709); -const cpFile = __webpack_require__(912); -const CpyError = __webpack_require__(921); +const os = __webpack_require__(11); +const pAll = __webpack_require__(708); +const arrify = __webpack_require__(710); +const globby = __webpack_require__(711); +const isGlob = __webpack_require__(606); +const cpFile = __webpack_require__(914); +const junk = __webpack_require__(926); +const CpyError = __webpack_require__(927); + +const defaultOptions = { + ignoreJunk: true +}; -const preprocessSrcPath = (srcPath, options) => options.cwd ? path.resolve(options.cwd, srcPath) : srcPath; +const preprocessSourcePath = (source, options) => options.cwd ? path.resolve(options.cwd, source) : source; -const preprocessDestPath = (srcPath, dest, options) => { - let basename = path.basename(srcPath); - const dirname = path.dirname(srcPath); +const preprocessDestinationPath = (source, destination, options) => { + let basename = path.basename(source); + const dirname = path.dirname(source); if (typeof options.rename === 'string') { basename = options.rename; @@ -80877,122 +80881,239 @@ const preprocessDestPath = (srcPath, dest, options) => { } if (options.cwd) { - dest = path.resolve(options.cwd, dest); + destination = path.resolve(options.cwd, destination); } if (options.parents) { - return path.join(dest, dirname, basename); + return path.join(destination, dirname, basename); } - return path.join(dest, basename); + return path.join(destination, basename); }; -const cpy = (src, dest, options = {}) => { - src = arrify(src); - +module.exports = (source, destination, { + concurrency = (os.cpus().length || 1) * 2, + ...options +} = {}) => { const progressEmitter = new EventEmitter(); - if (src.length === 0 || !dest) { - const promise = Promise.reject(new CpyError('`files` and `destination` required')); - promise.on = (...args) => { - progressEmitter.on(...args); - return promise; - }; + options = { + ...defaultOptions, + ...options + }; - return promise; - } + const promise = (async () => { + source = arrify(source); - const copyStatus = new Map(); - let completedFiles = 0; - let completedSize = 0; + if (source.length === 0 || !destination) { + throw new CpyError('`source` and `destination` required'); + } - const promise = globby(src, options) - .catch(error => { - throw new CpyError(`Cannot glob \`${src}\`: ${error.message}`, error); - }) - .then(files => { - if (files.length === 0) { - progressEmitter.emit('progress', { - totalFiles: 0, - percent: 1, - completedFiles: 0, - completedSize: 0 - }); + const copyStatus = new Map(); + let completedFiles = 0; + let completedSize = 0; + + let files; + try { + files = await globby(source, options); + + if (options.ignoreJunk) { + files = files.filter(file => junk.not(path.basename(file))); } + } catch (error) { + throw new CpyError(`Cannot glob \`${source}\`: ${error.message}`, error); + } - return Promise.all(files.map(srcPath => { - const from = preprocessSrcPath(srcPath, options); - const to = preprocessDestPath(srcPath, dest, options); + const sourcePaths = source.filter(value => !isGlob(value)); - return cpFile(from, to, options) - .on('progress', event => { - const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0}; + if (files.length === 0 || (sourcePaths.length > 0 && !sourcePaths.every(value => files.includes(value)))) { + throw new CpyError(`Cannot copy \`${source}\`: the file doesn't exist`); + } - if (fileStatus.written !== event.written || fileStatus.percent !== event.percent) { - completedSize -= fileStatus.written; - completedSize += event.written; + const fileProgressHandler = event => { + const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0}; - if (event.percent === 1 && fileStatus.percent !== 1) { - completedFiles++; - } + if (fileStatus.written !== event.written || fileStatus.percent !== event.percent) { + completedSize -= fileStatus.written; + completedSize += event.written; - copyStatus.set(event.src, {written: event.written, percent: event.percent}); + if (event.percent === 1 && fileStatus.percent !== 1) { + completedFiles++; + } - progressEmitter.emit('progress', { - totalFiles: files.length, - percent: completedFiles / files.length, - completedFiles, - completedSize - }); - } - }) - .then(() => to) - .catch(error => { - throw new CpyError(`Cannot copy from \`${from}\` to \`${to}\`: ${error.message}`, error); - }); - })); - }); + copyStatus.set(event.src, { + written: event.written, + percent: event.percent + }); - promise.on = (...args) => { - progressEmitter.on(...args); + progressEmitter.emit('progress', { + totalFiles: files.length, + percent: completedFiles / files.length, + completedFiles, + completedSize + }); + } + }; + + return pAll(files.map(sourcePath => { + return async () => { + const from = preprocessSourcePath(sourcePath, options); + const to = preprocessDestinationPath(sourcePath, destination, options); + + try { + await cpFile(from, to, options).on('progress', fileProgressHandler); + } catch (error) { + throw new CpyError(`Cannot copy from \`${from}\` to \`${to}\`: ${error.message}`, error); + } + + return to; + }; + }), {concurrency}); + })(); + + promise.on = (...arguments_) => { + progressEmitter.on(...arguments_); return promise; }; return promise; }; -module.exports = cpy; + +/***/ }), +/* 708 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const pMap = __webpack_require__(709); + +module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release -module.exports.default = cpy; +module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 709 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const pMap = (iterable, mapper, options) => new Promise((resolve, reject) => { + options = Object.assign({ + concurrency: Infinity + }, options); + + if (typeof mapper !== 'function') { + throw new TypeError('Mapper function is required'); + } + + const {concurrency} = options; + + if (!(typeof concurrency === 'number' && concurrency >= 1)) { + throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${concurrency}\` (${typeof concurrency})`); + } + + const ret = []; + const iterator = iterable[Symbol.iterator](); + let isRejected = false; + let isIterableDone = false; + let resolvingCount = 0; + let currentIndex = 0; + + const next = () => { + if (isRejected) { + return; + } + + const nextItem = iterator.next(); + const i = currentIndex; + currentIndex++; + + if (nextItem.done) { + isIterableDone = true; + + if (resolvingCount === 0) { + resolve(ret); + } + + return; + } + + resolvingCount++; + + Promise.resolve(nextItem.value) + .then(element => mapper(element, i)) + .then( + value => { + ret[i] = value; + resolvingCount--; + next(); + }, + error => { + isRejected = true; + reject(error); + } + ); + }; + + for (let i = 0; i < concurrency; i++) { + next(); + + if (isIterableDone) { + break; + } + } +}); + +module.exports = pMap; +// TODO: Remove this for the next major release +module.exports.default = pMap; + + +/***/ }), +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = function (val) { - if (val === null || val === undefined) { + +const arrify = value => { + if (value === null || value === undefined) { return []; } - return Array.isArray(val) ? val : [val]; + if (Array.isArray(value)) { + return value; + } + + if (typeof value === 'string') { + return [value]; + } + + if (typeof value[Symbol.iterator] === 'function') { + return [...value]; + } + + return [value]; }; +module.exports = arrify; + /***/ }), -/* 709 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(710); -const glob = __webpack_require__(712); -const fastGlob = __webpack_require__(717); -const dirGlob = __webpack_require__(905); -const gitignore = __webpack_require__(908); +const arrayUnion = __webpack_require__(712); +const glob = __webpack_require__(714); +const fastGlob = __webpack_require__(719); +const dirGlob = __webpack_require__(907); +const gitignore = __webpack_require__(910); const DEFAULT_FILTER = () => false; @@ -81137,12 +81258,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 710 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(711); +var arrayUniq = __webpack_require__(713); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -81150,7 +81271,7 @@ module.exports = function () { /***/ }), -/* 711 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81219,7 +81340,7 @@ if ('Set' in global) { /***/ }), -/* 712 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -81268,13 +81389,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(713) +var inherits = __webpack_require__(715) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(715) -var common = __webpack_require__(716) +var globSync = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -82015,7 +82136,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 713 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -82025,12 +82146,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(714); + module.exports = __webpack_require__(716); } /***/ }), -/* 714 */ +/* 716 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -82063,7 +82184,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 715 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -82073,12 +82194,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(712).Glob +var Glob = __webpack_require__(714).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(716) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -82555,7 +82676,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 716 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -82801,10 +82922,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 717 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(718); +const pkg = __webpack_require__(720); module.exports = pkg.async; module.exports.default = pkg.async; @@ -82817,19 +82938,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 718 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(719); -var taskManager = __webpack_require__(720); -var reader_async_1 = __webpack_require__(876); -var reader_stream_1 = __webpack_require__(900); -var reader_sync_1 = __webpack_require__(901); -var arrayUtils = __webpack_require__(903); -var streamUtils = __webpack_require__(904); +var optionsManager = __webpack_require__(721); +var taskManager = __webpack_require__(722); +var reader_async_1 = __webpack_require__(878); +var reader_stream_1 = __webpack_require__(902); +var reader_sync_1 = __webpack_require__(903); +var arrayUtils = __webpack_require__(905); +var streamUtils = __webpack_require__(906); /** * Synchronous API. */ @@ -82895,7 +83016,7 @@ function isString(source) { /***/ }), -/* 719 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82933,13 +83054,13 @@ exports.prepare = prepare; /***/ }), -/* 720 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(721); +var patternUtils = __webpack_require__(723); /** * Generate tasks based on parent directory of each pattern. */ @@ -83030,16 +83151,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 721 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(722); -var isGlob = __webpack_require__(725); -var micromatch = __webpack_require__(726); +var globParent = __webpack_require__(724); +var isGlob = __webpack_require__(727); +var micromatch = __webpack_require__(728); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -83185,15 +83306,15 @@ exports.matchAny = matchAny; /***/ }), -/* 722 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(723); -var pathDirname = __webpack_require__(724); +var isglob = __webpack_require__(725); +var pathDirname = __webpack_require__(726); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -83216,7 +83337,7 @@ module.exports = function globParent(str) { /***/ }), -/* 723 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -83247,7 +83368,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 724 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83397,7 +83518,7 @@ module.exports.win32 = win32; /***/ }), -/* 725 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -83449,7 +83570,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 726 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83460,18 +83581,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(727); -var toRegex = __webpack_require__(829); -var extend = __webpack_require__(837); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(831); +var extend = __webpack_require__(839); /** * Local dependencies */ -var compilers = __webpack_require__(840); -var parsers = __webpack_require__(872); -var cache = __webpack_require__(873); -var utils = __webpack_require__(874); +var compilers = __webpack_require__(842); +var parsers = __webpack_require__(874); +var cache = __webpack_require__(875); +var utils = __webpack_require__(876); var MAX_LENGTH = 1024 * 64; /** @@ -84333,7 +84454,7 @@ module.exports = micromatch; /***/ }), -/* 727 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84343,18 +84464,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(728); -var unique = __webpack_require__(740); -var extend = __webpack_require__(737); +var toRegex = __webpack_require__(730); +var unique = __webpack_require__(742); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var Braces = __webpack_require__(766); -var utils = __webpack_require__(742); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var Braces = __webpack_require__(768); +var utils = __webpack_require__(744); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -84658,15 +84779,15 @@ module.exports = braces; /***/ }), -/* 728 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); -var extend = __webpack_require__(737); -var not = __webpack_require__(739); +var define = __webpack_require__(731); +var extend = __webpack_require__(739); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -84813,7 +84934,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 729 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84826,7 +84947,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(730); +var isDescriptor = __webpack_require__(732); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -84851,7 +84972,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 730 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84864,9 +84985,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(731); -var isAccessor = __webpack_require__(732); -var isData = __webpack_require__(735); +var typeOf = __webpack_require__(733); +var isAccessor = __webpack_require__(734); +var isData = __webpack_require__(737); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -84880,7 +85001,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 731 */ +/* 733 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -85033,7 +85154,7 @@ function isBuffer(val) { /***/ }), -/* 732 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85046,7 +85167,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(733); +var typeOf = __webpack_require__(735); // accessor descriptor properties var accessor = { @@ -85109,10 +85230,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 733 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85231,7 +85352,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 734 */ +/* 736 */ /***/ (function(module, exports) { /*! @@ -85258,7 +85379,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 735 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85271,7 +85392,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(736); +var typeOf = __webpack_require__(738); // data descriptor properties var data = { @@ -85320,10 +85441,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 736 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85442,13 +85563,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 737 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(738); +var isObject = __webpack_require__(740); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -85482,7 +85603,7 @@ function hasOwn(obj, key) { /***/ }), -/* 738 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85502,13 +85623,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 739 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); +var extend = __webpack_require__(739); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -85575,7 +85696,7 @@ module.exports = toRegex; /***/ }), -/* 740 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85625,13 +85746,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 741 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(742); +var utils = __webpack_require__(744); module.exports = function(braces, options) { braces.compiler @@ -85914,25 +86035,25 @@ function hasQueue(node) { /***/ }), -/* 742 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(743); +var splitString = __webpack_require__(745); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(737); -utils.flatten = __webpack_require__(749); -utils.isObject = __webpack_require__(747); -utils.fillRange = __webpack_require__(750); -utils.repeat = __webpack_require__(755); -utils.unique = __webpack_require__(740); +utils.extend = __webpack_require__(739); +utils.flatten = __webpack_require__(751); +utils.isObject = __webpack_require__(749); +utils.fillRange = __webpack_require__(752); +utils.repeat = __webpack_require__(757); +utils.unique = __webpack_require__(742); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -86264,7 +86385,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 743 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86277,7 +86398,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(744); +var extend = __webpack_require__(746); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -86442,14 +86563,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 744 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(745); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(747); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -86509,7 +86630,7 @@ function isEnum(obj, key) { /***/ }), -/* 745 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86522,7 +86643,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -86530,7 +86651,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 746 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86543,7 +86664,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); function isObjectObject(o) { return isObject(o) === true @@ -86574,7 +86695,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 747 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86593,7 +86714,7 @@ module.exports = function isObject(val) { /***/ }), -/* 748 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86640,7 +86761,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 749 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86669,7 +86790,7 @@ function flat(arr, res) { /***/ }), -/* 750 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86683,10 +86804,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(751); -var extend = __webpack_require__(737); -var repeat = __webpack_require__(753); -var toRegex = __webpack_require__(754); +var isNumber = __webpack_require__(753); +var extend = __webpack_require__(739); +var repeat = __webpack_require__(755); +var toRegex = __webpack_require__(756); /** * Return a range of numbers or letters. @@ -86884,7 +87005,7 @@ module.exports = fillRange; /***/ }), -/* 751 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86897,7 +87018,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); module.exports = function isNumber(num) { var type = typeOf(num); @@ -86913,10 +87034,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 752 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -87035,7 +87156,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 753 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87112,7 +87233,7 @@ function repeat(str, num) { /***/ }), -/* 754 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87125,8 +87246,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(753); -var isNumber = __webpack_require__(751); +var repeat = __webpack_require__(755); +var isNumber = __webpack_require__(753); var cache = {}; function toRegexRange(min, max, options) { @@ -87413,7 +87534,7 @@ module.exports = toRegexRange; /***/ }), -/* 755 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87438,14 +87559,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 756 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(757); -var utils = __webpack_require__(742); +var Node = __webpack_require__(759); +var utils = __webpack_require__(744); /** * Braces parsers @@ -87805,15 +87926,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 757 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var define = __webpack_require__(758); -var utils = __webpack_require__(765); +var isObject = __webpack_require__(749); +var define = __webpack_require__(760); +var utils = __webpack_require__(767); var ownNames; /** @@ -88304,7 +88425,7 @@ exports = module.exports = Node; /***/ }), -/* 758 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88317,7 +88438,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -88342,7 +88463,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 759 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88355,9 +88476,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(760); -var isAccessor = __webpack_require__(761); -var isData = __webpack_require__(763); +var typeOf = __webpack_require__(762); +var isAccessor = __webpack_require__(763); +var isData = __webpack_require__(765); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -88371,7 +88492,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 760 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -88506,7 +88627,7 @@ function isBuffer(val) { /***/ }), -/* 761 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88519,7 +88640,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(762); +var typeOf = __webpack_require__(764); // accessor descriptor properties var accessor = { @@ -88582,7 +88703,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 762 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -88717,7 +88838,7 @@ function isBuffer(val) { /***/ }), -/* 763 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88730,7 +88851,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(764); +var typeOf = __webpack_require__(766); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -88773,7 +88894,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 764 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -88908,13 +89029,13 @@ function isBuffer(val) { /***/ }), -/* 765 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); var utils = module.exports; /** @@ -89934,17 +90055,17 @@ function assert(val, message) { /***/ }), -/* 766 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var utils = __webpack_require__(742); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var utils = __webpack_require__(744); /** * Customize Snapdragon parser and renderer @@ -90045,17 +90166,17 @@ module.exports = Braces; /***/ }), -/* 767 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(768); -var define = __webpack_require__(729); -var Compiler = __webpack_require__(797); -var Parser = __webpack_require__(826); -var utils = __webpack_require__(806); +var Base = __webpack_require__(770); +var define = __webpack_require__(731); +var Compiler = __webpack_require__(799); +var Parser = __webpack_require__(828); +var utils = __webpack_require__(808); var regexCache = {}; var cache = {}; @@ -90226,20 +90347,20 @@ module.exports.Parser = Parser; /***/ }), -/* 768 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(769); -var CacheBase = __webpack_require__(770); -var Emitter = __webpack_require__(771); -var isObject = __webpack_require__(747); -var merge = __webpack_require__(788); -var pascal = __webpack_require__(791); -var cu = __webpack_require__(792); +var define = __webpack_require__(771); +var CacheBase = __webpack_require__(772); +var Emitter = __webpack_require__(773); +var isObject = __webpack_require__(749); +var merge = __webpack_require__(790); +var pascal = __webpack_require__(793); +var cu = __webpack_require__(794); /** * Optionally define a custom `cache` namespace to use. @@ -90668,7 +90789,7 @@ module.exports.namespace = namespace; /***/ }), -/* 769 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90681,7 +90802,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -90706,21 +90827,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 770 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var Emitter = __webpack_require__(771); -var visit = __webpack_require__(772); -var toPath = __webpack_require__(775); -var union = __webpack_require__(776); -var del = __webpack_require__(780); -var get = __webpack_require__(778); -var has = __webpack_require__(785); -var set = __webpack_require__(779); +var isObject = __webpack_require__(749); +var Emitter = __webpack_require__(773); +var visit = __webpack_require__(774); +var toPath = __webpack_require__(777); +var union = __webpack_require__(778); +var del = __webpack_require__(782); +var get = __webpack_require__(780); +var has = __webpack_require__(787); +var set = __webpack_require__(781); /** * Create a `Cache` constructor that when instantiated will @@ -90974,7 +91095,7 @@ module.exports.namespace = namespace; /***/ }), -/* 771 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { @@ -91143,7 +91264,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 772 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91156,8 +91277,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(773); -var mapVisit = __webpack_require__(774); +var visit = __webpack_require__(775); +var mapVisit = __webpack_require__(776); module.exports = function(collection, method, val) { var result; @@ -91180,7 +91301,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 773 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91193,7 +91314,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -91220,14 +91341,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 774 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(773); +var visit = __webpack_require__(775); /** * Map `visit` over an array of objects. @@ -91264,7 +91385,7 @@ function isObject(val) { /***/ }), -/* 775 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91277,7 +91398,7 @@ function isObject(val) { -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -91304,16 +91425,16 @@ function filter(arr) { /***/ }), -/* 776 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(738); -var union = __webpack_require__(777); -var get = __webpack_require__(778); -var set = __webpack_require__(779); +var isObject = __webpack_require__(740); +var union = __webpack_require__(779); +var get = __webpack_require__(780); +var set = __webpack_require__(781); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -91341,7 +91462,7 @@ function arrayify(val) { /***/ }), -/* 777 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91377,7 +91498,7 @@ module.exports = function union(init) { /***/ }), -/* 778 */ +/* 780 */ /***/ (function(module, exports) { /*! @@ -91433,7 +91554,7 @@ function toString(val) { /***/ }), -/* 779 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91446,10 +91567,10 @@ function toString(val) { -var split = __webpack_require__(743); -var extend = __webpack_require__(737); -var isPlainObject = __webpack_require__(746); -var isObject = __webpack_require__(738); +var split = __webpack_require__(745); +var extend = __webpack_require__(739); +var isPlainObject = __webpack_require__(748); +var isObject = __webpack_require__(740); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -91495,7 +91616,7 @@ function isValidKey(key) { /***/ }), -/* 780 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91508,8 +91629,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(747); -var has = __webpack_require__(781); +var isObject = __webpack_require__(749); +var has = __webpack_require__(783); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -91534,7 +91655,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 781 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91547,9 +91668,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(782); -var hasValues = __webpack_require__(784); -var get = __webpack_require__(778); +var isObject = __webpack_require__(784); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(780); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -91560,7 +91681,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 782 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91573,7 +91694,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(783); +var isArray = __webpack_require__(785); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -91581,7 +91702,7 @@ module.exports = function isObject(val) { /***/ }), -/* 783 */ +/* 785 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -91592,7 +91713,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 784 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91635,7 +91756,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 785 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91648,9 +91769,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(747); -var hasValues = __webpack_require__(786); -var get = __webpack_require__(778); +var isObject = __webpack_require__(749); +var hasValues = __webpack_require__(788); +var get = __webpack_require__(780); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -91658,7 +91779,7 @@ module.exports = function(val, prop) { /***/ }), -/* 786 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91671,8 +91792,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(787); -var isNumber = __webpack_require__(751); +var typeOf = __webpack_require__(789); +var isNumber = __webpack_require__(753); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -91725,10 +91846,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 787 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -91850,14 +91971,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 788 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(789); -var forIn = __webpack_require__(790); +var isExtendable = __webpack_require__(791); +var forIn = __webpack_require__(792); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -91921,7 +92042,7 @@ module.exports = mixinDeep; /***/ }), -/* 789 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91934,7 +92055,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -91942,7 +92063,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 790 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91965,7 +92086,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 791 */ +/* 793 */ /***/ (function(module, exports) { /*! @@ -91992,14 +92113,14 @@ module.exports = pascalcase; /***/ }), -/* 792 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(793); +var utils = __webpack_require__(795); /** * Expose class utils @@ -92364,7 +92485,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 793 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92378,10 +92499,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(777); -utils.define = __webpack_require__(729); -utils.isObj = __webpack_require__(747); -utils.staticExtend = __webpack_require__(794); +utils.union = __webpack_require__(779); +utils.define = __webpack_require__(731); +utils.isObj = __webpack_require__(749); +utils.staticExtend = __webpack_require__(796); /** @@ -92392,7 +92513,7 @@ module.exports = utils; /***/ }), -/* 794 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92405,8 +92526,8 @@ module.exports = utils; -var copy = __webpack_require__(795); -var define = __webpack_require__(729); +var copy = __webpack_require__(797); +var define = __webpack_require__(731); var util = __webpack_require__(29); /** @@ -92489,15 +92610,15 @@ module.exports = extend; /***/ }), -/* 795 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); -var copyDescriptor = __webpack_require__(796); -var define = __webpack_require__(729); +var typeOf = __webpack_require__(754); +var copyDescriptor = __webpack_require__(798); +var define = __webpack_require__(731); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -92670,7 +92791,7 @@ module.exports.has = has; /***/ }), -/* 796 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92758,16 +92879,16 @@ function isObject(val) { /***/ }), -/* 797 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:compiler'); -var utils = __webpack_require__(806); +var use = __webpack_require__(800); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:compiler'); +var utils = __webpack_require__(808); /** * Create a new `Compiler` with the given `options`. @@ -92921,7 +93042,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(825); + var sourcemaps = __webpack_require__(827); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -92942,7 +93063,7 @@ module.exports = Compiler; /***/ }), -/* 798 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92955,7 +93076,7 @@ module.exports = Compiler; -var utils = __webpack_require__(799); +var utils = __webpack_require__(801); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -93070,7 +93191,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 799 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93084,8 +93205,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(729); -utils.isObject = __webpack_require__(747); +utils.define = __webpack_require__(731); +utils.isObject = __webpack_require__(749); utils.isString = function(val) { @@ -93100,7 +93221,7 @@ module.exports = utils; /***/ }), -/* 800 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -93109,14 +93230,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(801); + module.exports = __webpack_require__(803); } else { - module.exports = __webpack_require__(804); + module.exports = __webpack_require__(806); } /***/ }), -/* 801 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -93125,7 +93246,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(804); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -93307,7 +93428,7 @@ function localstorage() { /***/ }), -/* 802 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { @@ -93323,7 +93444,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(803); +exports.humanize = __webpack_require__(805); /** * The currently active debug mode names, and names to skip. @@ -93515,7 +93636,7 @@ function coerce(val) { /***/ }), -/* 803 */ +/* 805 */ /***/ (function(module, exports) { /** @@ -93673,7 +93794,7 @@ function plural(ms, n, name) { /***/ }), -/* 804 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -93689,7 +93810,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(804); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -93868,7 +93989,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -93927,13 +94048,13 @@ exports.enable(load()); /***/ }), -/* 805 */ +/* 807 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 806 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93943,9 +94064,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(737); -exports.SourceMap = __webpack_require__(807); -exports.sourceMapResolve = __webpack_require__(818); +exports.extend = __webpack_require__(739); +exports.SourceMap = __webpack_require__(809); +exports.sourceMapResolve = __webpack_require__(820); /** * Convert backslash in the given string to forward slashes @@ -93988,7 +94109,7 @@ exports.last = function(arr, n) { /***/ }), -/* 807 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -93996,13 +94117,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(814).SourceMapConsumer; -exports.SourceNode = __webpack_require__(817).SourceNode; +exports.SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(816).SourceMapConsumer; +exports.SourceNode = __webpack_require__(819).SourceNode; /***/ }), -/* 808 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94012,10 +94133,10 @@ exports.SourceNode = __webpack_require__(817).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(809); -var util = __webpack_require__(811); -var ArraySet = __webpack_require__(812).ArraySet; -var MappingList = __webpack_require__(813).MappingList; +var base64VLQ = __webpack_require__(811); +var util = __webpack_require__(813); +var ArraySet = __webpack_require__(814).ArraySet; +var MappingList = __webpack_require__(815).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -94424,7 +94545,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 809 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94464,7 +94585,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(810); +var base64 = __webpack_require__(812); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -94570,7 +94691,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 810 */ +/* 812 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94643,7 +94764,7 @@ exports.decode = function (charCode) { /***/ }), -/* 811 */ +/* 813 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95066,7 +95187,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 812 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95076,7 +95197,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(813); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -95193,7 +95314,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 813 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95203,7 +95324,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(813); /** * Determine whether mappingB is after mappingA with respect to generated @@ -95278,7 +95399,7 @@ exports.MappingList = MappingList; /***/ }), -/* 814 */ +/* 816 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95288,11 +95409,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); -var binarySearch = __webpack_require__(815); -var ArraySet = __webpack_require__(812).ArraySet; -var base64VLQ = __webpack_require__(809); -var quickSort = __webpack_require__(816).quickSort; +var util = __webpack_require__(813); +var binarySearch = __webpack_require__(817); +var ArraySet = __webpack_require__(814).ArraySet; +var base64VLQ = __webpack_require__(811); +var quickSort = __webpack_require__(818).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -96366,7 +96487,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 815 */ +/* 817 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -96483,7 +96604,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 816 */ +/* 818 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -96603,7 +96724,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 817 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -96613,8 +96734,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -var util = __webpack_require__(811); +var SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +var util = __webpack_require__(813); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -97022,17 +97143,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 818 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(819) -var resolveUrl = __webpack_require__(820) -var decodeUriComponent = __webpack_require__(821) -var urix = __webpack_require__(823) -var atob = __webpack_require__(824) +var sourceMappingURL = __webpack_require__(821) +var resolveUrl = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(823) +var urix = __webpack_require__(825) +var atob = __webpack_require__(826) @@ -97330,7 +97451,7 @@ module.exports = { /***/ }), -/* 819 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -97393,7 +97514,7 @@ void (function(root, factory) { /***/ }), -/* 820 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -97411,13 +97532,13 @@ module.exports = resolveUrl /***/ }), -/* 821 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(824) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -97428,7 +97549,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 822 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97529,7 +97650,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 823 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -97552,7 +97673,7 @@ module.exports = urix /***/ }), -/* 824 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97566,7 +97687,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 825 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97574,8 +97695,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(729); -var utils = __webpack_require__(806); +var define = __webpack_require__(731); +var utils = __webpack_require__(808); /** * Expose `mixin()`. @@ -97718,19 +97839,19 @@ exports.comment = function(node) { /***/ }), -/* 826 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); +var use = __webpack_require__(800); var util = __webpack_require__(29); -var Cache = __webpack_require__(827); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:parser'); -var Position = __webpack_require__(828); -var utils = __webpack_require__(806); +var Cache = __webpack_require__(829); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:parser'); +var Position = __webpack_require__(830); +var utils = __webpack_require__(808); /** * Create a new `Parser` with the given `input` and `options`. @@ -98258,7 +98379,7 @@ module.exports = Parser; /***/ }), -/* 827 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98365,13 +98486,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 828 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); +var define = __webpack_require__(731); /** * Store position for a node @@ -98386,16 +98507,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 829 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(830); -var define = __webpack_require__(836); -var extend = __webpack_require__(837); -var not = __webpack_require__(839); +var safe = __webpack_require__(832); +var define = __webpack_require__(838); +var extend = __webpack_require__(839); +var not = __webpack_require__(841); var MAX_LENGTH = 1024 * 64; /** @@ -98548,10 +98669,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 830 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(831); +var parse = __webpack_require__(833); var types = parse.types; module.exports = function (re, opts) { @@ -98597,13 +98718,13 @@ function isRegExp (x) { /***/ }), -/* 831 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(832); -var types = __webpack_require__(833); -var sets = __webpack_require__(834); -var positions = __webpack_require__(835); +var util = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); +var positions = __webpack_require__(837); module.exports = function(regexpStr) { @@ -98885,11 +99006,11 @@ module.exports.types = types; /***/ }), -/* 832 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); -var sets = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); // All of these are private and only used by randexp. @@ -99002,7 +99123,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 833 */ +/* 835 */ /***/ (function(module, exports) { module.exports = { @@ -99018,10 +99139,10 @@ module.exports = { /***/ }), -/* 834 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(835); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -99106,10 +99227,10 @@ exports.anyChar = function() { /***/ }), -/* 835 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(835); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -99129,7 +99250,7 @@ exports.end = function() { /***/ }), -/* 836 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99142,8 +99263,8 @@ exports.end = function() { -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -99174,14 +99295,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 837 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(838); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(840); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99241,7 +99362,7 @@ function isEnum(obj, key) { /***/ }), -/* 838 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99254,7 +99375,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99262,14 +99383,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 839 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(837); -var safe = __webpack_require__(830); +var extend = __webpack_require__(839); +var safe = __webpack_require__(832); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -99341,14 +99462,14 @@ module.exports = toRegex; /***/ }), -/* 840 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(841); -var extglob = __webpack_require__(856); +var nanomatch = __webpack_require__(843); +var extglob = __webpack_require__(858); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -99425,7 +99546,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 841 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99436,17 +99557,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(728); -var extend = __webpack_require__(842); +var toRegex = __webpack_require__(730); +var extend = __webpack_require__(844); /** * Local dependencies */ -var compilers = __webpack_require__(844); -var parsers = __webpack_require__(845); -var cache = __webpack_require__(848); -var utils = __webpack_require__(850); +var compilers = __webpack_require__(846); +var parsers = __webpack_require__(847); +var cache = __webpack_require__(850); +var utils = __webpack_require__(852); var MAX_LENGTH = 1024 * 64; /** @@ -100270,14 +100391,14 @@ module.exports = nanomatch; /***/ }), -/* 842 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(843); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(845); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -100337,7 +100458,7 @@ function isEnum(obj, key) { /***/ }), -/* 843 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100350,7 +100471,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -100358,7 +100479,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 844 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100704,15 +100825,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 845 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(728); -var isOdd = __webpack_require__(846); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(730); +var isOdd = __webpack_require__(848); /** * Characters to use in negation regex (we want to "not" match @@ -101098,7 +101219,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 846 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101111,7 +101232,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(847); +var isNumber = __webpack_require__(849); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -101125,7 +101246,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 847 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101153,14 +101274,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 848 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 849 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101173,7 +101294,7 @@ module.exports = new (__webpack_require__(849))(); -var MapCache = __webpack_require__(827); +var MapCache = __webpack_require__(829); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -101295,7 +101416,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 850 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101308,14 +101429,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(851)(); -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(852); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(842); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(855); -utils.unique = __webpack_require__(740); +var isWindows = __webpack_require__(853)(); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(854); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(844); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(857); +utils.unique = __webpack_require__(742); /** * Returns true if the given value is effectively an empty string @@ -101681,7 +101802,7 @@ utils.unixify = function(options) { /***/ }), -/* 851 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -101709,7 +101830,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 852 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101722,8 +101843,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -101754,7 +101875,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 853 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101808,7 +101929,7 @@ function diffArray(one, two) { /***/ }), -/* 854 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101821,7 +101942,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -101850,7 +101971,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 855 */ +/* 857 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -101985,7 +102106,7 @@ function isBuffer(val) { /***/ }), -/* 856 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101995,18 +102116,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(737); -var unique = __webpack_require__(740); -var toRegex = __webpack_require__(728); +var extend = __webpack_require__(739); +var unique = __webpack_require__(742); +var toRegex = __webpack_require__(730); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); -var Extglob = __webpack_require__(871); -var utils = __webpack_require__(870); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); +var Extglob = __webpack_require__(873); +var utils = __webpack_require__(872); var MAX_LENGTH = 1024 * 64; /** @@ -102323,13 +102444,13 @@ module.exports = extglob; /***/ }), -/* 857 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); +var brackets = __webpack_require__(860); /** * Extglob compilers @@ -102499,7 +102620,7 @@ module.exports = function(extglob) { /***/ }), -/* 858 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102509,17 +102630,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(859); -var parsers = __webpack_require__(861); +var compilers = __webpack_require__(861); +var parsers = __webpack_require__(863); /** * Module dependencies */ -var debug = __webpack_require__(863)('expand-brackets'); -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var toRegex = __webpack_require__(728); +var debug = __webpack_require__(865)('expand-brackets'); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var toRegex = __webpack_require__(730); /** * Parses the given POSIX character class `pattern` and returns a @@ -102717,13 +102838,13 @@ module.exports = brackets; /***/ }), -/* 859 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(860); +var posix = __webpack_require__(862); module.exports = function(brackets) { brackets.compiler @@ -102811,7 +102932,7 @@ module.exports = function(brackets) { /***/ }), -/* 860 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102840,14 +102961,14 @@ module.exports = { /***/ }), -/* 861 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(862); -var define = __webpack_require__(729); +var utils = __webpack_require__(864); +var define = __webpack_require__(731); /** * Text regex @@ -103066,14 +103187,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 862 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(728); -var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(730); +var regexNot = __webpack_require__(741); var cached; /** @@ -103107,7 +103228,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 863 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -103116,14 +103237,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(864); + module.exports = __webpack_require__(866); } else { - module.exports = __webpack_require__(867); + module.exports = __webpack_require__(869); } /***/ }), -/* 864 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -103132,7 +103253,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(867); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -103314,7 +103435,7 @@ function localstorage() { /***/ }), -/* 865 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { @@ -103330,7 +103451,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(866); +exports.humanize = __webpack_require__(868); /** * The currently active debug mode names, and names to skip. @@ -103522,7 +103643,7 @@ function coerce(val) { /***/ }), -/* 866 */ +/* 868 */ /***/ (function(module, exports) { /** @@ -103680,7 +103801,7 @@ function plural(ms, n, name) { /***/ }), -/* 867 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -103696,7 +103817,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(867); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -103875,7 +103996,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -103934,15 +104055,15 @@ exports.enable(load()); /***/ }), -/* 868 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); -var define = __webpack_require__(869); -var utils = __webpack_require__(870); +var brackets = __webpack_require__(860); +var define = __webpack_require__(871); +var utils = __webpack_require__(872); /** * Characters to use in text regex (we want to "not" match @@ -104097,7 +104218,7 @@ module.exports = parsers; /***/ }), -/* 869 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104110,7 +104231,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -104135,14 +104256,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 870 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(739); -var Cache = __webpack_require__(849); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(851); /** * Utils @@ -104211,7 +104332,7 @@ utils.createRegex = function(str) { /***/ }), -/* 871 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104221,16 +104342,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(767); -var define = __webpack_require__(869); -var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(769); +var define = __webpack_require__(871); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); /** * Customize Snapdragon parser and renderer @@ -104296,16 +104417,16 @@ module.exports = Extglob; /***/ }), -/* 872 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(856); -var nanomatch = __webpack_require__(841); -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(829); +var extglob = __webpack_require__(858); +var nanomatch = __webpack_require__(843); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(831); var not; /** @@ -104386,14 +104507,14 @@ function textRegex(pattern) { /***/ }), -/* 873 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 874 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104406,13 +104527,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(836); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(837); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(875); -utils.unique = __webpack_require__(740); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(838); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(839); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(877); +utils.unique = __webpack_require__(742); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -104709,7 +104830,7 @@ utils.unixify = function(options) { /***/ }), -/* 875 */ +/* 877 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -104844,7 +104965,7 @@ function isBuffer(val) { /***/ }), -/* 876 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104863,9 +104984,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -104926,15 +105047,15 @@ exports.default = ReaderAsync; /***/ }), -/* 877 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(878); -const readdirAsync = __webpack_require__(886); -const readdirStream = __webpack_require__(889); +const readdirSync = __webpack_require__(880); +const readdirAsync = __webpack_require__(888); +const readdirStream = __webpack_require__(891); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -105018,7 +105139,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 878 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105026,11 +105147,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(881); let syncFacade = { - fs: __webpack_require__(884), - forEach: __webpack_require__(885), + fs: __webpack_require__(886), + forEach: __webpack_require__(887), sync: true }; @@ -105059,7 +105180,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 879 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105068,9 +105189,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(880); -const stat = __webpack_require__(882); -const call = __webpack_require__(883); +const normalizeOptions = __webpack_require__(882); +const stat = __webpack_require__(884); +const call = __webpack_require__(885); /** * Asynchronously reads the contents of a directory and streams the results @@ -105446,14 +105567,14 @@ module.exports = DirectoryReader; /***/ }), -/* 880 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(881); +const globToRegExp = __webpack_require__(883); module.exports = normalizeOptions; @@ -105630,7 +105751,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 881 */ +/* 883 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -105767,13 +105888,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 882 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(883); +const call = __webpack_require__(885); module.exports = stat; @@ -105848,7 +105969,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 883 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105909,14 +106030,14 @@ function callOnce (fn) { /***/ }), -/* 884 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(883); +const call = __webpack_require__(885); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -105980,7 +106101,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 885 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106009,7 +106130,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 886 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106017,12 +106138,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(887); -const DirectoryReader = __webpack_require__(879); +const maybe = __webpack_require__(889); +const DirectoryReader = __webpack_require__(881); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(890), async: true }; @@ -106064,7 +106185,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 887 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106091,7 +106212,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 888 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106127,7 +106248,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 889 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106135,11 +106256,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(881); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(890), async: true }; @@ -106159,16 +106280,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 890 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(891); -var entry_1 = __webpack_require__(893); -var pathUtil = __webpack_require__(892); +var deep_1 = __webpack_require__(893); +var entry_1 = __webpack_require__(895); +var pathUtil = __webpack_require__(894); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -106234,14 +106355,14 @@ exports.default = Reader; /***/ }), -/* 891 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); -var patternUtils = __webpack_require__(721); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -106324,7 +106445,7 @@ exports.default = DeepFilter; /***/ }), -/* 892 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106355,14 +106476,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 893 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); -var patternUtils = __webpack_require__(721); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -106447,7 +106568,7 @@ exports.default = EntryFilter; /***/ }), -/* 894 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106467,8 +106588,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -106518,14 +106639,14 @@ exports.default = FileSystemStream; /***/ }), -/* 895 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(896); -const statProvider = __webpack_require__(898); +const optionsManager = __webpack_require__(898); +const statProvider = __webpack_require__(900); /** * Asynchronous API. */ @@ -106556,13 +106677,13 @@ exports.statSync = statSync; /***/ }), -/* 896 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(897); +const fsAdapter = __webpack_require__(899); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -106575,7 +106696,7 @@ exports.prepare = prepare; /***/ }), -/* 897 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106598,7 +106719,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 898 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106650,7 +106771,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 899 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106681,7 +106802,7 @@ exports.default = FileSystem; /***/ }), -/* 900 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106701,9 +106822,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -106771,7 +106892,7 @@ exports.default = ReaderStream; /***/ }), -/* 901 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106790,9 +106911,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_sync_1 = __webpack_require__(902); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_sync_1 = __webpack_require__(904); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -106852,7 +106973,7 @@ exports.default = ReaderSync; /***/ }), -/* 902 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106871,8 +106992,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -106918,7 +107039,7 @@ exports.default = FileSystemSync; /***/ }), -/* 903 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106934,7 +107055,7 @@ exports.flatten = flatten; /***/ }), -/* 904 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106955,13 +107076,13 @@ exports.merge = merge; /***/ }), -/* 905 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(906); +const pathType = __webpack_require__(908); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -107027,13 +107148,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 906 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(907); +const pify = __webpack_require__(909); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -107076,7 +107197,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 907 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107167,17 +107288,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 908 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(717); -const gitIgnore = __webpack_require__(909); -const pify = __webpack_require__(910); -const slash = __webpack_require__(911); +const fastGlob = __webpack_require__(719); +const gitIgnore = __webpack_require__(911); +const pify = __webpack_require__(912); +const slash = __webpack_require__(913); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -107275,7 +107396,7 @@ module.exports.sync = options => { /***/ }), -/* 909 */ +/* 911 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -107744,7 +107865,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 910 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107819,7 +107940,7 @@ module.exports = (input, options) => { /***/ }), -/* 911 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107837,67 +107958,74 @@ module.exports = input => { /***/ }), -/* 912 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const {Buffer} = __webpack_require__(913); -const CpFileError = __webpack_require__(914); -const fs = __webpack_require__(918); -const ProgressEmitter = __webpack_require__(920); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); +const fs = __webpack_require__(922); +const ProgressEmitter = __webpack_require__(925); + +const cpFileAsync = async (source, destination, options, progressEmitter) => { + let readError; + const stat = await fs.stat(source); + progressEmitter.size = stat.size; + + const read = await fs.createReadStream(source); + await fs.makeDir(path.dirname(destination)); + const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'}); + read.on('data', () => { + progressEmitter.written = write.bytesWritten; + }); + read.once('error', error => { + readError = new CpFileError(`Cannot read from \`${source}\`: ${error.message}`, error); + write.end(); + }); -const cpFile = (source, destination, options) => { - if (!source || !destination) { - return Promise.reject(new CpFileError('`source` and `destination` required')); + let updateStats = false; + try { + const writePromise = pEvent(write, 'close'); + read.pipe(write); + await writePromise; + progressEmitter.written = progressEmitter.size; + updateStats = true; + } catch (error) { + if (options.overwrite || error.code !== 'EEXIST') { + throw new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error); + } } - options = Object.assign({overwrite: true}, options); - - const progressEmitter = new ProgressEmitter(path.resolve(source), path.resolve(destination)); - - const promise = fs - .stat(source) - .then(stat => { - progressEmitter.size = stat.size; - }) - .then(() => fs.createReadStream(source)) - .then(read => fs.makeDir(path.dirname(destination)).then(() => read)) - .then(read => new Promise((resolve, reject) => { - const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'}); - - read.on('data', () => { - progressEmitter.written = write.bytesWritten; - }); + if (readError) { + throw readError; + } - write.on('error', error => { - if (!options.overwrite && error.code === 'EEXIST') { - resolve(false); - return; - } + if (updateStats) { + const stats = await fs.lstat(source); - reject(new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error)); - }); + return Promise.all([ + fs.utimes(destination, stats.atime, stats.mtime), + fs.chmod(destination, stats.mode), + fs.chown(destination, stats.uid, stats.gid) + ]); + } +}; - write.on('close', () => { - progressEmitter.written = progressEmitter.size; - resolve(true); - }); +const cpFile = (source, destination, options) => { + if (!source || !destination) { + return Promise.reject(new CpFileError('`source` and `destination` required')); + } - read.pipe(write); - })) - .then(updateStats => { - if (updateStats) { - return fs.lstat(source).then(stats => Promise.all([ - fs.utimes(destination, stats.atime, stats.mtime), - fs.chmod(destination, stats.mode), - fs.chown(destination, stats.uid, stats.gid) - ])); - } - }); + options = { + overwrite: true, + ...options + }; + const progressEmitter = new ProgressEmitter(path.resolve(source), path.resolve(destination)); + const promise = cpFileAsync(source, destination, options, progressEmitter); promise.on = (...args) => { progressEmitter.on(...args); return promise; @@ -107907,8 +108035,6 @@ const cpFile = (source, destination, options) => { }; module.exports = cpFile; -// TODO: Remove this for the next major release -module.exports.default = cpFile; const checkSourceIsFile = (stat, source) => { if (stat.isDirectory()) { @@ -107925,7 +108051,16 @@ const fixupAttributes = (destination, stat) => { fs.chownSync(destination, stat.uid, stat.gid); }; -const copySyncNative = (source, destination, options) => { +module.exports.sync = (source, destination, options) => { + if (!source || !destination) { + throw new CpFileError('`source` and `destination` required'); + } + + options = { + overwrite: true, + ...options + }; + const stat = fs.statSync(source); checkSourceIsFile(stat, source); fs.makeDirSync(path.dirname(destination)); @@ -107945,136 +108080,383 @@ const copySyncNative = (source, destination, options) => { fixupAttributes(destination, stat); }; -const copySyncFallback = (source, destination, options) => { - let bytesRead; - let position; - let read; // eslint-disable-line prefer-const - let write; - const BUF_LENGTH = 100 * 1024; - const buffer = Buffer.alloc(BUF_LENGTH); - const readSync = position => fs.readSync(read, buffer, 0, BUF_LENGTH, position, source); - const writeSync = () => fs.writeSync(write, buffer, 0, bytesRead, undefined, destination); - read = fs.openSync(source, 'r'); - bytesRead = readSync(0); - position = bytesRead; - fs.makeDirSync(path.dirname(destination)); +/***/ }), +/* 915 */ +/***/ (function(module, exports, __webpack_require__) { - try { - write = fs.openSync(destination, options.overwrite ? 'w' : 'wx'); - } catch (error) { - if (!options.overwrite && error.code === 'EEXIST') { - return; +"use strict"; + +const pTimeout = __webpack_require__(916); + +const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; + +const normalizeEmitter = emitter => { + const addListener = emitter.on || emitter.addListener || emitter.addEventListener; + const removeListener = emitter.off || emitter.removeListener || emitter.removeEventListener; + + if (!addListener || !removeListener) { + throw new TypeError('Emitter is not compatible'); + } + + return { + addListener: addListener.bind(emitter), + removeListener: removeListener.bind(emitter) + }; +}; + +const normalizeEvents = event => Array.isArray(event) ? event : [event]; + +const multiple = (emitter, event, options) => { + let cancel; + const ret = new Promise((resolve, reject) => { + options = { + rejectionEvents: ['error'], + multiArgs: false, + resolveImmediately: false, + ...options + }; + + if (!(options.count >= 0 && (options.count === Infinity || Number.isInteger(options.count)))) { + throw new TypeError('The `count` option should be at least 0 or more'); } - throw error; + // Allow multiple events + const events = normalizeEvents(event); + + const items = []; + const {addListener, removeListener} = normalizeEmitter(emitter); + + const onItem = (...args) => { + const value = options.multiArgs ? args : args[0]; + + if (options.filter && !options.filter(value)) { + return; + } + + items.push(value); + + if (options.count === items.length) { + cancel(); + resolve(items); + } + }; + + const rejectHandler = error => { + cancel(); + reject(error); + }; + + cancel = () => { + for (const event of events) { + removeListener(event, onItem); + } + + for (const rejectionEvent of options.rejectionEvents) { + removeListener(rejectionEvent, rejectHandler); + } + }; + + for (const event of events) { + addListener(event, onItem); + } + + for (const rejectionEvent of options.rejectionEvents) { + addListener(rejectionEvent, rejectHandler); + } + + if (options.resolveImmediately) { + resolve(items); + } + }); + + ret.cancel = cancel; + + if (typeof options.timeout === 'number') { + const timeout = pTimeout(ret, options.timeout); + timeout.cancel = cancel; + return timeout; } - writeSync(); + return ret; +}; - while (bytesRead === BUF_LENGTH) { - bytesRead = readSync(position); - writeSync(); - position += bytesRead; +const pEvent = (emitter, event, options) => { + if (typeof options === 'function') { + options = {filter: options}; } - const stat = fs.fstatSync(read, source); - fs.futimesSync(write, stat.atime, stat.mtime, destination); - fs.closeSync(read); - fs.closeSync(write); - fixupAttributes(destination, stat); + options = { + ...options, + count: 1, + resolveImmediately: false + }; + + const arrayPromise = multiple(emitter, event, options); + const promise = arrayPromise.then(array => array[0]); // eslint-disable-line promise/prefer-await-to-then + promise.cancel = arrayPromise.cancel; + + return promise; }; -module.exports.sync = (source, destination, options) => { - if (!source || !destination) { - throw new CpFileError('`source` and `destination` required'); +module.exports = pEvent; +// TODO: Remove this for the next major release +module.exports.default = pEvent; + +module.exports.multiple = multiple; + +module.exports.iterator = (emitter, event, options) => { + if (typeof options === 'function') { + options = {filter: options}; } - options = Object.assign({overwrite: true}, options); + // Allow multiple events + const events = normalizeEvents(event); - if (fs.copyFileSync) { - copySyncNative(source, destination, options); - } else { - copySyncFallback(source, destination, options); + options = { + rejectionEvents: ['error'], + resolutionEvents: [], + limit: Infinity, + multiArgs: false, + ...options + }; + + const {limit} = options; + const isValidLimit = limit >= 0 && (limit === Infinity || Number.isInteger(limit)); + if (!isValidLimit) { + throw new TypeError('The `limit` option should be a non-negative integer or Infinity'); + } + + if (limit === 0) { + // Return an empty async iterator to avoid any further cost + return { + [Symbol.asyncIterator]() { + return this; + }, + async next() { + return { + done: true, + value: undefined + }; + } + }; + } + + const {addListener, removeListener} = normalizeEmitter(emitter); + + let isDone = false; + let error; + let hasPendingError = false; + const nextQueue = []; + const valueQueue = []; + let eventCount = 0; + let isLimitReached = false; + + const valueHandler = (...args) => { + eventCount++; + isLimitReached = eventCount === limit; + + const value = options.multiArgs ? args : args[0]; + + if (nextQueue.length > 0) { + const {resolve} = nextQueue.shift(); + + resolve({done: false, value}); + + if (isLimitReached) { + cancel(); + } + + return; + } + + valueQueue.push(value); + + if (isLimitReached) { + cancel(); + } + }; + + const cancel = () => { + isDone = true; + for (const event of events) { + removeListener(event, valueHandler); + } + + for (const rejectionEvent of options.rejectionEvents) { + removeListener(rejectionEvent, rejectHandler); + } + + for (const resolutionEvent of options.resolutionEvents) { + removeListener(resolutionEvent, resolveHandler); + } + + while (nextQueue.length > 0) { + const {resolve} = nextQueue.shift(); + resolve({done: true, value: undefined}); + } + }; + + const rejectHandler = (...args) => { + error = options.multiArgs ? args : args[0]; + + if (nextQueue.length > 0) { + const {reject} = nextQueue.shift(); + reject(error); + } else { + hasPendingError = true; + } + + cancel(); + }; + + const resolveHandler = (...args) => { + const value = options.multiArgs ? args : args[0]; + + if (options.filter && !options.filter(value)) { + return; + } + + if (nextQueue.length > 0) { + const {resolve} = nextQueue.shift(); + resolve({done: true, value}); + } else { + valueQueue.push(value); + } + + cancel(); + }; + + for (const event of events) { + addListener(event, valueHandler); + } + + for (const rejectionEvent of options.rejectionEvents) { + addListener(rejectionEvent, rejectHandler); + } + + for (const resolutionEvent of options.resolutionEvents) { + addListener(resolutionEvent, resolveHandler); } + + return { + [symbolAsyncIterator]() { + return this; + }, + async next() { + if (valueQueue.length > 0) { + const value = valueQueue.shift(); + return { + done: isDone && valueQueue.length === 0 && !isLimitReached, + value + }; + } + + if (hasPendingError) { + hasPendingError = false; + throw error; + } + + if (isDone) { + return { + done: true, + value: undefined + }; + } + + return new Promise((resolve, reject) => nextQueue.push({resolve, reject})); + }, + async return(value) { + cancel(); + return { + done: isDone, + value + }; + } + }; }; /***/ }), -/* 913 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { -/* eslint-disable node/no-deprecated-api */ -var buffer = __webpack_require__(585) -var Buffer = buffer.Buffer +"use strict"; -// alternative to using Object.keys for old browsers -function copyProps (src, dst) { - for (var key in src) { - dst[key] = src[key] - } -} -if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { - module.exports = buffer -} else { - // Copy properties from require('buffer') - copyProps(buffer, exports) - exports.Buffer = SafeBuffer -} +const pFinally = __webpack_require__(917); -function SafeBuffer (arg, encodingOrOffset, length) { - return Buffer(arg, encodingOrOffset, length) +class TimeoutError extends Error { + constructor(message) { + super(message); + this.name = 'TimeoutError'; + } } -// Copy static methods from Buffer -copyProps(Buffer, SafeBuffer) +module.exports = (promise, ms, fallback) => new Promise((resolve, reject) => { + if (typeof ms !== 'number' || ms < 0) { + throw new TypeError('Expected `ms` to be a positive number'); + } -SafeBuffer.from = function (arg, encodingOrOffset, length) { - if (typeof arg === 'number') { - throw new TypeError('Argument must not be a number') - } - return Buffer(arg, encodingOrOffset, length) -} + const timer = setTimeout(() => { + if (typeof fallback === 'function') { + try { + resolve(fallback()); + } catch (err) { + reject(err); + } + return; + } -SafeBuffer.alloc = function (size, fill, encoding) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - var buf = Buffer(size) - if (fill !== undefined) { - if (typeof encoding === 'string') { - buf.fill(fill, encoding) - } else { - buf.fill(fill) - } - } else { - buf.fill(0) - } - return buf -} + const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${ms} milliseconds`; + const err = fallback instanceof Error ? fallback : new TimeoutError(message); -SafeBuffer.allocUnsafe = function (size) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - return Buffer(size) -} + if (typeof promise.cancel === 'function') { + promise.cancel(); + } -SafeBuffer.allocUnsafeSlow = function (size) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - return buffer.SlowBuffer(size) -} + reject(err); + }, ms); + + pFinally( + promise.then(resolve, reject), + () => { + clearTimeout(timer); + } + ); +}); + +module.exports.TimeoutError = TimeoutError; /***/ }), -/* 914 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(915); +module.exports = (promise, onFinally) => { + onFinally = onFinally || (() => {}); + + return promise.then( + val => new Promise(resolve => { + resolve(onFinally()); + }).then(() => val), + err => new Promise(resolve => { + resolve(onFinally()); + }).then(() => { + throw err; + }) + ); +}; + + +/***/ }), +/* 918 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const NestedError = __webpack_require__(919); class CpFileError extends NestedError { constructor(message, nested) { @@ -108088,10 +108470,10 @@ module.exports = CpFileError; /***/ }), -/* 915 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(916); +var inherits = __webpack_require__(920); var NestedError = function (message, nested) { this.nested = nested; @@ -108142,7 +108524,7 @@ module.exports = NestedError; /***/ }), -/* 916 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -108150,12 +108532,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(917); + module.exports = __webpack_require__(921); } /***/ }), -/* 917 */ +/* 921 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -108184,87 +108566,58 @@ if (typeof Object.create === 'function') { /***/ }), -/* 918 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(559); -const pify = __webpack_require__(919); -const CpFileError = __webpack_require__(914); +const makeDir = __webpack_require__(923); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); -const fsP = pify(fs); +const stat = promisify(fs.stat); +const lstat = promisify(fs.lstat); +const utimes = promisify(fs.utimes); +const chmod = promisify(fs.chmod); +const chown = promisify(fs.chown); exports.closeSync = fs.closeSync.bind(fs); exports.createWriteStream = fs.createWriteStream.bind(fs); -exports.createReadStream = (path, options) => new Promise((resolve, reject) => { +exports.createReadStream = async (path, options) => { const read = fs.createReadStream(path, options); - read.once('error', error => { - reject(new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error)); - }); - - read.once('readable', () => { - resolve(read); - }); + try { + await pEvent(read, ['readable', 'end']); + } catch (error) { + throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error); + } - read.once('end', () => { - resolve(read); - }); -}); + return read; +}; -exports.stat = path => fsP.stat(path).catch(error => { +exports.stat = path => stat(path).catch(error => { throw new CpFileError(`Cannot stat path \`${path}\`: ${error.message}`, error); }); -exports.lstat = path => fsP.lstat(path).catch(error => { +exports.lstat = path => lstat(path).catch(error => { throw new CpFileError(`lstat \`${path}\` failed: ${error.message}`, error); }); -exports.utimes = (path, atime, mtime) => fsP.utimes(path, atime, mtime).catch(error => { +exports.utimes = (path, atime, mtime) => utimes(path, atime, mtime).catch(error => { throw new CpFileError(`utimes \`${path}\` failed: ${error.message}`, error); }); -exports.chmod = (path, mode) => fsP.chmod(path, mode).catch(error => { +exports.chmod = (path, mode) => chmod(path, mode).catch(error => { throw new CpFileError(`chmod \`${path}\` failed: ${error.message}`, error); }); -exports.chown = (path, uid, gid) => fsP.chown(path, uid, gid).catch(error => { +exports.chown = (path, uid, gid) => chown(path, uid, gid).catch(error => { throw new CpFileError(`chown \`${path}\` failed: ${error.message}`, error); }); -exports.openSync = (path, flags, mode) => { - try { - return fs.openSync(path, flags, mode); - } catch (error) { - if (flags.includes('w')) { - throw new CpFileError(`Cannot write to \`${path}\`: ${error.message}`, error); - } - - throw new CpFileError(`Cannot open \`${path}\`: ${error.message}`, error); - } -}; - -// eslint-disable-next-line max-params -exports.readSync = (fileDescriptor, buffer, offset, length, position, path) => { - try { - return fs.readSync(fileDescriptor, buffer, offset, length, position); - } catch (error) { - throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error); - } -}; - -// eslint-disable-next-line max-params -exports.writeSync = (fileDescriptor, buffer, offset, length, position, path) => { - try { - return fs.writeSync(fileDescriptor, buffer, offset, length, position); - } catch (error) { - throw new CpFileError(`Cannot write to \`${path}\`: ${error.message}`, error); - } -}; - exports.statSync = path => { try { return fs.statSync(path); @@ -108273,22 +108626,6 @@ exports.statSync = path => { } }; -exports.fstatSync = (fileDescriptor, path) => { - try { - return fs.fstatSync(fileDescriptor); - } catch (error) { - throw new CpFileError(`fstat \`${path}\` failed: ${error.message}`, error); - } -}; - -exports.futimesSync = (fileDescriptor, atime, mtime, path) => { - try { - return fs.futimesSync(fileDescriptor, atime, mtime, path); - } catch (error) { - throw new CpFileError(`futimes \`${path}\` failed: ${error.message}`, error); - } -}; - exports.utimesSync = (path, atime, mtime) => { try { return fs.utimesSync(path, atime, mtime); @@ -108325,210 +108662,1938 @@ exports.makeDirSync = path => { } }; -if (fs.copyFileSync) { - exports.copyFileSync = (source, destination, flags) => { - try { - fs.copyFileSync(source, destination, flags); - } catch (error) { - throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error); - } - }; -} +exports.copyFileSync = (source, destination, flags) => { + try { + fs.copyFileSync(source, destination, flags); + } catch (error) { + throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error); + } +}; /***/ }), -/* 919 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const fs = __webpack_require__(23); +const path = __webpack_require__(16); +const {promisify} = __webpack_require__(29); +const semver = __webpack_require__(924); -const processFn = (fn, options) => function (...args) { - const P = options.promiseModule; +const defaults = { + mode: 0o777 & (~process.umask()), + fs +}; - return new P((resolve, reject) => { - if (options.multiArgs) { - args.push((...result) => { - if (options.errorFirst) { - if (result[0]) { - reject(result); - } else { - result.shift(); - resolve(result); - } - } else { - resolve(result); - } - }); - } else if (options.errorFirst) { - args.push((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - } else { - args.push(resolve); +const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); + +// https://github.com/nodejs/node/issues/8987 +// https://github.com/libuv/libuv/pull/1088 +const checkPath = pth => { + if (process.platform === 'win32') { + const pathHasInvalidWinCharacters = /[<>:"|?*]/.test(pth.replace(path.parse(pth).root, '')); + + if (pathHasInvalidWinCharacters) { + const error = new Error(`Path contains invalid characters: ${pth}`); + error.code = 'EINVAL'; + throw error; } + } +}; - fn.apply(this, args); - }); +const permissionError = pth => { + // This replicates the exception of `fs.mkdir` with native the + // `recusive` option when run on an invalid drive under Windows. + const error = new Error(`operation not permitted, mkdir '${pth}'`); + error.code = 'EPERM'; + error.errno = -4048; + error.path = pth; + error.syscall = 'mkdir'; + return error; }; -module.exports = (input, options) => { - options = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, options); +const makeDir = async (input, options) => { + checkPath(input); + options = { + ...defaults, + ...options + }; - const objType = typeof input; - if (!(input !== null && (objType === 'object' || objType === 'function'))) { - throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``); + const mkdir = promisify(options.fs.mkdir); + const stat = promisify(options.fs.stat); + + if (useNativeRecursiveOption && options.fs.mkdir === fs.mkdir) { + const pth = path.resolve(input); + + await mkdir(pth, { + mode: options.mode, + recursive: true + }); + + return pth; } - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return options.include ? options.include.some(match) : !options.exclude.some(match); + const make = async pth => { + try { + await mkdir(pth, options.mode); + + return pth; + } catch (error) { + if (error.code === 'EPERM') { + throw error; + } + + if (error.code === 'ENOENT') { + if (path.dirname(pth) === pth) { + throw permissionError(pth); + } + + if (error.message.includes('null bytes')) { + throw error; + } + + await make(path.dirname(pth)); + + return make(pth); + } + + const stats = await stat(pth); + if (!stats.isDirectory()) { + throw error; + } + + return pth; + } }; - let ret; - if (objType === 'function') { - ret = function (...args) { - return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args); - }; - } else { - ret = Object.create(Object.getPrototypeOf(input)); - } + return make(path.resolve(input)); +}; - for (const key in input) { // eslint-disable-line guard-for-in - const property = input[key]; - ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property; +module.exports = makeDir; + +module.exports.sync = (input, options) => { + checkPath(input); + options = { + ...defaults, + ...options + }; + + if (useNativeRecursiveOption && options.fs.mkdirSync === fs.mkdirSync) { + const pth = path.resolve(input); + + fs.mkdirSync(pth, { + mode: options.mode, + recursive: true + }); + + return pth; } - return ret; + const make = pth => { + try { + options.fs.mkdirSync(pth, options.mode); + } catch (error) { + if (error.code === 'EPERM') { + throw error; + } + + if (error.code === 'ENOENT') { + if (path.dirname(pth) === pth) { + throw permissionError(pth); + } + + if (error.message.includes('null bytes')) { + throw error; + } + + make(path.dirname(pth)); + return make(pth); + } + + try { + if (!options.fs.statSync(pth).isDirectory()) { + throw new Error('The path is not a directory'); + } + } catch (_) { + throw error; + } + } + + return pth; + }; + + return make(path.resolve(input)); }; /***/ }), -/* 920 */ -/***/ (function(module, exports, __webpack_require__) { +/* 924 */ +/***/ (function(module, exports) { -"use strict"; +exports = module.exports = SemVer -const EventEmitter = __webpack_require__(379); +var debug +/* istanbul ignore next */ +if (typeof process === 'object' && + process.env && + process.env.NODE_DEBUG && + /\bsemver\b/i.test(process.env.NODE_DEBUG)) { + debug = function () { + var args = Array.prototype.slice.call(arguments, 0) + args.unshift('SEMVER') + console.log.apply(console, args) + } +} else { + debug = function () {} +} -const written = new WeakMap(); +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +exports.SEMVER_SPEC_VERSION = '2.0.0' -class ProgressEmitter extends EventEmitter { - constructor(source, destination) { - super(); - this._source = source; - this._destination = destination; - } +var MAX_LENGTH = 256 +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || + /* istanbul ignore next */ 9007199254740991 - set written(value) { - written.set(this, value); - this.emitProgress(); - } +// Max safe segment length for coercion. +var MAX_SAFE_COMPONENT_LENGTH = 16 - get written() { - return written.get(this); - } +// The actual regexps go on exports.re +var re = exports.re = [] +var src = exports.src = [] +var t = exports.tokens = {} +var R = 0 - emitProgress() { - const {size, written} = this; - this.emit('progress', { - src: this._source, - dest: this._destination, - size, - written, - percent: written === size ? 1 : written / size - }); - } +function tok (n) { + t[n] = R++ } -module.exports = ProgressEmitter; +// The following Regular Expressions can be used for tokenizing, +// validating, and parsing SemVer version strings. +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. -/***/ }), -/* 921 */ -/***/ (function(module, exports, __webpack_require__) { +tok('NUMERICIDENTIFIER') +src[t.NUMERICIDENTIFIER] = '0|[1-9]\\d*' +tok('NUMERICIDENTIFIERLOOSE') +src[t.NUMERICIDENTIFIERLOOSE] = '[0-9]+' -"use strict"; +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or +// more letters, digits, or hyphens. -const NestedError = __webpack_require__(922); +tok('NONNUMERICIDENTIFIER') +src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*' -class CpyError extends NestedError { - constructor(message, nested) { - super(message, nested); - Object.assign(this, nested); - this.name = 'CpyError'; - } -} +// ## Main Version +// Three dot-separated numeric identifiers. -module.exports = CpyError; +tok('MAINVERSION') +src[t.MAINVERSION] = '(' + src[t.NUMERICIDENTIFIER] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIER] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIER] + ')' +tok('MAINVERSIONLOOSE') +src[t.MAINVERSIONLOOSE] = '(' + src[t.NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIERLOOSE] + ')' -/***/ }), -/* 922 */ -/***/ (function(module, exports, __webpack_require__) { +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. -var inherits = __webpack_require__(29).inherits; +tok('PRERELEASEIDENTIFIER') +src[t.PRERELEASEIDENTIFIER] = '(?:' + src[t.NUMERICIDENTIFIER] + + '|' + src[t.NONNUMERICIDENTIFIER] + ')' -var NestedError = function (message, nested) { - this.nested = nested; +tok('PRERELEASEIDENTIFIERLOOSE') +src[t.PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[t.NUMERICIDENTIFIERLOOSE] + + '|' + src[t.NONNUMERICIDENTIFIER] + ')' - if (message instanceof Error) { - nested = message; - } else if (typeof message !== 'undefined') { - Object.defineProperty(this, 'message', { - value: message, - writable: true, - enumerable: false, - configurable: true - }); - } +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. - Error.captureStackTrace(this, this.constructor); - var oldStackDescriptor = Object.getOwnPropertyDescriptor(this, 'stack'); - var stackDescriptor = buildStackDescriptor(oldStackDescriptor, nested); - Object.defineProperty(this, 'stack', stackDescriptor); -}; +tok('PRERELEASE') +src[t.PRERELEASE] = '(?:-(' + src[t.PRERELEASEIDENTIFIER] + + '(?:\\.' + src[t.PRERELEASEIDENTIFIER] + ')*))' -function buildStackDescriptor(oldStackDescriptor, nested) { - if (oldStackDescriptor.get) { - return { - get: function () { - var stack = oldStackDescriptor.get.call(this); - return buildCombinedStacks(stack, this.nested); - } - }; - } else { - var stack = oldStackDescriptor.value; - return { - value: buildCombinedStacks(stack, nested) - }; - } +tok('PRERELEASELOOSE') +src[t.PRERELEASELOOSE] = '(?:-?(' + src[t.PRERELEASEIDENTIFIERLOOSE] + + '(?:\\.' + src[t.PRERELEASEIDENTIFIERLOOSE] + ')*))' + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +tok('BUILDIDENTIFIER') +src[t.BUILDIDENTIFIER] = '[0-9A-Za-z-]+' + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +tok('BUILD') +src[t.BUILD] = '(?:\\+(' + src[t.BUILDIDENTIFIER] + + '(?:\\.' + src[t.BUILDIDENTIFIER] + ')*))' + +// ## Full Version String +// A main version, followed optionally by a pre-release version and +// build metadata. + +// Note that the only major, minor, patch, and pre-release sections of +// the version string are capturing groups. The build metadata is not a +// capturing group, because it should not ever be used in version +// comparison. + +tok('FULL') +tok('FULLPLAIN') +src[t.FULLPLAIN] = 'v?' + src[t.MAINVERSION] + + src[t.PRERELEASE] + '?' + + src[t.BUILD] + '?' + +src[t.FULL] = '^' + src[t.FULLPLAIN] + '$' + +// like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty +// common in the npm registry. +tok('LOOSEPLAIN') +src[t.LOOSEPLAIN] = '[v=\\s]*' + src[t.MAINVERSIONLOOSE] + + src[t.PRERELEASELOOSE] + '?' + + src[t.BUILD] + '?' + +tok('LOOSE') +src[t.LOOSE] = '^' + src[t.LOOSEPLAIN] + '$' + +tok('GTLT') +src[t.GTLT] = '((?:<|>)?=?)' + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +tok('XRANGEIDENTIFIERLOOSE') +src[t.XRANGEIDENTIFIERLOOSE] = src[t.NUMERICIDENTIFIERLOOSE] + '|x|X|\\*' +tok('XRANGEIDENTIFIER') +src[t.XRANGEIDENTIFIER] = src[t.NUMERICIDENTIFIER] + '|x|X|\\*' + +tok('XRANGEPLAIN') +src[t.XRANGEPLAIN] = '[v=\\s]*(' + src[t.XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIER] + ')' + + '(?:' + src[t.PRERELEASE] + ')?' + + src[t.BUILD] + '?' + + ')?)?' + +tok('XRANGEPLAINLOOSE') +src[t.XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[t.XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIERLOOSE] + ')' + + '(?:' + src[t.PRERELEASELOOSE] + ')?' + + src[t.BUILD] + '?' + + ')?)?' + +tok('XRANGE') +src[t.XRANGE] = '^' + src[t.GTLT] + '\\s*' + src[t.XRANGEPLAIN] + '$' +tok('XRANGELOOSE') +src[t.XRANGELOOSE] = '^' + src[t.GTLT] + '\\s*' + src[t.XRANGEPLAINLOOSE] + '$' + +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +tok('COERCE') +src[t.COERCE] = '(^|[^\\d])' + + '(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:$|[^\\d])' +tok('COERCERTL') +re[t.COERCERTL] = new RegExp(src[t.COERCE], 'g') + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +tok('LONETILDE') +src[t.LONETILDE] = '(?:~>?)' + +tok('TILDETRIM') +src[t.TILDETRIM] = '(\\s*)' + src[t.LONETILDE] + '\\s+' +re[t.TILDETRIM] = new RegExp(src[t.TILDETRIM], 'g') +var tildeTrimReplace = '$1~' + +tok('TILDE') +src[t.TILDE] = '^' + src[t.LONETILDE] + src[t.XRANGEPLAIN] + '$' +tok('TILDELOOSE') +src[t.TILDELOOSE] = '^' + src[t.LONETILDE] + src[t.XRANGEPLAINLOOSE] + '$' + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +tok('LONECARET') +src[t.LONECARET] = '(?:\\^)' + +tok('CARETTRIM') +src[t.CARETTRIM] = '(\\s*)' + src[t.LONECARET] + '\\s+' +re[t.CARETTRIM] = new RegExp(src[t.CARETTRIM], 'g') +var caretTrimReplace = '$1^' + +tok('CARET') +src[t.CARET] = '^' + src[t.LONECARET] + src[t.XRANGEPLAIN] + '$' +tok('CARETLOOSE') +src[t.CARETLOOSE] = '^' + src[t.LONECARET] + src[t.XRANGEPLAINLOOSE] + '$' + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +tok('COMPARATORLOOSE') +src[t.COMPARATORLOOSE] = '^' + src[t.GTLT] + '\\s*(' + src[t.LOOSEPLAIN] + ')$|^$' +tok('COMPARATOR') +src[t.COMPARATOR] = '^' + src[t.GTLT] + '\\s*(' + src[t.FULLPLAIN] + ')$|^$' + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +tok('COMPARATORTRIM') +src[t.COMPARATORTRIM] = '(\\s*)' + src[t.GTLT] + + '\\s*(' + src[t.LOOSEPLAIN] + '|' + src[t.XRANGEPLAIN] + ')' + +// this one has to use the /g flag +re[t.COMPARATORTRIM] = new RegExp(src[t.COMPARATORTRIM], 'g') +var comparatorTrimReplace = '$1$2$3' + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be +// checked against either the strict or loose comparator form +// later. +tok('HYPHENRANGE') +src[t.HYPHENRANGE] = '^\\s*(' + src[t.XRANGEPLAIN] + ')' + + '\\s+-\\s+' + + '(' + src[t.XRANGEPLAIN] + ')' + + '\\s*$' + +tok('HYPHENRANGELOOSE') +src[t.HYPHENRANGELOOSE] = '^\\s*(' + src[t.XRANGEPLAINLOOSE] + ')' + + '\\s+-\\s+' + + '(' + src[t.XRANGEPLAINLOOSE] + ')' + + '\\s*$' + +// Star ranges basically just allow anything at all. +tok('STAR') +src[t.STAR] = '(<|>)?=?\\s*\\*' + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (var i = 0; i < R; i++) { + debug(i, src[i]) + if (!re[i]) { + re[i] = new RegExp(src[i]) + } } -function buildCombinedStacks(stack, nested) { - if (nested) { - stack += '\nCaused By: ' + nested.stack; +exports.parse = parse +function parse (version, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false } - return stack; + } + + if (version instanceof SemVer) { + return version + } + + if (typeof version !== 'string') { + return null + } + + if (version.length > MAX_LENGTH) { + return null + } + + var r = options.loose ? re[t.LOOSE] : re[t.FULL] + if (!r.test(version)) { + return null + } + + try { + return new SemVer(version, options) + } catch (er) { + return null + } } -inherits(NestedError, Error); -NestedError.prototype.name = 'NestedError'; +exports.valid = valid +function valid (version, options) { + var v = parse(version, options) + return v ? v.version : null +} + +exports.clean = clean +function clean (version, options) { + var s = parse(version.trim().replace(/^[=v]+/, ''), options) + return s ? s.version : null +} +exports.SemVer = SemVer -module.exports = NestedError; +function SemVer (version, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + if (version instanceof SemVer) { + if (version.loose === options.loose) { + return version + } else { + version = version.version + } + } else if (typeof version !== 'string') { + throw new TypeError('Invalid Version: ' + version) + } + + if (version.length > MAX_LENGTH) { + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + } + if (!(this instanceof SemVer)) { + return new SemVer(version, options) + } -/***/ }), -/* 923 */ + debug('SemVer', version, options) + this.options = options + this.loose = !!options.loose + + var m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL]) + + if (!m) { + throw new TypeError('Invalid Version: ' + version) + } + + this.raw = version + + // these are actually numbers + this.major = +m[1] + this.minor = +m[2] + this.patch = +m[3] + + if (this.major > MAX_SAFE_INTEGER || this.major < 0) { + throw new TypeError('Invalid major version') + } + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) { + throw new TypeError('Invalid minor version') + } + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) { + throw new TypeError('Invalid patch version') + } + + // numberify any prerelease numeric ids + if (!m[4]) { + this.prerelease = [] + } else { + this.prerelease = m[4].split('.').map(function (id) { + if (/^[0-9]+$/.test(id)) { + var num = +id + if (num >= 0 && num < MAX_SAFE_INTEGER) { + return num + } + } + return id + }) + } + + this.build = m[5] ? m[5].split('.') : [] + this.format() +} + +SemVer.prototype.format = function () { + this.version = this.major + '.' + this.minor + '.' + this.patch + if (this.prerelease.length) { + this.version += '-' + this.prerelease.join('.') + } + return this.version +} + +SemVer.prototype.toString = function () { + return this.version +} + +SemVer.prototype.compare = function (other) { + debug('SemVer.compare', this.version, this.options, other) + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + return this.compareMain(other) || this.comparePre(other) +} + +SemVer.prototype.compareMain = function (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + return compareIdentifiers(this.major, other.major) || + compareIdentifiers(this.minor, other.minor) || + compareIdentifiers(this.patch, other.patch) +} + +SemVer.prototype.comparePre = function (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) { + return -1 + } else if (!this.prerelease.length && other.prerelease.length) { + return 1 + } else if (!this.prerelease.length && !other.prerelease.length) { + return 0 + } + + var i = 0 + do { + var a = this.prerelease[i] + var b = other.prerelease[i] + debug('prerelease compare', i, a, b) + if (a === undefined && b === undefined) { + return 0 + } else if (b === undefined) { + return 1 + } else if (a === undefined) { + return -1 + } else if (a === b) { + continue + } else { + return compareIdentifiers(a, b) + } + } while (++i) +} + +SemVer.prototype.compareBuild = function (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + var i = 0 + do { + var a = this.build[i] + var b = other.build[i] + debug('prerelease compare', i, a, b) + if (a === undefined && b === undefined) { + return 0 + } else if (b === undefined) { + return 1 + } else if (a === undefined) { + return -1 + } else if (a === b) { + continue + } else { + return compareIdentifiers(a, b) + } + } while (++i) +} + +// preminor will bump the version up to the next minor release, and immediately +// down to pre-release. premajor and prepatch work the same way. +SemVer.prototype.inc = function (release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0 + this.patch = 0 + this.minor = 0 + this.major++ + this.inc('pre', identifier) + break + case 'preminor': + this.prerelease.length = 0 + this.patch = 0 + this.minor++ + this.inc('pre', identifier) + break + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0 + this.inc('patch', identifier) + this.inc('pre', identifier) + break + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) { + this.inc('patch', identifier) + } + this.inc('pre', identifier) + break + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if (this.minor !== 0 || + this.patch !== 0 || + this.prerelease.length === 0) { + this.major++ + } + this.minor = 0 + this.patch = 0 + this.prerelease = [] + break + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) { + this.minor++ + } + this.patch = 0 + this.prerelease = [] + break + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) { + this.patch++ + } + this.prerelease = [] + break + // This probably shouldn't be used publicly. + // 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) { + this.prerelease = [0] + } else { + var i = this.prerelease.length + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++ + i = -2 + } + } + if (i === -1) { + // didn't increment anything + this.prerelease.push(0) + } + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) { + this.prerelease = [identifier, 0] + } + } else { + this.prerelease = [identifier, 0] + } + } + break + + default: + throw new Error('invalid increment argument: ' + release) + } + this.format() + this.raw = this.version + return this +} + +exports.inc = inc +function inc (version, release, loose, identifier) { + if (typeof (loose) === 'string') { + identifier = loose + loose = undefined + } + + try { + return new SemVer(version, loose).inc(release, identifier).version + } catch (er) { + return null + } +} + +exports.diff = diff +function diff (version1, version2) { + if (eq(version1, version2)) { + return null + } else { + var v1 = parse(version1) + var v2 = parse(version2) + var prefix = '' + if (v1.prerelease.length || v2.prerelease.length) { + prefix = 'pre' + var defaultResult = 'prerelease' + } + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return prefix + key + } + } + } + return defaultResult // may be undefined + } +} + +exports.compareIdentifiers = compareIdentifiers + +var numeric = /^[0-9]+$/ +function compareIdentifiers (a, b) { + var anum = numeric.test(a) + var bnum = numeric.test(b) + + if (anum && bnum) { + a = +a + b = +b + } + + return a === b ? 0 + : (anum && !bnum) ? -1 + : (bnum && !anum) ? 1 + : a < b ? -1 + : 1 +} + +exports.rcompareIdentifiers = rcompareIdentifiers +function rcompareIdentifiers (a, b) { + return compareIdentifiers(b, a) +} + +exports.major = major +function major (a, loose) { + return new SemVer(a, loose).major +} + +exports.minor = minor +function minor (a, loose) { + return new SemVer(a, loose).minor +} + +exports.patch = patch +function patch (a, loose) { + return new SemVer(a, loose).patch +} + +exports.compare = compare +function compare (a, b, loose) { + return new SemVer(a, loose).compare(new SemVer(b, loose)) +} + +exports.compareLoose = compareLoose +function compareLoose (a, b) { + return compare(a, b, true) +} + +exports.compareBuild = compareBuild +function compareBuild (a, b, loose) { + var versionA = new SemVer(a, loose) + var versionB = new SemVer(b, loose) + return versionA.compare(versionB) || versionA.compareBuild(versionB) +} + +exports.rcompare = rcompare +function rcompare (a, b, loose) { + return compare(b, a, loose) +} + +exports.sort = sort +function sort (list, loose) { + return list.sort(function (a, b) { + return exports.compareBuild(a, b, loose) + }) +} + +exports.rsort = rsort +function rsort (list, loose) { + return list.sort(function (a, b) { + return exports.compareBuild(b, a, loose) + }) +} + +exports.gt = gt +function gt (a, b, loose) { + return compare(a, b, loose) > 0 +} + +exports.lt = lt +function lt (a, b, loose) { + return compare(a, b, loose) < 0 +} + +exports.eq = eq +function eq (a, b, loose) { + return compare(a, b, loose) === 0 +} + +exports.neq = neq +function neq (a, b, loose) { + return compare(a, b, loose) !== 0 +} + +exports.gte = gte +function gte (a, b, loose) { + return compare(a, b, loose) >= 0 +} + +exports.lte = lte +function lte (a, b, loose) { + return compare(a, b, loose) <= 0 +} + +exports.cmp = cmp +function cmp (a, op, b, loose) { + switch (op) { + case '===': + if (typeof a === 'object') + a = a.version + if (typeof b === 'object') + b = b.version + return a === b + + case '!==': + if (typeof a === 'object') + a = a.version + if (typeof b === 'object') + b = b.version + return a !== b + + case '': + case '=': + case '==': + return eq(a, b, loose) + + case '!=': + return neq(a, b, loose) + + case '>': + return gt(a, b, loose) + + case '>=': + return gte(a, b, loose) + + case '<': + return lt(a, b, loose) + + case '<=': + return lte(a, b, loose) + + default: + throw new TypeError('Invalid operator: ' + op) + } +} + +exports.Comparator = Comparator +function Comparator (comp, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + if (comp instanceof Comparator) { + if (comp.loose === !!options.loose) { + return comp + } else { + comp = comp.value + } + } + + if (!(this instanceof Comparator)) { + return new Comparator(comp, options) + } + + debug('comparator', comp, options) + this.options = options + this.loose = !!options.loose + this.parse(comp) + + if (this.semver === ANY) { + this.value = '' + } else { + this.value = this.operator + this.semver.version + } + + debug('comp', this) +} + +var ANY = {} +Comparator.prototype.parse = function (comp) { + var r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + var m = comp.match(r) + + if (!m) { + throw new TypeError('Invalid comparator: ' + comp) + } + + this.operator = m[1] !== undefined ? m[1] : '' + if (this.operator === '=') { + this.operator = '' + } + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) { + this.semver = ANY + } else { + this.semver = new SemVer(m[2], this.options.loose) + } +} + +Comparator.prototype.toString = function () { + return this.value +} + +Comparator.prototype.test = function (version) { + debug('Comparator.test', version, this.options.loose) + + if (this.semver === ANY || version === ANY) { + return true + } + + if (typeof version === 'string') { + try { + version = new SemVer(version, this.options) + } catch (er) { + return false + } + } + + return cmp(version, this.operator, this.semver, this.options) +} + +Comparator.prototype.intersects = function (comp, options) { + if (!(comp instanceof Comparator)) { + throw new TypeError('a Comparator is required') + } + + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + var rangeTmp + + if (this.operator === '') { + if (this.value === '') { + return true + } + rangeTmp = new Range(comp.value, options) + return satisfies(this.value, rangeTmp, options) + } else if (comp.operator === '') { + if (comp.value === '') { + return true + } + rangeTmp = new Range(this.value, options) + return satisfies(comp.semver, rangeTmp, options) + } + + var sameDirectionIncreasing = + (this.operator === '>=' || this.operator === '>') && + (comp.operator === '>=' || comp.operator === '>') + var sameDirectionDecreasing = + (this.operator === '<=' || this.operator === '<') && + (comp.operator === '<=' || comp.operator === '<') + var sameSemVer = this.semver.version === comp.semver.version + var differentDirectionsInclusive = + (this.operator === '>=' || this.operator === '<=') && + (comp.operator === '>=' || comp.operator === '<=') + var oppositeDirectionsLessThan = + cmp(this.semver, '<', comp.semver, options) && + ((this.operator === '>=' || this.operator === '>') && + (comp.operator === '<=' || comp.operator === '<')) + var oppositeDirectionsGreaterThan = + cmp(this.semver, '>', comp.semver, options) && + ((this.operator === '<=' || this.operator === '<') && + (comp.operator === '>=' || comp.operator === '>')) + + return sameDirectionIncreasing || sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || oppositeDirectionsGreaterThan +} + +exports.Range = Range +function Range (range, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + if (range instanceof Range) { + if (range.loose === !!options.loose && + range.includePrerelease === !!options.includePrerelease) { + return range + } else { + return new Range(range.raw, options) + } + } + + if (range instanceof Comparator) { + return new Range(range.value, options) + } + + if (!(this instanceof Range)) { + return new Range(range, options) + } + + this.options = options + this.loose = !!options.loose + this.includePrerelease = !!options.includePrerelease + + // First, split based on boolean or || + this.raw = range + this.set = range.split(/\s*\|\|\s*/).map(function (range) { + return this.parseRange(range.trim()) + }, this).filter(function (c) { + // throw out any that are not relevant for whatever reason + return c.length + }) + + if (!this.set.length) { + throw new TypeError('Invalid SemVer Range: ' + range) + } + + this.format() +} + +Range.prototype.format = function () { + this.range = this.set.map(function (comps) { + return comps.join(' ').trim() + }).join('||').trim() + return this.range +} + +Range.prototype.toString = function () { + return this.range +} + +Range.prototype.parseRange = function (range) { + var loose = this.options.loose + range = range.trim() + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + var hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] + range = range.replace(hr, hyphenReplace) + debug('hyphen replace', range) + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) + debug('comparator trim', range, re[t.COMPARATORTRIM]) + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[t.TILDETRIM], tildeTrimReplace) + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[t.CARETTRIM], caretTrimReplace) + + // normalize spaces + range = range.split(/\s+/).join(' ') + + // At this point, the range is completely trimmed and + // ready to be split into comparators. + + var compRe = loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + var set = range.split(' ').map(function (comp) { + return parseComparator(comp, this.options) + }, this).join(' ').split(/\s+/) + if (this.options.loose) { + // in loose mode, throw out any that are not valid comparators + set = set.filter(function (comp) { + return !!comp.match(compRe) + }) + } + set = set.map(function (comp) { + return new Comparator(comp, this.options) + }, this) + + return set +} + +Range.prototype.intersects = function (range, options) { + if (!(range instanceof Range)) { + throw new TypeError('a Range is required') + } + + return this.set.some(function (thisComparators) { + return ( + isSatisfiable(thisComparators, options) && + range.set.some(function (rangeComparators) { + return ( + isSatisfiable(rangeComparators, options) && + thisComparators.every(function (thisComparator) { + return rangeComparators.every(function (rangeComparator) { + return thisComparator.intersects(rangeComparator, options) + }) + }) + ) + }) + ) + }) +} + +// take a set of comparators and determine whether there +// exists a version which can satisfy it +function isSatisfiable (comparators, options) { + var result = true + var remainingComparators = comparators.slice() + var testComparator = remainingComparators.pop() + + while (result && remainingComparators.length) { + result = remainingComparators.every(function (otherComparator) { + return testComparator.intersects(otherComparator, options) + }) + + testComparator = remainingComparators.pop() + } + + return result +} + +// Mostly just for testing and legacy API reasons +exports.toComparators = toComparators +function toComparators (range, options) { + return new Range(range, options).set.map(function (comp) { + return comp.map(function (c) { + return c.value + }).join(' ').trim().split(' ') + }) +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +function parseComparator (comp, options) { + debug('comp', comp, options) + comp = replaceCarets(comp, options) + debug('caret', comp) + comp = replaceTildes(comp, options) + debug('tildes', comp) + comp = replaceXRanges(comp, options) + debug('xrange', comp) + comp = replaceStars(comp, options) + debug('stars', comp) + return comp +} + +function isX (id) { + return !id || id.toLowerCase() === 'x' || id === '*' +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes (comp, options) { + return comp.trim().split(/\s+/).map(function (comp) { + return replaceTilde(comp, options) + }).join(' ') +} + +function replaceTilde (comp, options) { + var r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] + return comp.replace(r, function (_, M, m, p, pr) { + debug('tilde', comp, _, M, m, p, pr) + var ret + + if (isX(M)) { + ret = '' + } else if (isX(m)) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0' + } else if (isX(p)) { + // ~1.2 == >=1.2.0 <1.3.0 + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0' + } else if (pr) { + debug('replaceTilde pr', pr) + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + M + '.' + (+m + 1) + '.0' + } else { + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0' + } + + debug('tilde return', ret) + return ret + }) +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets (comp, options) { + return comp.trim().split(/\s+/).map(function (comp) { + return replaceCaret(comp, options) + }).join(' ') +} + +function replaceCaret (comp, options) { + debug('caret', comp, options) + var r = options.loose ? re[t.CARETLOOSE] : re[t.CARET] + return comp.replace(r, function (_, M, m, p, pr) { + debug('caret', comp, _, M, m, p, pr) + var ret + + if (isX(M)) { + ret = '' + } else if (isX(m)) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0' + } else if (isX(p)) { + if (M === '0') { + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0' + } else { + ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0' + } + } else if (pr) { + debug('replaceCaret pr', pr) + if (M === '0') { + if (m === '0') { + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + M + '.' + m + '.' + (+p + 1) + } else { + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + M + '.' + (+m + 1) + '.0' + } + } else { + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + (+M + 1) + '.0.0' + } + } else { + debug('no pr') + if (M === '0') { + if (m === '0') { + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + m + '.' + (+p + 1) + } else { + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0' + } + } else { + ret = '>=' + M + '.' + m + '.' + p + + ' <' + (+M + 1) + '.0.0' + } + } + + debug('caret return', ret) + return ret + }) +} + +function replaceXRanges (comp, options) { + debug('replaceXRanges', comp, options) + return comp.split(/\s+/).map(function (comp) { + return replaceXRange(comp, options) + }).join(' ') +} + +function replaceXRange (comp, options) { + comp = comp.trim() + var r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] + return comp.replace(r, function (ret, gtlt, M, m, p, pr) { + debug('xRange', comp, ret, gtlt, M, m, p, pr) + var xM = isX(M) + var xm = xM || isX(m) + var xp = xm || isX(p) + var anyX = xp + + if (gtlt === '=' && anyX) { + gtlt = '' + } + + // if we're including prereleases in the match, then we need + // to fix this to -0, the lowest possible prerelease value + pr = options.includePrerelease ? '-0' : '' + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0-0' + } else { + // nothing is forbidden + ret = '*' + } + } else if (gtlt && anyX) { + // we know patch is an x, because we have any x at all. + // replace X with 0 + if (xm) { + m = 0 + } + p = 0 + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + gtlt = '>=' + if (xm) { + M = +M + 1 + m = 0 + p = 0 + } else { + m = +m + 1 + p = 0 + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<' + if (xm) { + M = +M + 1 + } else { + m = +m + 1 + } + } + + ret = gtlt + M + '.' + m + '.' + p + pr + } else if (xm) { + ret = '>=' + M + '.0.0' + pr + ' <' + (+M + 1) + '.0.0' + pr + } else if (xp) { + ret = '>=' + M + '.' + m + '.0' + pr + + ' <' + M + '.' + (+m + 1) + '.0' + pr + } + + debug('xRange return', ret) + + return ret + }) +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +function replaceStars (comp, options) { + debug('replaceStars', comp, options) + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[t.STAR], '') +} + +// This function is passed to string.replace(re[t.HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace ($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) { + if (isX(fM)) { + from = '' + } else if (isX(fm)) { + from = '>=' + fM + '.0.0' + } else if (isX(fp)) { + from = '>=' + fM + '.' + fm + '.0' + } else { + from = '>=' + from + } + + if (isX(tM)) { + to = '' + } else if (isX(tm)) { + to = '<' + (+tM + 1) + '.0.0' + } else if (isX(tp)) { + to = '<' + tM + '.' + (+tm + 1) + '.0' + } else if (tpr) { + to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr + } else { + to = '<=' + to + } + + return (from + ' ' + to).trim() +} + +// if ANY of the sets match ALL of its comparators, then pass +Range.prototype.test = function (version) { + if (!version) { + return false + } + + if (typeof version === 'string') { + try { + version = new SemVer(version, this.options) + } catch (er) { + return false + } + } + + for (var i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version, this.options)) { + return true + } + } + return false +} + +function testSet (set, version, options) { + for (var i = 0; i < set.length; i++) { + if (!set[i].test(version)) { + return false + } + } + + if (version.prerelease.length && !options.includePrerelease) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (i = 0; i < set.length; i++) { + debug(set[i].semver) + if (set[i].semver === ANY) { + continue + } + + if (set[i].semver.prerelease.length > 0) { + var allowed = set[i].semver + if (allowed.major === version.major && + allowed.minor === version.minor && + allowed.patch === version.patch) { + return true + } + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false + } + + return true +} + +exports.satisfies = satisfies +function satisfies (version, range, options) { + try { + range = new Range(range, options) + } catch (er) { + return false + } + return range.test(version) +} + +exports.maxSatisfying = maxSatisfying +function maxSatisfying (versions, range, options) { + var max = null + var maxSV = null + try { + var rangeObj = new Range(range, options) + } catch (er) { + return null + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { + // satisfies(v, range, options) + if (!max || maxSV.compare(v) === -1) { + // compare(max, v, true) + max = v + maxSV = new SemVer(max, options) + } + } + }) + return max +} + +exports.minSatisfying = minSatisfying +function minSatisfying (versions, range, options) { + var min = null + var minSV = null + try { + var rangeObj = new Range(range, options) + } catch (er) { + return null + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { + // satisfies(v, range, options) + if (!min || minSV.compare(v) === 1) { + // compare(min, v, true) + min = v + minSV = new SemVer(min, options) + } + } + }) + return min +} + +exports.minVersion = minVersion +function minVersion (range, loose) { + range = new Range(range, loose) + + var minver = new SemVer('0.0.0') + if (range.test(minver)) { + return minver + } + + minver = new SemVer('0.0.0-0') + if (range.test(minver)) { + return minver + } + + minver = null + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i] + + comparators.forEach(function (comparator) { + // Clone to avoid manipulating the comparator's semver object. + var compver = new SemVer(comparator.semver.version) + switch (comparator.operator) { + case '>': + if (compver.prerelease.length === 0) { + compver.patch++ + } else { + compver.prerelease.push(0) + } + compver.raw = compver.format() + /* fallthrough */ + case '': + case '>=': + if (!minver || gt(minver, compver)) { + minver = compver + } + break + case '<': + case '<=': + /* Ignore maximum versions */ + break + /* istanbul ignore next */ + default: + throw new Error('Unexpected operation: ' + comparator.operator) + } + }) + } + + if (minver && range.test(minver)) { + return minver + } + + return null +} + +exports.validRange = validRange +function validRange (range, options) { + try { + // Return '*' instead of '' so that truthiness works. + // This will throw if it's invalid anyway + return new Range(range, options).range || '*' + } catch (er) { + return null + } +} + +// Determine if version is less than all the versions possible in the range +exports.ltr = ltr +function ltr (version, range, options) { + return outside(version, range, '<', options) +} + +// Determine if version is greater than all the versions possible in the range. +exports.gtr = gtr +function gtr (version, range, options) { + return outside(version, range, '>', options) +} + +exports.outside = outside +function outside (version, range, hilo, options) { + version = new SemVer(version, options) + range = new Range(range, options) + + var gtfn, ltefn, ltfn, comp, ecomp + switch (hilo) { + case '>': + gtfn = gt + ltefn = lte + ltfn = lt + comp = '>' + ecomp = '>=' + break + case '<': + gtfn = lt + ltefn = gte + ltfn = gt + comp = '<' + ecomp = '<=' + break + default: + throw new TypeError('Must provide a hilo val of "<" or ">"') + } + + // If it satisifes the range it is not outside + if (satisfies(version, range, options)) { + return false + } + + // From now on, variable terms are as if we're in "gtr" mode. + // but note that everything is flipped for the "ltr" function. + + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i] + + var high = null + var low = null + + comparators.forEach(function (comparator) { + if (comparator.semver === ANY) { + comparator = new Comparator('>=0.0.0') + } + high = high || comparator + low = low || comparator + if (gtfn(comparator.semver, high.semver, options)) { + high = comparator + } else if (ltfn(comparator.semver, low.semver, options)) { + low = comparator + } + }) + + // If the edge version comparator has a operator then our version + // isn't outside it + if (high.operator === comp || high.operator === ecomp) { + return false + } + + // If the lowest version comparator has an operator and our version + // is less than it then it isn't higher than the range + if ((!low.operator || low.operator === comp) && + ltefn(version, low.semver)) { + return false + } else if (low.operator === ecomp && ltfn(version, low.semver)) { + return false + } + } + return true +} + +exports.prerelease = prerelease +function prerelease (version, options) { + var parsed = parse(version, options) + return (parsed && parsed.prerelease.length) ? parsed.prerelease : null +} + +exports.intersects = intersects +function intersects (r1, r2, options) { + r1 = new Range(r1, options) + r2 = new Range(r2, options) + return r1.intersects(r2) +} + +exports.coerce = coerce +function coerce (version, options) { + if (version instanceof SemVer) { + return version + } + + if (typeof version === 'number') { + version = String(version) + } + + if (typeof version !== 'string') { + return null + } + + options = options || {} + + var match = null + if (!options.rtl) { + match = version.match(re[t.COERCE]) + } else { + // Find the right-most coercible string that does not share + // a terminus with a more left-ward coercible string. + // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4' + // + // Walk through the string checking with a /g regexp + // Manually set the index so as to pick up overlapping matches. + // Stop when we get a match that ends at the string end, since no + // coercible string can be more right-ward without the same terminus. + var next + while ((next = re[t.COERCERTL].exec(version)) && + (!match || match.index + match[0].length !== version.length) + ) { + if (!match || + next.index + next[0].length !== match.index + match[0].length) { + match = next + } + re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length + } + // leave it in a clean state + re[t.COERCERTL].lastIndex = -1 + } + + if (match === null) { + return null + } + + return parse(match[2] + + '.' + (match[3] || '0') + + '.' + (match[4] || '0'), options) +} + + +/***/ }), +/* 925 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const EventEmitter = __webpack_require__(379); + +const written = new WeakMap(); + +class ProgressEmitter extends EventEmitter { + constructor(source, destination) { + super(); + this._source = source; + this._destination = destination; + } + + set written(value) { + written.set(this, value); + this.emitProgress(); + } + + get written() { + return written.get(this); + } + + emitProgress() { + const {size, written} = this; + this.emit('progress', { + src: this._source, + dest: this._destination, + size, + written, + percent: written === size ? 1 : written / size + }); + } +} + +module.exports = ProgressEmitter; + + +/***/ }), +/* 926 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const blacklist = [ + // # All + '^npm-debug\\.log$', // Error log for npm + '^\\..*\\.swp$', // Swap file for vim state + + // # macOS + '^\\.DS_Store$', // Stores custom folder attributes + '^\\.AppleDouble$', // Stores additional file resources + '^\\.LSOverride$', // Contains the absolute path to the app to be used + '^Icon\\r$', // Custom Finder icon: http://superuser.com/questions/298785/icon-file-on-os-x-desktop + '^\\._.*', // Thumbnail + '^\\.Spotlight-V100(?:$|\\/)', // Directory that might appear on external disk + '\\.Trashes', // File that might appear on external disk + '^__MACOSX$', // Resource fork + + // # Linux + '~$', // Backup file + + // # Windows + '^Thumbs\\.db$', // Image file cache + '^ehthumbs\\.db$', // Folder config file + '^Desktop\\.ini$', // Stores custom folder attributes + '@eaDir$' // Synology Diskstation "hidden" folder where the server stores thumbnails +]; + +exports.re = () => { + throw new Error('`junk.re` was renamed to `junk.regex`'); +}; + +exports.regex = new RegExp(blacklist.join('|')); + +exports.is = filename => exports.regex.test(filename); + +exports.not = filename => !exports.is(filename); + +// TODO: Remove this for the next major release +exports.default = module.exports; + + +/***/ }), +/* 927 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const NestedError = __webpack_require__(928); + +class CpyError extends NestedError { + constructor(message, nested) { + super(message, nested); + Object.assign(this, nested); + this.name = 'CpyError'; + } +} + +module.exports = CpyError; + + +/***/ }), +/* 928 */ +/***/ (function(module, exports, __webpack_require__) { + +var inherits = __webpack_require__(29).inherits; + +var NestedError = function (message, nested) { + this.nested = nested; + + if (message instanceof Error) { + nested = message; + } else if (typeof message !== 'undefined') { + Object.defineProperty(this, 'message', { + value: message, + writable: true, + enumerable: false, + configurable: true + }); + } + + Error.captureStackTrace(this, this.constructor); + var oldStackDescriptor = Object.getOwnPropertyDescriptor(this, 'stack'); + var stackDescriptor = buildStackDescriptor(oldStackDescriptor, nested); + Object.defineProperty(this, 'stack', stackDescriptor); +}; + +function buildStackDescriptor(oldStackDescriptor, nested) { + if (oldStackDescriptor.get) { + return { + get: function () { + var stack = oldStackDescriptor.get.call(this); + return buildCombinedStacks(stack, this.nested); + } + }; + } else { + var stack = oldStackDescriptor.value; + return { + value: buildCombinedStacks(stack, nested) + }; + } +} + +function buildCombinedStacks(stack, nested) { + if (nested) { + stack += '\nCaused By: ' + nested.stack; + } + return stack; +} + +inherits(NestedError, Error); +NestedError.prototype.name = 'NestedError'; + + +module.exports = NestedError; + + +/***/ }), +/* 929 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index f57365905292b..444d46307b059 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -39,7 +39,7 @@ "babel-loader": "^8.0.6", "chalk": "^2.4.2", "cmd-shim": "^2.1.0", - "cpy": "^7.3.0", + "cpy": "^8.0.0", "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^3.2.0", @@ -63,8 +63,8 @@ "tempy": "^0.3.0", "typescript": "3.7.2", "unlazy-loader": "^0.1.3", - "webpack": "^4.41.0", - "webpack-cli": "^3.3.9", + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10", "wrap-ansi": "^3.0.1", "write-pkg": "^4.0.0" }, diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 6948ae81806eb..73deadba0a619 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -27,6 +27,6 @@ "rxjs": "6.5.2", "serve-static": "1.14.1", "styled-components": "^3", - "webpack": "4.34.0" + "webpack": "^4.41.5" } } \ No newline at end of file diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index d50f6a15c2e0b..8645923a13d30 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -59,6 +59,19 @@ const makeSuccessMessage = options => { * @property {string} options.esFrom Optionally run from source instead of snapshot */ export async function runTests(options) { + if (!process.env.KBN_NP_PLUGINS_BUILT) { + const log = options.createLogger(); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + log.warning( + " Don't forget to use `node scripts/build_kibana_platform_plugins` to build plugins you plan on testing" + ); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + } + for (const configPath of options.configs) { const log = options.createLogger(); const opts = { diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 4bb4c660a01ab..fc245ca3fe921 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,13 +33,13 @@ "@babel/core": "^7.5.5", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "9.6.1", + "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", "chokidar": "3.2.1", "core-js": "^3.2.1", - "css-loader": "^2.1.1", + "css-loader": "^3.4.2", "expose-loader": "^0.7.5", "file-loader": "^4.2.0", "grunt": "1.0.4", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.5", + "postcss": "^7.0.26", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", @@ -64,10 +64,10 @@ "redux": "3.7.2", "redux-thunk": "2.2.0", "regenerator-runtime": "^0.13.3", - "sass-loader": "^7.3.1", + "sass-loader": "^8.0.2", "sinon": "^7.4.2", - "style-loader": "^0.23.1", - "webpack": "^4.41.0", + "style-loader": "^1.1.3", + "webpack": "^4.41.5", "webpack-dev-server": "^3.8.2", "yeoman-generator": "1.1.1", "yo": "2.0.6" diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index fc9d159ea9b95..0b1a31619fdf9 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -17,7 +17,7 @@ "@yarnpkg/lockfile": "^1.1.0", "angular": "^1.7.9", "core-js": "^3.2.1", - "css-loader": "^2.1.1", + "css-loader": "^3.4.2", "custom-event-polyfill": "^0.3.0", "del": "^5.1.0", "jquery": "^3.4.1", @@ -30,7 +30,7 @@ "read-pkg": "^5.2.0", "regenerator-runtime": "^0.13.3", "symbol-observable": "^1.2.0", - "webpack": "4.41.0", + "webpack": "^4.41.5", "whatwg-fetch": "^3.0.0" } } diff --git a/renovate.json5 b/renovate.json5 index 1fbe83476d4a8..642c4a98b5799 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -123,6 +123,14 @@ '@types/bluebird', ], }, + { + groupSlug: 'browserslist-useragent', + groupName: 'browserslist-useragent related packages', + packageNames: [ + 'browserslist-useragent', + '@types/browserslist-useragent', + ], + }, { groupSlug: 'chance', groupName: 'chance related packages', @@ -929,6 +937,14 @@ '@types/vinyl-fs', ], }, + { + groupSlug: 'watchpack', + groupName: 'watchpack related packages', + packageNames: [ + 'watchpack', + '@types/watchpack', + ], + }, { groupSlug: 'webpack', groupName: 'webpack related packages', diff --git a/scripts/build_kibana_platform_plugins.js b/scripts/build_kibana_platform_plugins.js new file mode 100644 index 0000000000000..4d6963144d085 --- /dev/null +++ b/scripts/build_kibana_platform_plugins.js @@ -0,0 +1,20 @@ +/* + * 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. + */ + +require('@kbn/optimizer/target/cli'); diff --git a/src/cli/cluster/cluster_manager.test.ts b/src/cli/cluster/cluster_manager.test.ts index bd37e854e1691..707778861fb59 100644 --- a/src/cli/cluster/cluster_manager.test.ts +++ b/src/cli/cluster/cluster_manager.test.ts @@ -17,7 +17,24 @@ * under the License. */ +import * as Rx from 'rxjs'; + import { mockCluster } from './cluster_manager.test.mocks'; + +jest.mock('./run_kbn_optimizer', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires,no-shadow + const Rx = require('rxjs'); + + return { + runKbnOptimizer: () => + new Rx.BehaviorSubject({ + type: 'compiler success', + durSec: 0, + bundles: [], + }), + }; +}); + jest.mock('readline', () => ({ createInterface: jest.fn(() => ({ on: jest.fn(), @@ -26,6 +43,13 @@ jest.mock('readline', () => ({ })), })); +const mockConfig: any = { + get: (key: string) => { + expect(key).toBe('optimize.enabled'); + return false; + }, +}; + import { sample } from 'lodash'; import { ClusterManager } from './cluster_manager'; @@ -51,7 +75,7 @@ describe('CLI cluster manager', () => { }); test('has two workers', () => { - const manager = new ClusterManager({}, {} as any); + const manager = new ClusterManager({}, mockConfig); expect(manager.workers).toHaveLength(2); for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker); @@ -61,7 +85,7 @@ describe('CLI cluster manager', () => { }); test('delivers broadcast messages to other workers', () => { - const manager = new ClusterManager({}, {} as any); + const manager = new ClusterManager({}, mockConfig); for (const worker of manager.workers) { Worker.prototype.start.call(worker); // bypass the debounced start method @@ -86,92 +110,59 @@ describe('CLI cluster manager', () => { test('correctly configures `BasePathProxy`.', async () => { const basePathProxyMock = { start: jest.fn() }; - new ClusterManager({}, {} as any, basePathProxyMock as any); + new ClusterManager({}, mockConfig, basePathProxyMock as any); expect(basePathProxyMock.start).toHaveBeenCalledWith({ shouldRedirectFromOldBasePath: expect.any(Function), - blockUntil: expect.any(Function), + delayUntil: expect.any(Function), }); }); - describe('proxy is configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', () => { + describe('basePathProxy config', () => { let clusterManager: ClusterManager; let shouldRedirectFromOldBasePath: (path: string) => boolean; - let blockUntil: () => Promise; + let delayUntil: () => Rx.Observable; + beforeEach(async () => { const basePathProxyMock = { start: jest.fn() }; - - clusterManager = new ClusterManager({}, {} as any, basePathProxyMock as any); - - jest.spyOn(clusterManager.server, 'on'); - jest.spyOn(clusterManager.server, 'off'); - - [[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.start.mock.calls; - }); - - test('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', () => { - expect(shouldRedirectFromOldBasePath('')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); + clusterManager = new ClusterManager({}, mockConfig, basePathProxyMock as any); + [[{ delayUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.start.mock.calls; }); - test('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', () => { - expect(shouldRedirectFromOldBasePath('app/')).toBe(true); - expect(shouldRedirectFromOldBasePath('login')).toBe(true); - expect(shouldRedirectFromOldBasePath('logout')).toBe(true); - expect(shouldRedirectFromOldBasePath('status')).toBe(true); - }); - - test('`blockUntil()` resolves immediately if worker has already crashed.', async () => { - clusterManager.server.crashed = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(clusterManager.server.on).not.toHaveBeenCalled(); - expect(clusterManager.server.off).not.toHaveBeenCalled(); + describe('shouldRedirectFromOldBasePath()', () => { + test('returns `false` for unknown paths.', () => { + expect(shouldRedirectFromOldBasePath('')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); + }); + + test('returns `true` for `app` and other known paths.', () => { + expect(shouldRedirectFromOldBasePath('app/')).toBe(true); + expect(shouldRedirectFromOldBasePath('login')).toBe(true); + expect(shouldRedirectFromOldBasePath('logout')).toBe(true); + expect(shouldRedirectFromOldBasePath('status')).toBe(true); + }); }); - test('`blockUntil()` resolves immediately if worker is already listening.', async () => { - clusterManager.server.listening = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(clusterManager.server.on).not.toHaveBeenCalled(); - expect(clusterManager.server.off).not.toHaveBeenCalled(); - }); - - test('`blockUntil()` resolves when worker crashes.', async () => { - const blockUntilPromise = blockUntil(); - - expect(clusterManager.server.on).toHaveBeenCalledTimes(2); - expect(clusterManager.server.on).toHaveBeenCalledWith('crashed', expect.any(Function)); - - const [, [eventName, onCrashed]] = (clusterManager.server.on as jest.Mock).mock.calls; - // Check event name to make sure we call the right callback, - // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. - expect(eventName).toBe('crashed'); - expect(clusterManager.server.off).not.toHaveBeenCalled(); - - onCrashed(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(clusterManager.server.off).toHaveBeenCalledTimes(2); - }); - - test('`blockUntil()` resolves when worker starts listening.', async () => { - const blockUntilPromise = blockUntil(); - - expect(clusterManager.server.on).toHaveBeenCalledTimes(2); - expect(clusterManager.server.on).toHaveBeenCalledWith('listening', expect.any(Function)); - - const [[eventName, onListening]] = (clusterManager.server.on as jest.Mock).mock.calls; - // Check event name to make sure we call the right callback, - // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. - expect(eventName).toBe('listening'); - expect(clusterManager.server.off).not.toHaveBeenCalled(); - - onListening(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(clusterManager.server.off).toHaveBeenCalledTimes(2); + describe('delayUntil()', () => { + test('returns an observable which emits when the server and kbnOptimizer are ready and completes', async () => { + clusterManager.serverReady$.next(false); + clusterManager.optimizerReady$.next(false); + clusterManager.kbnOptimizerReady$.next(false); + + const events: Array = []; + delayUntil().subscribe( + () => events.push('next'), + error => events.push(error), + () => events.push('complete') + ); + + clusterManager.serverReady$.next(true); + expect(events).toEqual([]); + + clusterManager.kbnOptimizerReady$.next(true); + expect(events).toEqual(['next', 'complete']); + }); }); }); }); diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index 3fa4bdcbc5fa5..2f308915fb332 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -19,22 +19,29 @@ import { resolve } from 'path'; import { format as formatUrl } from 'url'; + import opn from 'opn'; -import { debounce, invoke, bindAll, once, uniq } from 'lodash'; -import * as Rx from 'rxjs'; -import { first, mapTo, filter, map, take } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; import { FSWatcher } from 'chokidar'; +import * as Rx from 'rxjs'; +import { startWith, mapTo, filter, map, take, tap } from 'rxjs/operators'; +import { runKbnOptimizer } from './run_kbn_optimizer'; import { LegacyConfig } from '../../core/server/legacy'; import { BasePathProxyServer } from '../../core/server/http'; -// @ts-ignore -import Log from '../log'; +import { Log } from './log'; import { Worker } from './worker'; process.env.kbnWorkerType = 'managr'; +const firstAllTrue = (...sources: Array>) => + Rx.combineLatest(...sources).pipe( + filter(values => values.every(v => v === true)), + take(1), + mapTo(undefined) + ); + export class ClusterManager { public optimizer: Worker; public server: Worker; @@ -42,10 +49,17 @@ export class ClusterManager { private watcher: FSWatcher | null = null; private basePathProxy: BasePathProxyServer | undefined; - private log: any; + private log: Log; private addedCount = 0; private inReplMode: boolean; + // exposed for testing + public readonly serverReady$ = new Rx.ReplaySubject(1); + // exposed for testing + public readonly optimizerReady$ = new Rx.ReplaySubject(1); + // exposed for testing + public readonly kbnOptimizerReady$ = new Rx.ReplaySubject(1); + constructor( opts: Record, config: LegacyConfig, @@ -55,6 +69,23 @@ export class ClusterManager { this.inReplMode = !!opts.repl; this.basePathProxy = basePathProxy; + if (config.get('optimize.enabled') !== false) { + // run @kbn/optimizer and write it's state to kbnOptimizerReady$ + runKbnOptimizer(opts, config) + .pipe( + map(({ state }) => state.phase === 'success' || state.phase === 'issue'), + tap({ + error: error => { + this.log.bad('New platform optimizer error', error.stack); + process.exit(1); + }, + }) + ) + .subscribe(this.kbnOptimizerReady$); + } else { + this.kbnOptimizerReady$.next(true); + } + const serverArgv = []; const optimizerArgv = ['--plugins.initialize=false', '--server.autoListen=false']; @@ -86,6 +117,27 @@ export class ClusterManager { })), ]; + // write server status to the serverReady$ subject + Rx.merge( + Rx.fromEvent(this.server, 'starting').pipe(mapTo(false)), + Rx.fromEvent(this.server, 'listening').pipe(mapTo(true)), + Rx.fromEvent(this.server, 'crashed').pipe(mapTo(true)) + ) + .pipe(startWith(this.server.listening || this.server.crashed)) + .subscribe(this.serverReady$); + + // write optimizer status to the optimizerReady$ subject + Rx.merge( + Rx.fromEvent(this.optimizer, 'optimizeStatus'), + Rx.defer(() => { + if (this.optimizer.fork) { + this.optimizer.fork.send({ optimizeReady: '?' }); + } + }) + ) + .pipe(map((msg: any) => msg && !!msg.success)) + .subscribe(this.optimizerReady$); + // broker messages between workers this.workers.forEach(worker => { worker.on('broadcast', msg => { @@ -109,8 +161,6 @@ export class ClusterManager { }); }); - bindAll(this, 'onWatcherAdd', 'onWatcherError', 'onWatcherChange'); - if (opts.open) { this.setupOpen( formatUrl({ @@ -137,11 +187,11 @@ export class ClusterManager { .reduce( (acc, path) => acc.concat( - resolve(path, 'test'), - resolve(path, 'build'), - resolve(path, 'target'), - resolve(path, 'scripts'), - resolve(path, 'docs') + resolve(path, 'test/**'), + resolve(path, 'build/**'), + resolve(path, 'target/**'), + resolve(path, 'scripts/**'), + resolve(path, 'docs/**') ), [] as string[] ); @@ -152,33 +202,36 @@ export class ClusterManager { startCluster() { this.setupManualRestart(); - invoke(this.workers, 'start'); + for (const worker of this.workers) { + worker.start(); + } if (this.basePathProxy) { this.basePathProxy.start({ - blockUntil: this.blockUntil.bind(this), - shouldRedirectFromOldBasePath: this.shouldRedirectFromOldBasePath.bind(this), + delayUntil: () => firstAllTrue(this.serverReady$, this.kbnOptimizerReady$), + + shouldRedirectFromOldBasePath: (path: string) => { + // strip `s/{id}` prefix when checking for need to redirect + if (path.startsWith('s/')) { + path = path + .split('/') + .slice(2) + .join('/'); + } + + const isApp = path.startsWith('app/'); + const isKnownShortPath = ['login', 'logout', 'status'].includes(path); + return isApp || isKnownShortPath; + }, }); } } setupOpen(openUrl: string) { - const serverListening$ = Rx.merge( - Rx.fromEvent(this.server, 'listening').pipe(mapTo(true)), - Rx.fromEvent(this.server, 'fork:exit').pipe(mapTo(false)), - Rx.fromEvent(this.server, 'crashed').pipe(mapTo(false)) - ); - - const optimizeSuccess$ = Rx.fromEvent(this.optimizer, 'optimizeStatus').pipe( - map((msg: any) => !!msg.success) - ); - - Rx.combineLatest(serverListening$, optimizeSuccess$) - .pipe( - filter(([serverListening, optimizeSuccess]) => serverListening && optimizeSuccess), - take(1) - ) + firstAllTrue(this.serverReady$, this.kbnOptimizerReady$, this.optimizerReady$) .toPromise() - .then(() => opn(openUrl)); + .then(() => { + opn(openUrl); + }); } setupWatching(extraPaths: string[], pluginInternalDirsIgnore: string[]) { @@ -187,53 +240,51 @@ export class ClusterManager { // eslint-disable-next-line @typescript-eslint/no-var-requires const { fromRoot } = require('../../core/server/utils'); - const watchPaths = [ - fromRoot('src/core'), - fromRoot('src/legacy/core_plugins'), - fromRoot('src/legacy/server'), - fromRoot('src/legacy/ui'), - fromRoot('src/legacy/utils'), - fromRoot('x-pack/legacy/common'), - fromRoot('x-pack/legacy/plugins'), - fromRoot('x-pack/legacy/server'), - fromRoot('config'), - ...extraPaths, - ].map(path => resolve(path)); + const watchPaths = Array.from( + new Set( + [ + fromRoot('src/core'), + fromRoot('src/legacy/core_plugins'), + fromRoot('src/legacy/server'), + fromRoot('src/legacy/ui'), + fromRoot('src/legacy/utils'), + fromRoot('x-pack/legacy/common'), + fromRoot('x-pack/legacy/plugins'), + fromRoot('x-pack/legacy/server'), + fromRoot('config'), + ...extraPaths, + ].map(path => resolve(path)) + ) + ); const ignorePaths = [ + /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, + /\.test\.(js|ts)$/, + ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), fromRoot('x-pack/legacy/plugins/reporting/.chromium'), fromRoot('x-pack/legacy/plugins/siem/cypress'), fromRoot('x-pack/legacy/plugins/apm/cypress'), fromRoot('x-pack/legacy/plugins/apm/scripts'), - fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes + fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, + 'plugins/java_languageserver', ]; - this.watcher = chokidar.watch(uniq(watchPaths), { + this.watcher = chokidar.watch(watchPaths, { cwd: fromRoot('.'), - ignored: [ - /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, - /\.test\.(js|ts)$/, - ...pluginInternalDirsIgnore, - ...ignorePaths, - 'plugins/java_languageserver', - ], + ignored: ignorePaths, }) as FSWatcher; this.watcher.on('add', this.onWatcherAdd); this.watcher.on('error', this.onWatcherError); + this.watcher.once('ready', () => { + // start sending changes to workers + this.watcher!.removeListener('add', this.onWatcherAdd); + this.watcher!.on('all', this.onWatcherChange); - this.watcher.on( - 'ready', - once(() => { - // start sending changes to workers - this.watcher!.removeListener('add', this.onWatcherAdd); - this.watcher!.on('all', this.onWatcherChange); - - this.log.good('watching for changes', `(${this.addedCount} files)`); - this.startCluster(); - }) - ); + this.log.good('watching for changes', `(${this.addedCount} files)`); + this.startCluster(); + }); } setupManualRestart() { @@ -249,7 +300,20 @@ export class ClusterManager { let nls = 0; const clear = () => (nls = 0); - const clearSoon = debounce(clear, 2000); + + let clearTimer: number | undefined; + const clearSoon = () => { + clearSoon.cancel(); + clearTimer = setTimeout(() => { + clearTimer = undefined; + clear(); + }); + }; + + clearSoon.cancel = () => { + clearTimeout(clearTimer); + clearTimer = undefined; + }; rl.setPrompt(''); rl.prompt(); @@ -274,41 +338,18 @@ export class ClusterManager { }); } - onWatcherAdd() { + onWatcherAdd = () => { this.addedCount += 1; - } + }; - onWatcherChange(e: any, path: string) { - invoke(this.workers, 'onChange', path); - } + onWatcherChange = (e: any, path: string) => { + for (const worker of this.workers) { + worker.onChange(path); + } + }; - onWatcherError(err: any) { + onWatcherError = (err: any) => { this.log.bad('failed to watch files!\n', err.stack); process.exit(1); // eslint-disable-line no-process-exit - } - - shouldRedirectFromOldBasePath(path: string) { - // strip `s/{id}` prefix when checking for need to redirect - if (path.startsWith('s/')) { - path = path - .split('/') - .slice(2) - .join('/'); - } - - const isApp = path.startsWith('app/'); - const isKnownShortPath = ['login', 'logout', 'status'].includes(path); - return isApp || isKnownShortPath; - } - - blockUntil() { - // Wait until `server` worker either crashes or starts to listen. - if (this.server.listening || this.server.crashed) { - return Promise.resolve(); - } - - return Rx.race(Rx.fromEvent(this.server, 'listening'), Rx.fromEvent(this.server, 'crashed')) - .pipe(first()) - .toPromise(); - } + }; } diff --git a/src/cli/cluster/log.ts b/src/cli/cluster/log.ts new file mode 100644 index 0000000000000..af73059c0758e --- /dev/null +++ b/src/cli/cluster/log.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 Chalk from 'chalk'; + +export class Log { + constructor(private readonly quiet: boolean, private readonly silent: boolean) {} + + good(label: string, ...args: any[]) { + if (this.quiet || this.silent) { + return; + } + + // eslint-disable-next-line no-console + console.log(Chalk.black.bgGreen(` ${label.trim()} `), ...args); + } + + warn(label: string, ...args: any[]) { + if (this.quiet || this.silent) { + return; + } + + // eslint-disable-next-line no-console + console.log(Chalk.black.bgYellow(` ${label.trim()} `), ...args); + } + + bad(label: string, ...args: any[]) { + if (this.silent) { + return; + } + + // eslint-disable-next-line no-console + console.log(Chalk.white.bgRed(` ${label.trim()} `), ...args); + } + + write(label: string, ...args: any[]) { + // eslint-disable-next-line no-console + console.log(` ${label.trim()} `, ...args); + } +} diff --git a/src/cli/cluster/run_kbn_optimizer.ts b/src/cli/cluster/run_kbn_optimizer.ts new file mode 100644 index 0000000000000..7752d4a45ab65 --- /dev/null +++ b/src/cli/cluster/run_kbn_optimizer.ts @@ -0,0 +1,79 @@ +/* + * 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 Chalk from 'chalk'; +import moment from 'moment'; +import { + ToolingLog, + pickLevelFromFlags, + ToolingLogTextWriter, + parseLogLevel, + REPO_ROOT, +} from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; + +import { LegacyConfig } from '../../core/server/legacy'; + +export function runKbnOptimizer(opts: Record, config: LegacyConfig) { + const optimizerConfig = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + watch: true, + oss: !!opts.oss, + examples: !!opts.runExamples, + pluginPaths: config.get('plugins.paths'), + }); + + const dim = Chalk.dim('np bld'); + const name = Chalk.magentaBright('@kbn/optimizer'); + const time = () => moment().format('HH:mm:ss.SSS'); + const level = (msgType: string) => { + switch (msgType) { + case 'info': + return Chalk.green(msgType); + case 'success': + return Chalk.cyan(msgType); + case 'debug': + return Chalk.gray(msgType); + default: + return msgType; + } + }; + const { flags: levelFlags } = parseLogLevel(pickLevelFromFlags(opts)); + const toolingLog = new ToolingLog(); + const has = (obj: T, x: any): x is keyof T => obj.hasOwnProperty(x); + + toolingLog.setWriters([ + { + write(msg) { + if (has(levelFlags, msg.type) && !levelFlags[msg.type]) { + return false; + } + + ToolingLogTextWriter.write( + process.stdout, + `${dim} log [${time()}] [${level(msg.type)}][${name}] `, + msg + ); + return true; + }, + }, + ]); + + return runOptimizer(optimizerConfig).pipe(logOptimizerState(toolingLog, optimizerConfig)); +} diff --git a/src/cli/cluster/worker.test.ts b/src/cli/cluster/worker.test.ts index 4f9337681e083..e775f71442a77 100644 --- a/src/cli/cluster/worker.test.ts +++ b/src/cli/cluster/worker.test.ts @@ -20,8 +20,8 @@ import { mockCluster } from './cluster_manager.test.mocks'; import { Worker, ClusterWorker } from './worker'; -// @ts-ignore -import Log from '../log'; + +import { Log } from './log'; const workersToShutdown: Worker[] = []; diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts index fb87f1a87654c..c73d3edbf7df7 100644 --- a/src/cli/cluster/worker.ts +++ b/src/cli/cluster/worker.ts @@ -199,6 +199,7 @@ export class Worker extends EventEmitter { } this.fork = cluster.fork(this.env) as ClusterWorker; + this.emit('starting'); this.forkBinder = new BinderFor(this.fork); // when the fork sends a message, comes online, or loses its connection, then react diff --git a/src/cli/command.js b/src/cli/command.js index 06ee87e3198fd..6f083bb2a1fa2 100644 --- a/src/cli/command.js +++ b/src/cli/command.js @@ -18,17 +18,17 @@ */ import _ from 'lodash'; +import Chalk from 'chalk'; import help from './help'; import { Command } from 'commander'; -import { red } from './color'; Command.prototype.error = function(err) { if (err && err.message) err = err.message; console.log( ` -${red(' ERROR ')} ${err} +${Chalk.white.bgRed(' ERROR ')} ${err} ${help(this, ' ')} ` diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 9cf5691b88399..be3fc319389d7 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -195,7 +195,7 @@ export default function(program) { [] ) .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) - .option('--optimize', 'Optimize and then stop the server'); + .option('--optimize', 'Run the legacy plugin optimizer and then stop the server'); if (CAN_REPL) { command.option('--repl', 'Run the server with a REPL prompt and access to the server object'); diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts index e24be35331f39..e5cbffc3e2d94 100644 --- a/src/core/public/plugins/plugin_loader.test.ts +++ b/src/core/public/plugins/plugin_loader.test.ts @@ -62,7 +62,7 @@ test('`loadPluginBundles` creates a script tag and loads initializer', async () const fakeScriptTag = createdScriptTags[0]; expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( 'src', - '/bundles/plugin/plugin-a.bundle.js' + '/bundles/plugin/plugin-a/plugin-a.plugin.js' ); expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('id', 'kbn-plugin-plugin-a'); expect(fakeScriptTag.onload).toBeInstanceOf(Function); @@ -85,7 +85,7 @@ test('`loadPluginBundles` includes the basePath', async () => { const fakeScriptTag = createdScriptTags[0]; expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( 'src', - '/mybasepath/bundles/plugin/plugin-a.bundle.js' + '/mybasepath/bundles/plugin/plugin-a/plugin-a.plugin.js' ); }); @@ -96,7 +96,7 @@ test('`loadPluginBundles` rejects if script.onerror is called', async () => { fakeScriptTag1.onerror(new Error('Whoa there!')); await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin/plugin-a.bundle.js)"` + `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` ); }); @@ -105,7 +105,7 @@ test('`loadPluginBundles` rejects if timeout is reached', async () => { // Override the timeout to 1 ms for testi. loadPluginBundle(addBasePath, 'plugin-a', { timeoutMs: 1 }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin/plugin-a.bundle.js)"` + `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` ); }); @@ -120,6 +120,6 @@ test('`loadPluginBundles` rejects if bundle does attach an initializer to window fakeScriptTag1.onload(); await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin/plugin-a.bundle.js)."` + `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin/plugin-a/plugin-a.plugin.js)."` ); }); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts index 776ed7d7c5570..63aba0dde2af8 100644 --- a/src/core/public/plugins/plugin_loader.ts +++ b/src/core/public/plugins/plugin_loader.ts @@ -74,7 +74,7 @@ export const loadPluginBundle: LoadPluginBundle = < const coreWindow = (window as unknown) as CoreWindow; // Assumes that all plugin bundles get put into the bundles/plugins subdirectory - const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`); + const bundlePath = addBasePath(`/bundles/plugin/${pluginName}/${pluginName}.plugin.js`); script.setAttribute('src', bundlePath); script.setAttribute('id', `kbn-plugin-${pluginName}`); script.setAttribute('async', ''); diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index db363fcd4d751..05a8f40a09a88 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -100,6 +100,11 @@ export class Env { this.binDir = resolve(this.homeDir, 'bin'); this.logDir = resolve(this.homeDir, 'log'); + /** + * BEWARE: this needs to stay roughly synchronized with the @kbn/optimizer + * `packages/kbn-optimizer/src/optimizer_config.ts` determines the paths + * that should be searched for plugins to build + */ this.pluginSearchPaths = [ resolve(this.homeDir, 'src', 'plugins'), ...(options.cliArgs.oss ? [] : [resolve(this.homeDir, 'x-pack', 'plugins')]), diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index 276e3955a4678..e418726465efa 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -17,13 +17,17 @@ * under the License. */ -import apm from 'elastic-apm-node'; - -import { ByteSizeValue } from '@kbn/config-schema'; -import { Server, Request } from 'hapi'; import Url from 'url'; import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; + +import apm from 'elastic-apm-node'; +import { ByteSizeValue } from '@kbn/config-schema'; +import { Server, Request, ResponseToolkit } from 'hapi'; import { sample } from 'lodash'; +import BrowserslistUserAgent from 'browserslist-useragent'; +import * as Rx from 'rxjs'; +import { take } from 'rxjs/operators'; + import { DevConfig } from '../dev'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; @@ -33,9 +37,37 @@ const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); export interface BasePathProxyServerOptions { shouldRedirectFromOldBasePath: (path: string) => boolean; - blockUntil: () => Promise; + delayUntil: () => Rx.Observable; } +// Before we proxy request to a target port we may want to wait until some +// condition is met (e.g. until target listener is ready). +const checkForBrowserCompat = (log: Logger) => async (request: Request, h: ResponseToolkit) => { + if (!request.headers['user-agent'] || process.env.BROWSERSLIST_ENV === 'production') { + return h.continue; + } + + const matches = BrowserslistUserAgent.matchesUA(request.headers['user-agent'], { + env: 'dev', + allowHigherVersions: true, + ignoreMinor: true, + ignorePath: true, + }); + + if (!matches) { + log.warn(` + Request with user-agent [${request.headers['user-agent']}] + seems like it is coming from a browser that is not supported by the dev browserlist. + + Please run Kibana with the environment variable BROWSERSLIST_ENV=production to enable + support for all production browsers (like IE). + + `); + } + + return h.continue; +}; + export class BasePathProxyServer { private server?: Server; private httpsAgent?: HttpsAgent; @@ -108,7 +140,7 @@ export class BasePathProxyServer { } private setupRoutes({ - blockUntil, + delayUntil, shouldRedirectFromOldBasePath, }: Readonly) { if (this.server === undefined) { @@ -122,6 +154,9 @@ export class BasePathProxyServer { }, method: 'GET', path: '/', + options: { + pre: [checkForBrowserCompat(this.log)], + }, }); this.server.route({ @@ -138,11 +173,14 @@ export class BasePathProxyServer { method: '*', options: { pre: [ + checkForBrowserCompat(this.log), // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { apm.setTransactionName(`${request.method.toUpperCase()} /{basePath}/{kbnPath*}`); - await blockUntil(); + await delayUntil() + .pipe(take(1)) + .toPromise(); return responseToolkit.continue; }, ], @@ -172,10 +210,13 @@ export class BasePathProxyServer { method: '*', options: { pre: [ + checkForBrowserCompat(this.log), // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { - await blockUntil(); + await delayUntil() + .pipe(take(1)) + .toPromise(); return responseToolkit.continue; }, ], diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index e8e20580a36db..46436461505c0 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -88,7 +88,7 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', { entryPointPath: 'path/to/plugin/public' }]]), + internal: new Map([['plugin-id', { publicTargetDir: 'path/to/target/public' }]]), browserConfigs: new Map(), }, }, diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 6768e85c8db17..df618b2c0a706 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -22,6 +22,7 @@ import { mockDiscover, mockPackage } from './plugins_service.test.mocks'; import { resolve, join } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@kbn/config-schema'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { ConfigPath, ConfigService, Env } from '../config'; import { rawConfigServiceMock } from '../config/raw_config_service.mock'; @@ -48,6 +49,8 @@ let mockPluginSystem: jest.Mocked; const setupDeps = coreMock.createInternalSetup(); const logger = loggingServiceMock.create(); +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + ['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach(path => { jest.doMock(join(path, 'server'), () => ({}), { virtual: true, @@ -540,10 +543,10 @@ describe('PluginsService', () => { expect(uiPlugins.internal).toMatchInlineSnapshot(` Map { "plugin-1" => Object { - "entryPointPath": "path-1/public", + "publicTargetDir": /path-1/target/public, }, "plugin-2" => Object { - "entryPointPath": "path-2/public", + "publicTargetDir": /path-2/target/public, }, } `); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 5a50cf8ea8ba2..427cc19a8614f 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -17,6 +17,7 @@ * under the License. */ +import Path from 'path'; import { Observable } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; @@ -214,7 +215,9 @@ export class PluginsService implements CoreService ({ elasticsearchUrl: url.format( Object.assign(url.parse(head(_legacyEsConfig.hosts)), { auth: false }) diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/index.ts index 714203de20385..4a609225e6d7f 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/index.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/index.ts @@ -17,13 +17,7 @@ * under the License. */ -import { resolve } from 'path'; - // eslint-disable-next-line import/no-default-export export default function(kibana: any) { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); + return new kibana.Plugin({}); } diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss deleted file mode 100644 index 548e85746f866..0000000000000 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/dashboard_embeddable_container/public/index'; diff --git a/src/legacy/core_plugins/data/index.ts b/src/legacy/core_plugins/data/index.ts index c91500cd545d4..428f0c305a375 100644 --- a/src/legacy/core_plugins/data/index.ts +++ b/src/legacy/core_plugins/data/index.ts @@ -37,7 +37,6 @@ export default function DataPlugin(kibana: any) { uiExports: { interpreter: ['plugins/data/search/expressions/boot'], injectDefaultVars: () => ({}), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, savedObjectsManagement: { query: { diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss deleted file mode 100644 index 22877e217279f..0000000000000 --- a/src/legacy/core_plugins/data/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/data/public/index' diff --git a/src/legacy/core_plugins/embeddable_api/index.ts b/src/legacy/core_plugins/embeddable_api/index.ts index 465e13df10bbc..52206e3d0f105 100644 --- a/src/legacy/core_plugins/embeddable_api/index.ts +++ b/src/legacy/core_plugins/embeddable_api/index.ts @@ -17,14 +17,9 @@ * under the License. */ -import { resolve } from 'path'; import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types'; // eslint-disable-next-line import/no-default-export export default function(kibana: LegacyPluginApi): ArrayOrItem { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); + return new kibana.Plugin({}); } diff --git a/src/legacy/core_plugins/embeddable_api/public/index.scss b/src/legacy/core_plugins/embeddable_api/public/index.scss deleted file mode 100644 index 3f1977b909c31..0000000000000 --- a/src/legacy/core_plugins/embeddable_api/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/embeddable/public/index'; diff --git a/src/legacy/core_plugins/inspector_views/package.json b/src/legacy/core_plugins/inspector_views/package.json deleted file mode 100644 index 74c61c2bcfd2a..0000000000000 --- a/src/legacy/core_plugins/inspector_views/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "inspector_views", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/inspector_views/public/index.scss b/src/legacy/core_plugins/inspector_views/public/index.scss deleted file mode 100644 index d6a076c540f88..0000000000000 --- a/src/legacy/core_plugins/inspector_views/public/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Temporary reference -@import '../../../../plugins/inspector/public/views/index'; diff --git a/src/legacy/core_plugins/interpreter/index.ts b/src/legacy/core_plugins/interpreter/index.ts index db6f17a2960a9..9427a2f8a2d0f 100644 --- a/src/legacy/core_plugins/interpreter/index.ts +++ b/src/legacy/core_plugins/interpreter/index.ts @@ -31,7 +31,6 @@ export default function InterpreterPlugin(kibana: any) { injectDefaultVars: server => ({ serverBasePath: server.config().get('server.basePath'), }), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), }, config: (Joi: any) => { return Joi.object({ diff --git a/src/legacy/core_plugins/interpreter/public/index.scss b/src/legacy/core_plugins/interpreter/public/index.scss deleted file mode 100644 index 360f35020764d..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -@import '../../../../plugins/expressions/public/index'; diff --git a/src/legacy/core_plugins/navigation/package.json b/src/legacy/core_plugins/navigation/package.json deleted file mode 100644 index 8fddb8e6aeced..0000000000000 --- a/src/legacy/core_plugins/navigation/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "navigation", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/navigation/public/index.scss b/src/legacy/core_plugins/navigation/public/index.scss deleted file mode 100644 index 8f2221eb4d4c7..0000000000000 --- a/src/legacy/core_plugins/navigation/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/navigation/public/top_nav_menu/index'; diff --git a/src/legacy/server/sass/build.js b/src/legacy/server/sass/build.js index 0957d52f46422..3d892ce321c2e 100644 --- a/src/legacy/server/sass/build.js +++ b/src/legacy/server/sass/build.js @@ -56,15 +56,7 @@ const makeAsset = (request, { path, root, boundry, copyRoot, urlRoot }) => { }; export class Build { - constructor({ - log, - sourcePath, - targetPath, - urlImports, - theme, - sourceMap = true, - outputStyle = 'nested', - }) { + constructor({ log, sourcePath, targetPath, urlImports, theme }) { this.log = log; this.sourcePath = sourcePath; this.sourceDir = dirname(this.sourcePath); @@ -73,8 +65,6 @@ export class Build { this.urlImports = urlImports; this.theme = theme; this.includedFiles = [sourcePath]; - this.sourceMap = sourceMap; - this.outputStyle = outputStyle; } /** @@ -97,11 +87,11 @@ export class Build { const rendered = await renderSass({ file: this.sourcePath, outFile: this.targetPath, - sourceMap: this.sourceMap, - outputStyle: this.outputStyle, - sourceMapEmbed: this.sourceMap, - includePaths: [resolve(__dirname, '../../../../node_modules')], importer: this.theme === 'dark' ? DARK_THEME_IMPORTER : undefined, + sourceMap: true, + outputStyle: 'nested', + sourceMapEmbed: true, + includePaths: [resolve(__dirname, '../../../../node_modules')], }); const processor = postcss([autoprefixer]); diff --git a/src/legacy/server/sass/build_all.js b/src/legacy/server/sass/build_all.js index d066e52792ca8..1d3d76d1cb01a 100644 --- a/src/legacy/server/sass/build_all.js +++ b/src/legacy/server/sass/build_all.js @@ -21,7 +21,7 @@ import { resolve } from 'path'; import { Build } from './build'; -export async function buildAll({ styleSheets, log, buildDir, sourceMap, outputStyle }) { +export async function buildAll({ styleSheets, log, buildDir }) { const bundles = await Promise.all( styleSheets.map(async styleSheet => { if (!styleSheet.localPath.endsWith('.scss')) { @@ -31,8 +31,6 @@ export async function buildAll({ styleSheets, log, buildDir, sourceMap, outputSt const bundle = new Build({ sourcePath: styleSheet.localPath, log, - sourceMap, - outputStyle, theme: styleSheet.theme, targetPath: resolve(buildDir, styleSheet.publicPath), urlImports: styleSheet.urlImports, diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index 459559e84b1a7..bb246d97bfe4e 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -30,8 +30,6 @@ export const UI_EXPORT_DEFAULTS = { ui: resolve(ROOT, 'src/legacy/ui/public'), __kibanaCore__$: resolve(ROOT, 'src/core/public'), test_harness: resolve(ROOT, 'src/test_harness/public'), - moment$: resolve(ROOT, 'webpackShims/moment'), - 'moment-timezone$': resolve(ROOT, 'webpackShims/moment-timezone'), }, styleSheetPaths: ['light', 'dark'].map(theme => ({ diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 72dd97ff58642..106dbcd9f8ab2 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,5 +1,6 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; +window.__kbnDarkMode__ = {{darkMode}}; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); @@ -12,17 +13,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { loadingMessage.style.display = 'flex'; window.onload = function () { - var files = [ - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js', - {{#each dllJsChunks}} - '{{this}}', - {{/each}} - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', - '{{regularBundlePath}}/commons.bundle.js', - '{{regularBundlePath}}/{{appId}}.bundle.js' - ]; - - var failure = function () { + function failure() { // make subsequent calls to failure() noop failure = function () {}; @@ -37,41 +28,73 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { document.body.innerHTML = err.outerHTML; } - function loadStyleSheet(path) { + function loadStyleSheet(url, cb) { var dom = document.createElement('link'); - dom.addEventListener('error', failure); dom.setAttribute('rel', 'stylesheet'); dom.setAttribute('type', 'text/css'); - dom.setAttribute('href', path); + dom.setAttribute('href', url); + dom.addEventListener('load', cb); document.head.appendChild(dom); } - function createJavascriptElement(path) { + function loadScript(url, cb) { var dom = document.createElement('script'); - - dom.setAttribute('defer', 'defer'); + dom.setAttribute('async', ''); dom.addEventListener('error', failure); - dom.setAttribute('src', file); - dom.addEventListener('load', next); + dom.setAttribute('src', url); + dom.addEventListener('load', cb); document.head.appendChild(dom); } - {{#each styleSheetPaths}} - loadStyleSheet('{{this}}'); - {{/each}} + function load(urlSet, cb) { + if (urlSet.deps) { + load({ urls: urlSet.deps }, function () { + load({ urls: urlSet.urls }, cb); + }); + return; + } - (function next() { - var file = files.shift(); - if (!file) return; + var pending = urlSet.urls.length; + urlSet.urls.forEach(function (url) { + var innerCb = function () { + pending = pending - 1; + if (pending === 0 && typeof cb === 'function') { + cb(); + } + } - var dom = document.createElement('script'); + if (typeof url !== 'string') { + load(url, innerCb); + } else if (url.slice(-4) === '.css') { + loadStyleSheet(url, innerCb); + } else { + loadScript(url, innerCb); + } + }); + } - dom.setAttribute('async', ''); - dom.addEventListener('error', failure); - dom.setAttribute('src', file); - dom.addEventListener('load', next); - document.head.appendChild(dom); - }()); + load({ + deps: [ + { + deps: [ + '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' + ], + urls: [ + {{#each dllJsChunks}} + '{{this}}', + {{/each}} + ] + }, + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', + '{{regularBundlePath}}/commons.bundle.js', + ], + urls: [ + '{{regularBundlePath}}/{{appId}}.bundle.js', + {{#each styleSheetPaths}} + '{{this}}', + {{/each}}, + ] + }); }; } diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 4158af19bd858..21c10bb20962f 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -142,6 +142,7 @@ export function uiRenderMixin(kbnServer, server, config) { dllJsChunks, styleSheetPaths, sharedDepsFilename: UiSharedDeps.distFilename, + darkMode, }, }); diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 539c55c969653..a833204eaa0e2 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -39,6 +39,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); +const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); const BABEL_EXCLUDE_RE = [/[\/\\](webpackShims|node_modules|bower_components)[\/\\]/]; const STATS_WARNINGS_FILTER = new RegExp( [ @@ -62,7 +63,6 @@ export default class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; - this.newPlatformPluginInfo = opts.newPlatformPluginInfo; this.profile = opts.profile || false; this.workers = opts.workers; @@ -147,7 +147,7 @@ export default class BaseOptimizer { return 1; } - return Math.max(1, Math.min(cpus.length - 1, 7)); + return Math.max(1, Math.min(cpus.length - 1, 3)); } getThreadLoaderPoolConfig() { @@ -247,7 +247,6 @@ export default class BaseOptimizer { cache: true, entry: { ...this.uiBundles.toWebpackEntries(), - ...this._getDiscoveredPluginEntryPoints(), light_theme: [require.resolve('../legacy/ui/public/styles/bootstrap_light.less')], dark_theme: [require.resolve('../legacy/ui/public/styles/bootstrap_dark.less')], }, @@ -262,12 +261,6 @@ export default class BaseOptimizer { sourceMapFilename: '[file].map', publicPath: PUBLIC_PATH_PLACEHOLDER, devtoolModuleFilenameTemplate: '[absolute-resource-path]', - - // When the entry point is loaded, assign it's exported `plugin` - // value to a key on the global `__kbnBundles__` object. - // NOTE: Only actually used by new platform plugins - library: ['__kbnBundles__', '[name]'], - libraryExport: 'plugin', }, optimization: { @@ -308,6 +301,11 @@ export default class BaseOptimizer { filename: '[name].style.css', }), + // ignore scss imports in new-platform code that finds its way into legacy bundles + new webpack.NormalModuleReplacementPlugin(/\.scss$/, resource => { + resource.request = EMPTY_MODULE_PATH; + }), + // replace imports for `uiExports/*` modules with a synthetic module // created by create_ui_exports_module.js new webpack.NormalModuleReplacementPlugin(/^uiExports\//, resource => { @@ -396,7 +394,10 @@ export default class BaseOptimizer { 'node_modules', fromRoot('node_modules'), ], - alias: this.uiBundles.getAliases(), + alias: { + ...this.uiBundles.getAliases(), + tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), + }, }, performance: { @@ -524,17 +525,6 @@ export default class BaseOptimizer { ); } - _getDiscoveredPluginEntryPoints() { - // New platform plugin entry points - return [...this.newPlatformPluginInfo.entries()].reduce( - (entryPoints, [pluginId, pluginInfo]) => { - entryPoints[`plugin/${pluginId}`] = pluginInfo.entryPointPath; - return entryPoints; - }, - {} - ); - } - getPresets() { return IS_CODE_COVERAGE ? [ISTANBUL_PRESET_PATH, BABEL_PRESET_PATH] : [BABEL_PRESET_PATH]; } diff --git a/src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js b/src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js new file mode 100644 index 0000000000000..519e301113ff5 --- /dev/null +++ b/src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js @@ -0,0 +1,20 @@ +/* + * 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. + */ + +module.exports = 'BAR'; diff --git a/src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js b/src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js new file mode 100644 index 0000000000000..8c959f9b4779f --- /dev/null +++ b/src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js @@ -0,0 +1,20 @@ +/* + * 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. + */ + +module.exports = '__REPLACE_WITH_PUBLIC_PATH__/FOO'; diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js index f0261d44e0347..0c2e98b5acd63 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.js @@ -21,6 +21,7 @@ import { isAbsolute, extname } from 'path'; import LruCache from 'lru-cache'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { createDynamicAssetResponse } from './dynamic_asset_response'; +import { assertIsNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; /** * Creates the routes that serves files from `bundlesPath` or from @@ -29,6 +30,7 @@ import { createDynamicAssetResponse } from './dynamic_asset_response'; * PUBLIC_PATH_PLACEHOLDER and replaces them with `publicPath`. * * @param {Object} options + * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins * @property {string} options.regularBundlesPath * @property {string} options.dllBundlesPath * @property {string} options.basePublicPath @@ -40,11 +42,13 @@ export function createBundlesRoute({ dllBundlesPath, basePublicPath, builtCssPath, + npUiPluginPublicDirs = [], }) { // rather than calculate the fileHash on every request, we - // provide a cache object to `createDynamicAssetResponse()` that + // provide a cache object to `resolveDynamicAssetResponse()` that // will store the 100 most recently used hashes. const fileHashCache = new LruCache(100); + assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); if (typeof regularBundlesPath !== 'string' || !isAbsolute(regularBundlesPath)) { throw new TypeError( @@ -73,6 +77,14 @@ export function createBundlesRoute({ UiSharedDeps.distDir, fileHashCache ), + ...npUiPluginPublicDirs.map(({ id, path }) => + buildRouteForBundles( + `${basePublicPath}/bundles/plugin/${id}/`, + `/bundles/plugin/${id}/`, + path, + fileHashCache + ) + ), buildRouteForBundles( `${basePublicPath}/bundles/`, '/bundles/', diff --git a/src/optimize/index.js b/src/optimize/index.js index 83825e5a8f386..b7b9f7712358a 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -21,6 +21,8 @@ import FsOptimizer from './fs_optimizer'; import { createBundlesRoute } from './bundles_route'; import { DllCompiler } from './dynamic_dll_plugin'; import { fromRoot } from '../core/server/utils'; +import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; + export default async (kbnServer, server, config) => { if (!config.get('optimize.enabled')) return; @@ -37,13 +39,14 @@ export default async (kbnServer, server, config) => { return await kbnServer.mixin(require('./watch/watch')); } - const { newPlatform, uiBundles } = kbnServer; + const { uiBundles } = kbnServer; server.route( createBundlesRoute({ regularBundlesPath: uiBundles.getWorkingDir(), dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: config.get('server.basePath'), builtCssPath: fromRoot('built_assets/css'), + npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), }) ); @@ -64,7 +67,6 @@ export default async (kbnServer, server, config) => { const optimizer = new FsOptimizer({ logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), uiBundles, - newPlatformPluginInfo: newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/optimize/intentionally_empty_module.js b/src/optimize/intentionally_empty_module.js new file mode 100644 index 0000000000000..9880b336e76e5 --- /dev/null +++ b/src/optimize/intentionally_empty_module.js @@ -0,0 +1,18 @@ +/* + * 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. + */ diff --git a/src/legacy/core_plugins/navigation/index.ts b/src/optimize/np_ui_plugin_public_dirs.js similarity index 53% rename from src/legacy/core_plugins/navigation/index.ts rename to src/optimize/np_ui_plugin_public_dirs.js index 32d5f040760c6..de05fd2b863b8 100644 --- a/src/legacy/core_plugins/navigation/index.ts +++ b/src/optimize/np_ui_plugin_public_dirs.js @@ -17,26 +17,28 @@ * under the License. */ -import { resolve } from 'path'; -import { Legacy } from '../../../../kibana'; +export function getNpUiPluginPublicDirs(kbnServer) { + return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( + ([id, { publicTargetDir }]) => ({ + id, + path: publicTargetDir, + }) + ); +} -// eslint-disable-next-line import/no-default-export -export default function NavigationPlugin(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'navigation', - require: [], - publicDir: resolve(__dirname, 'public'), - config: (Joi: any) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - init: (server: Legacy.Server) => ({}), - uiExports: { - injectDefaultVars: () => ({}), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }; +export function isNpUiPluginPublicDirs(something) { + return ( + Array.isArray(something) && + something.every( + s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' + ) + ); +} - return new kibana.Plugin(config); +export function assertIsNpUiPluginPublicDirs(something) { + if (!isNpUiPluginPublicDirs(something)) { + throw new TypeError( + 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' + ); + } } diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index ed1c51f933eaa..a31ef7229e5da 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -23,6 +23,7 @@ import WatchServer from './watch_server'; import WatchOptimizer, { STATUS } from './watch_optimizer'; import { DllCompiler } from '../dynamic_dll_plugin'; import { WatchCache } from './watch_cache'; +import { getNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; export default async (kbnServer, kibanaHapiServer, config) => { const logWithMetadata = (tags, message, metadata) => @@ -31,7 +32,6 @@ export default async (kbnServer, kibanaHapiServer, config) => { const watchOptimizer = new WatchOptimizer({ logWithMetadata, uiBundles: kbnServer.uiBundles, - newPlatformPluginInfo: kbnServer.newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), @@ -48,7 +48,8 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('optimize.watchHost'), config.get('optimize.watchPort'), config.get('server.basePath'), - watchOptimizer + watchOptimizer, + getNpUiPluginPublicDirs(kbnServer) ); watchOptimizer.status$.subscribe({ diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index a0595816f6a65..6c20f21c7768e 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer { }); } - bindToServer(server, basePath) { + bindToServer(server, basePath, npUiPluginPublicDirs) { // pause all requests received while the compiler is running // and continue once an outcome is reached (aborting the request // with an error if it was a failure). @@ -117,6 +117,7 @@ export default class WatchOptimizer extends BaseOptimizer { server.route( createBundlesRoute({ + npUiPluginPublicDirs: npUiPluginPublicDirs, regularBundlesPath: this.compiler.outputPath, dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, diff --git a/src/optimize/watch/watch_server.js b/src/optimize/watch/watch_server.js index f21db0de61824..74a96dc8aea6e 100644 --- a/src/optimize/watch/watch_server.js +++ b/src/optimize/watch/watch_server.js @@ -21,9 +21,10 @@ import { Server } from 'hapi'; import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins'; export default class WatchServer { - constructor(host, port, basePath, optimizer) { + constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) { this.basePath = basePath; this.optimizer = optimizer; + this.npUiPluginPublicDirs = npUiPluginPublicDirs; this.server = new Server({ host: host, port: port, @@ -34,7 +35,7 @@ export default class WatchServer { async init() { await this.optimizer.init(); - this.optimizer.bindToServer(this.server, this.basePath); + this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs); await this.server.start(); } } diff --git a/src/plugins/console/public/index.scss b/src/plugins/console/public/index.scss new file mode 100644 index 0000000000000..370ec54a85539 --- /dev/null +++ b/src/plugins/console/public/index.scss @@ -0,0 +1 @@ +@import 'styles/index' diff --git a/src/plugins/console/public/index.ts b/src/plugins/console/public/index.ts index 2af9d1d16af02..3fec5ff828065 100644 --- a/src/plugins/console/public/index.ts +++ b/src/plugins/console/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { ConsoleUIPlugin } from './plugin'; export { ConsoleUIPlugin as Plugin }; diff --git a/src/legacy/core_plugins/console_legacy/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss similarity index 100% rename from src/legacy/core_plugins/console_legacy/public/styles/_app.scss rename to src/plugins/console/public/styles/_app.scss diff --git a/src/legacy/core_plugins/console_legacy/public/styles/index.scss b/src/plugins/console/public/styles/_index.scss similarity index 77% rename from src/legacy/core_plugins/console_legacy/public/styles/index.scss rename to src/plugins/console/public/styles/_index.scss index dc45f6cfdacf5..22dc0e5833d2c 100644 --- a/src/legacy/core_plugins/console_legacy/public/styles/index.scss +++ b/src/plugins/console/public/styles/_index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "con" to avoid conflicts. // Examples // conChart diff --git a/src/legacy/core_plugins/console_legacy/public/styles/components/_help.scss b/src/plugins/console/public/styles/components/_help.scss similarity index 100% rename from src/legacy/core_plugins/console_legacy/public/styles/components/_help.scss rename to src/plugins/console/public/styles/components/_help.scss diff --git a/src/legacy/core_plugins/console_legacy/public/styles/components/_history.scss b/src/plugins/console/public/styles/components/_history.scss similarity index 89% rename from src/legacy/core_plugins/console_legacy/public/styles/components/_history.scss rename to src/plugins/console/public/styles/components/_history.scss index efd72245b3c48..5ce5cb52351b8 100644 --- a/src/legacy/core_plugins/console_legacy/public/styles/components/_history.scss +++ b/src/plugins/console/public/styles/components/_history.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/_styling_constants'; - .conHistory { @include euiBottomShadow; padding: $euiSizeM; diff --git a/src/legacy/core_plugins/console_legacy/public/styles/components/_index.scss b/src/plugins/console/public/styles/components/_index.scss similarity index 100% rename from src/legacy/core_plugins/console_legacy/public/styles/components/_index.scss rename to src/plugins/console/public/styles/components/_index.scss diff --git a/src/plugins/dashboard_embeddable_container/public/_index.scss b/src/plugins/dashboard_embeddable_container/public/index.scss similarity index 100% rename from src/plugins/dashboard_embeddable_container/public/_index.scss rename to src/plugins/dashboard_embeddable_container/public/index.scss diff --git a/src/plugins/dashboard_embeddable_container/public/index.ts b/src/plugins/dashboard_embeddable_container/public/index.ts index 73597525105db..e5f55c06b290c 100644 --- a/src/plugins/dashboard_embeddable_container/public/index.ts +++ b/src/plugins/dashboard_embeddable_container/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; import { DashboardEmbeddableContainerPublicPlugin } from './plugin'; diff --git a/src/plugins/data/public/_index.scss b/src/plugins/data/public/index.scss similarity index 100% rename from src/plugins/data/public/_index.scss rename to src/plugins/data/public/index.scss diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 548417f3769aa..8704ca08ae905 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; /* diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss index 21ba32ec6a6fe..3d416aade9a53 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss @@ -1 +1 @@ -@import 'filter_editor'; \ No newline at end of file +@import 'filter_editor'; diff --git a/src/plugins/embeddable/public/_index.scss b/src/plugins/embeddable/public/index.scss similarity index 100% rename from src/plugins/embeddable/public/_index.scss rename to src/plugins/embeddable/public/index.scss diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index af6c2acd3a9b1..b0e14a04a9944 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from 'src/core/public'; import { EmbeddablePublicPlugin } from './plugin'; diff --git a/src/plugins/expressions/public/_index.scss b/src/plugins/expressions/public/index.scss similarity index 100% rename from src/plugins/expressions/public/_index.scss rename to src/plugins/expressions/public/index.scss diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 59d529dc9caff..5f64c11f4efe6 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; diff --git a/src/plugins/inspector/public/index.scss b/src/plugins/inspector/public/index.scss new file mode 100644 index 0000000000000..57820cf70cc3f --- /dev/null +++ b/src/plugins/inspector/public/index.scss @@ -0,0 +1 @@ +@import 'views/index' diff --git a/src/plugins/inspector/public/index.ts b/src/plugins/inspector/public/index.ts index e90e05aa2830a..bf06ab88fa79a 100644 --- a/src/plugins/inspector/public/index.ts +++ b/src/plugins/inspector/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; import { InspectorPublicPlugin } from './plugin'; diff --git a/src/plugins/navigation/public/index.scss b/src/plugins/navigation/public/index.scss new file mode 100644 index 0000000000000..4734a2915c620 --- /dev/null +++ b/src/plugins/navigation/public/index.scss @@ -0,0 +1 @@ +@import "top_nav_menu/index"; diff --git a/src/plugins/navigation/public/index.ts b/src/plugins/navigation/public/index.ts index 1c0a36c597ce7..5afc91c4445e8 100644 --- a/src/plugins/navigation/public/index.ts +++ b/src/plugins/navigation/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; export function plugin(initializerContext: PluginInitializerContext) { return new NavigationPublicPlugin(initializerContext); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 91495c4024f3a..9679636a39d23 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -69,7 +69,8 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider return window.__RENDERING_SESSION__; }); - describe('rendering service', () => { + // Talked to @dover, he aggreed we can skip these tests that are unexpectedly flaky + describe.skip('rendering service', () => { it('renders "core" application', async () => { await navigateTo('/render/core'); diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 2605655ed7e7a..a7c05b6e5802d 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,6 +2,15 @@ source src/dev/ci_setup/setup_env.sh +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --verbose; + +# doesn't persist, also set in kibanaPipeline.groovy +export KBN_NP_PLUGINS_BUILT=true + echo " -> downloading es snapshot" node scripts/es snapshot --license=oss --download-only; diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 20b12b302cb39..f87d6e1102c45 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,6 +3,14 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --verbose; + +# doesn't persist, also set in kibanaPipeline.groovy +export KBN_NP_PLUGINS_BUILT=true + echo " -> downloading es snapshot" node scripts/es snapshot --download-only; diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index dd66586e912d6..dd2e626d1c860 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -87,6 +87,7 @@ def getPostBuildWorker(name, closure) { "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "IS_PIPELINE_JOB=1", + "KBN_NP_PLUGINS_BUILT=true", ]) { closure() } diff --git a/x-pack/index.js b/x-pack/index.js index ecb71f26c1609..858c3e8b68d18 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -9,10 +9,8 @@ import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; -import { searchprofiler } from './legacy/plugins/searchprofiler'; import { ml } from './legacy/plugins/ml'; import { tilemap } from './legacy/plugins/tilemap'; -import { watcher } from './legacy/plugins/watcher'; import { grokdebugger } from './legacy/plugins/grokdebugger'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { logstash } from './legacy/plugins/logstash'; @@ -49,10 +47,8 @@ module.exports = function(kibana) { reporting(kibana), spaces(kibana), security(kibana), - searchprofiler(kibana), ml(kibana), tilemap(kibana), - watcher(kibana), grokdebugger(kibana), dashboardMode(kibana), logstash(kibana), diff --git a/x-pack/legacy/plugins/apm/cypress/package.json b/x-pack/legacy/plugins/apm/cypress/package.json index ef8955fcbd1b0..59f76ba250ad7 100644 --- a/x-pack/legacy/plugins/apm/cypress/package.json +++ b/x-pack/legacy/plugins/apm/cypress/package.json @@ -16,6 +16,6 @@ "p-limit": "^2.2.1", "ts-loader": "^6.1.0", "typescript": "3.7.2", - "webpack": "^4.40.2" + "webpack": "^4.41.5" } } diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js index c711f9510a10b..0ce722eb90d43 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js @@ -99,16 +99,19 @@ module.exports = { { loader: 'css-loader', options: { - modules: true, - localIdentName: '[name]__[local]___[hash:base64:5]', - camelCase: true, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + localsConvention: 'camelCase', sourceMap: !isProd, }, }, { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, }, }, { diff --git a/x-pack/legacy/plugins/searchprofiler/index.ts b/x-pack/legacy/plugins/searchprofiler/index.ts deleted file mode 100644 index fab2e43847348..0000000000000 --- a/x-pack/legacy/plugins/searchprofiler/index.ts +++ /dev/null @@ -1,27 +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 { resolve } from 'path'; - -// TODO: -// Until we can process SCSS in new platform, this part of Searchprofiler -// legacy must remain here. - -export const searchprofiler = (kibana: any) => { - const publicSrc = resolve(__dirname, 'public'); - - return new kibana.Plugin({ - require: ['elasticsearch', 'xpack_main'], - id: 'searchprofiler', - configPrefix: 'xpack.searchprofiler', - publicDir: publicSrc, - - uiExports: { - styleSheetPaths: `${publicSrc}/index.scss`, - }, - init() {}, - }); -}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/index.scss b/x-pack/legacy/plugins/searchprofiler/public/index.scss deleted file mode 100644 index e04e81c023196..0000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Search profiler plugin styles - -// Prefix all styles with "prfDevTool" to avoid conflicts. -// Examples -// prfDevTool__ -// prfDevTool__cell -// prfDevTool__shardDetails - -@import 'styles/index'; diff --git a/x-pack/legacy/plugins/security/public/index.scss b/x-pack/legacy/plugins/security/public/index.scss index 187ad5231534d..0050d01a52493 100644 --- a/x-pack/legacy/plugins/security/public/index.scss +++ b/x-pack/legacy/plugins/security/public/index.scss @@ -7,14 +7,9 @@ // secChart__legend--small // secChart__legend-isLoading -$secFormWidth: 460px; - // Public components @import './components/index'; // Public views @import './views/index'; -// Styles of Kibana Platform plugin -@import '../../../../plugins/security/public/index'; - diff --git a/x-pack/legacy/plugins/watcher/index.ts b/x-pack/legacy/plugins/watcher/index.ts deleted file mode 100644 index fdf9ba1bad6e4..0000000000000 --- a/x-pack/legacy/plugins/watcher/index.ts +++ /dev/null @@ -1,19 +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 { resolve } from 'path'; - -const pluginDefinition = { - id: 'watcher', - configPrefix: 'xpack.watcher', - publicDir: resolve(__dirname, 'public'), - require: ['kibana'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - init(server: any) {}, -}; - -export const watcher = (kibana: any) => new kibana.Plugin(pluginDefinition); diff --git a/x-pack/package.json b/x-pack/package.json index c1225f609ebbb..43df763c22bdc 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -153,7 +153,7 @@ "react-docgen-typescript-loader": "^3.1.1", "react-test-renderer": "^16.12.0", "rxjs-marbles": "^5.0.3", - "sass-loader": "^7.3.1", + "sass-loader": "^8.0.2", "sass-resources-loader": "^2.0.1", "simple-git": "1.116.0", "sinon": "^7.4.2", @@ -346,7 +346,7 @@ "uuid": "3.3.2", "venn.js": "0.2.20", "vscode-languageserver": "^5.2.1", - "webpack": "4.41.0", + "webpack": "^4.41.5", "wellknown": "^0.5.0", "xml2js": "^0.4.22", "xregexp": "4.2.4" diff --git a/x-pack/plugins/searchprofiler/public/README.md b/x-pack/plugins/searchprofiler/public/README.md deleted file mode 100644 index 3cf79162b3965..0000000000000 --- a/x-pack/plugins/searchprofiler/public/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Please note - -See x-pack/legacy/plugins/searchprofiler/public for styles. diff --git a/x-pack/plugins/searchprofiler/public/index.scss b/x-pack/plugins/searchprofiler/public/index.scss new file mode 100644 index 0000000000000..370ec54a85539 --- /dev/null +++ b/x-pack/plugins/searchprofiler/public/index.scss @@ -0,0 +1 @@ +@import 'styles/index' diff --git a/x-pack/plugins/searchprofiler/public/index.ts b/x-pack/plugins/searchprofiler/public/index.ts index 3d77f703b42cd..33952a747018e 100644 --- a/x-pack/plugins/searchprofiler/public/index.ts +++ b/x-pack/plugins/searchprofiler/public/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import './styles/_index.scss'; import { PluginInitializerContext } from 'src/core/public'; import { SearchProfilerUIPlugin } from './plugin'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/_index.scss b/x-pack/plugins/searchprofiler/public/styles/_index.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/_index.scss rename to x-pack/plugins/searchprofiler/public/styles/_index.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/_mixins.scss b/x-pack/plugins/searchprofiler/public/styles/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/_mixins.scss rename to x-pack/plugins/searchprofiler/public/styles/_mixins.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss b/x-pack/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss rename to x-pack/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/components/_percentage_badge.scss b/x-pack/plugins/searchprofiler/public/styles/components/_percentage_badge.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/components/_percentage_badge.scss rename to x-pack/plugins/searchprofiler/public/styles/components/_percentage_badge.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/components/_profile_tree.scss b/x-pack/plugins/searchprofiler/public/styles/components/_profile_tree.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/components/_profile_tree.scss rename to x-pack/plugins/searchprofiler/public/styles/components/_profile_tree.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/containers/_main.scss b/x-pack/plugins/searchprofiler/public/styles/containers/_main.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/containers/_main.scss rename to x-pack/plugins/searchprofiler/public/styles/containers/_main.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss b/x-pack/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss rename to x-pack/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss diff --git a/x-pack/plugins/security/public/_index.scss b/x-pack/plugins/security/public/index.scss similarity index 68% rename from x-pack/plugins/security/public/_index.scss rename to x-pack/plugins/security/public/index.scss index 9fa81bad7c3f4..1bdb8cc178fdf 100644 --- a/x-pack/plugins/security/public/_index.scss +++ b/x-pack/plugins/security/public/index.scss @@ -1,2 +1,4 @@ +$secFormWidth: 460px; + // Management styles @import './management/index'; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 712f49afd306e..1c525dc6b9187 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import './index.scss'; import { PluginInitializer } from 'src/core/public'; import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; diff --git a/x-pack/legacy/plugins/watcher/public/index.scss b/x-pack/plugins/watcher/public/index.scss similarity index 80% rename from x-pack/legacy/plugins/watcher/public/index.scss rename to x-pack/plugins/watcher/public/index.scss index 33ebf21326c7b..101db14aee9e6 100644 --- a/x-pack/legacy/plugins/watcher/public/index.scss +++ b/x-pack/plugins/watcher/public/index.scss @@ -1,6 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - // Watcher plugin styles // Prefix all styles with "watcher" to avoid conflicts. diff --git a/x-pack/plugins/watcher/public/index.ts b/x-pack/plugins/watcher/public/index.ts index ff635579316e5..783668285e74a 100644 --- a/x-pack/plugins/watcher/public/index.ts +++ b/x-pack/plugins/watcher/public/index.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './index.scss'; import { WatcherUIPlugin } from './plugin'; export const plugin = () => new WatcherUIPlugin(); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index c780a8efae304..097812c576e92 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -80,9 +80,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('managementHome', { - timeout: 10000, - }); + await testSubjects.existOrFail('managementHome'); }); }); }); diff --git a/yarn.lock b/yarn.lock index be4b185b7b77f..1158fce12829e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2399,6 +2399,16 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" +"@jest/types@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" + integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@jimp/bmp@^0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.8.4.tgz#3246e0c6b073b3e2d9b61075ac0146d9124c9277" @@ -4094,6 +4104,11 @@ resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.56.tgz#20124077bd44061e018c7283c0bb83f4b00322dd" integrity sha512-HxtqilvklZ7i6XOaiP7uIJIrFXEVEhfbSY45nfv2DeBRngncI58Y4ZOUMiUkcT8sqgLL1ablmbfylChUg7A3GA== +"@types/anymatch@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" + integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== + "@types/archiver@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-3.0.0.tgz#c0a53e0ed3b7aef626ce683d081d7821d8c638b4" @@ -4206,6 +4221,11 @@ resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.1.tgz#a21e21ba08cc49d17b26baef98e1a77ee4d6cdb0" integrity sha512-kOiap+kSa4DPoookJXQGQyKy1rjZ55tgfKAh9F0m1NUdukkcwVzpSnXPMH42a5L+U++ugdQlh/xFJu/WAdr1aw== +"@types/browserslist-useragent@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53" + integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w== + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -4265,6 +4285,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.0.tgz#926f76f7e66f49cc59ad880bb15b030abbf0b66d" integrity sha512-gZ/Rb+MFXF0pXSEQxdRoPMm5jeO3TycjOdvbpbcpHX/B+n9AqaHFe5q6Ga9CsZ7ir/UgIWPfrBzUzn3F19VH/w== +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/color@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" @@ -4473,6 +4498,13 @@ "@types/glob" "*" fast-glob "^2.0.2" +"@types/graceful-fs@*": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" + integrity sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ== + dependencies: + "@types/node" "*" + "@types/graphql@^0.13.2": version "0.13.4" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.13.4.tgz#55ae9c29f0fd6b85ee536f5c72b4769d5c5e06b1" @@ -4681,6 +4713,14 @@ "@types/node" "*" rxjs "^6.5.1" +"@types/loader-utils@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-1.1.3.tgz#82b9163f2ead596c68a8c03e450fbd6e089df401" + integrity sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg== + dependencies: + "@types/node" "*" + "@types/webpack" "*" + "@types/lodash.clonedeep@^4.5.4": version "4.5.4" resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.4.tgz#2515c5f08bc95afebfb597711871b0497f5d7da7" @@ -5071,13 +5111,6 @@ dependencies: "@types/normalize-package-data" "*" -"@types/recompose@^0.30.5": - version "0.30.5" - resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.5.tgz#09890e3c504546b38193479e610e427ac0888393" - integrity sha512-PEQvFmudB9n0+ZvD8l7lh0olGAWmVAuVwCM4eotzWouH8/Kcr8/EcZyLhYILqoTlqzi6ey/3kbKQzJ/h3KkyXw== - dependencies: - "@types/react" "*" - "@types/recompose@^0.30.6": version "0.30.6" resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.6.tgz#f6ffae2008b84df916ed6633751f9287f344ea3e" @@ -5139,6 +5172,11 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -5204,6 +5242,11 @@ dependencies: "@types/superagent" "*" +"@types/tapable@*": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" + integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ== + "@types/tar-fs@^1.16.1": version "1.16.1" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.1.tgz#6e3fba276c173e365ae91e55f7b797a0e64298e5" @@ -5259,6 +5302,13 @@ resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6" integrity sha512-0+S1S9Iq0oJ9w9IaBC5W/z1WsPNDUIAJG+THGmqR4vUAxUPCzIY+dApTvyGsaBUWjafTDL0Dg8Z9+iRuk3/BQA== +"@types/uglify-js@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" + integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== + dependencies: + source-map "^0.6.1" + "@types/undertaker-registry@*": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da" @@ -5321,11 +5371,41 @@ dependencies: "@types/node" "*" +"@types/watchpack@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/watchpack/-/watchpack-1.1.5.tgz#e5622eb2a49e2239d94d8882275fbc7893147e97" + integrity sha512-9clzOLesGBv5/60QQ3UvpOPsRSNu4ybw4jUBq1aofGdA2NtS5dL2D/m6WAXycxdg+rcGOHTN2rgpTMAdJ4jMWg== + dependencies: + "@types/graceful-fs" "*" + "@types/node" "*" + chokidar "^2.1.2" + "@types/webpack-env@^1.13.7": version "1.14.1" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.14.1.tgz#0d8a53f308f017c53a5ddc3d07f4d6fa76b790d7" integrity sha512-0Ki9jAAhKDSuLDXOIMADg54Hu60SuBTEsWaJGGy5cV+SSUQ63J2a+RrYYGrErzz39fXzTibhKrAQJAb8M7PNcA== +"@types/webpack-sources@*": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" + integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + +"@types/webpack@*", "@types/webpack@^4.4.31", "@types/webpack@^4.41.3": + version "4.41.3" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.3.tgz#30c2251db1d69a45bbffd79c0577dd9baf50e7ba" + integrity sha512-dH+BZ6pHBZFrXpnif0YU/PbmUq3lQrvRPnqkxsciSIzvG/DE+Vm/Wrjn56T7V3+B5ryQa5fw0oGnHL8tk4ll6w== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + "@types/webpack-sources" "*" + source-map "^0.6.0" + "@types/wrap-ansi@^2.0.15": version "2.0.15" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-2.0.15.tgz#87affc11a46864cb6853b642e89363633d544aa7" @@ -5373,6 +5453,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^15.0.0": + version "15.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.3.tgz#41453a0bc7ab393e995d1f5451455638edbd2baf" + integrity sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ== + dependencies: + "@types/yargs-parser" "*" + "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -5693,11 +5780,6 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - acorn-globals@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" @@ -5772,11 +5854,6 @@ acorn@^6.0.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== -acorn@^6.0.5: - version "6.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784" - integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== - acorn@^6.2.1: version "6.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" @@ -6195,6 +6272,14 @@ ansi-styles@^3.2.0: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + ansi-styles@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" @@ -6968,18 +7053,18 @@ autobind-decorator@^1.3.4: resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.4.3.tgz#4c96ffa77b10622ede24f110f5dbbf56691417d1" integrity sha1-TJb/p3sQYi7eJPEQ9du/VmkUF9E= -autoprefixer@9.6.1, autoprefixer@^9.4.9: - version "9.6.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" - integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== +autoprefixer@^9.4.9, autoprefixer@^9.7.4: + version "9.7.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378" + integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g== dependencies: - browserslist "^4.6.3" - caniuse-lite "^1.0.30000980" + browserslist "^4.8.3" + caniuse-lite "^1.0.30001020" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.17" - postcss-value-parser "^4.0.0" + postcss "^7.0.26" + postcss-value-parser "^4.0.2" await-event@^2.1.0: version "2.1.0" @@ -7701,11 +7786,6 @@ big-time@2.x.x: resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" integrity sha1-aMffjcMPl+lT8lpnp2rJcTwWyd4= -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -8160,6 +8240,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" +browserslist-useragent@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.0.2.tgz#f0e209b2742baa5de0e451b52e678e8b4402617c" + integrity sha512-/UPzK9xZnk5mwwWx4wcuBKAKx/mD3MNY8sUuZ2NPqnr4RVFWZogX+8mOP0cQEYo8j78sHk0hiDNaVXZ1U3hM9A== + dependencies: + browserslist "^4.6.6" + semver "^6.3.0" + useragent "^2.3.0" + browserslist@4.6.6: version "4.6.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" @@ -8169,14 +8258,14 @@ browserslist@4.6.6: electron-to-chromium "^1.3.191" node-releases "^1.1.25" -browserslist@^4.6.0, browserslist@^4.6.3: - version "4.6.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.4.tgz#fd0638b3f8867fec2c604ed0ed9300379f8ec7c2" - integrity sha512-ErJT8qGfRt/VWHSr1HeqZzz50DvxHtr1fVL1m5wf20aGrG8e1ce8fpZ2EjZEfs09DDZYSvtRaDlMpWslBf8Low== +browserslist@^4.6.0, browserslist@^4.6.6, browserslist@^4.8.3: + version "4.8.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.5.tgz#691af4e327ac877b25e7a3f7ee869c4ef36cdea3" + integrity sha512-4LMHuicxkabIB+n9874jZX/az1IaZ5a+EUuvD7KFOu9x/Bd5YHyO0DIz2ls/Kl8g0ItS4X/ilEgf4T1Br0lgSg== dependencies: - caniuse-lite "^1.0.30000981" - electron-to-chromium "^1.3.188" - node-releases "^1.1.25" + caniuse-lite "^1.0.30001022" + electron-to-chromium "^1.3.338" + node-releases "^1.1.46" bser@^2.0.0: version "2.0.0" @@ -8565,11 +8654,6 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -camelcase@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" - integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ== - camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -8585,10 +8669,10 @@ can-use-dom@^0.1.0: resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= -caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000984: - version "1.0.30001016" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001016.tgz#16ea48d7d6e8caf3cad3295c2d746fe38c4e7f66" - integrity sha512-yYQ2QfotceRiH4U+h1Us86WJXtVHDmy3nEKIdYPsZCYnOV5/tMgGbmoIlrMzmh2VXlproqYtVaKeGDBkMZifFA== +caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022: + version "1.0.30001022" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6" + integrity sha512-FjwPPtt/I07KyLPkBQ0g7/XuZg6oUkYBVnPHNj3VHJbOjmmJ/GdSo/GUY6MwINEQvjhP6WZVbX8Tvms8xh0D5A== capture-exit@^2.0.0: version "2.0.0" @@ -8728,6 +8812,14 @@ chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^5.2.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -8948,7 +9040,7 @@ chroma-js@^2.0.4: dependencies: cross-env "^6.0.3" -chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: +chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== @@ -9044,6 +9136,14 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz#a99d8ec34c1c628a4541567aa7b457446460c62b" + integrity sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A== + dependencies: + "@types/webpack" "^4.4.31" + del "^4.1.1" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -9349,12 +9449,19 @@ color-convert@^1.9.1: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@^1.1.1: +color-name@^1.0.0, color-name@^1.1.1, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -9985,7 +10092,7 @@ cosmiconfig@^5.2.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cp-file@^6.1.0, cp-file@^6.2.0: +cp-file@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== @@ -9996,15 +10103,28 @@ cp-file@^6.1.0, cp-file@^6.2.0: pify "^4.0.1" safe-buffer "^5.0.1" -cpy@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/cpy/-/cpy-7.3.0.tgz#62f2847986b4ff9d029710568a49e9a9ab5a210e" - integrity sha512-auvDu6h/J+cO1uqV40ymL/VoPM0+qPpNGaNttTzkYVXO/+GeynuyAK/MwFcWgU/P82ezcZw7RaN34CIIWajKLA== +cp-file@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" + integrity sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw== dependencies: - arrify "^1.0.1" - cp-file "^6.1.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + nested-error-stacks "^2.0.0" + p-event "^4.1.0" + +cpy@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.0.0.tgz#8195db0db19a9ea6aa4f229784cbf3e3f53c3158" + integrity sha512-iTjLUqtVr45e17GFAyxA0lqFinbGMblMCTtAqrPzT/IETNtDuyyhDDk8weEZ08MiCc6EcuyNq2KtGH5J2BIAoQ== + dependencies: + arrify "^2.0.1" + cp-file "^7.0.0" globby "^9.2.0" + is-glob "^4.0.1" + junk "^3.1.0" nested-error-stacks "^2.1.0" + p-all "^2.1.0" crc32-stream@^3.0.1: version "3.0.1" @@ -10282,40 +10402,23 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" -css-loader@2.1.1, css-loader@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" - integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== - dependencies: - camelcase "^5.2.0" - icss-utils "^4.1.0" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.14" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^2.0.6" - postcss-modules-scope "^2.1.0" - postcss-modules-values "^2.0.0" - postcss-value-parser "^3.3.0" - schema-utils "^1.0.0" - -css-loader@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.0.tgz#bb570d89c194f763627fcf1f80059c6832d009b2" - integrity sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ== +css-loader@^3.0.0, css-loader@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.2.tgz#d3fdb3358b43f233b78501c5ed7b1c6da6133202" + integrity sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.17" + postcss "^7.0.23" postcss-modules-extract-imports "^2.0.0" postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.1.0" + postcss-modules-scope "^2.1.1" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.0" - schema-utils "^2.0.0" + postcss-value-parser "^4.0.2" + schema-utils "^2.6.0" css-select-base-adapter@^0.1.1: version "0.1.1" @@ -11448,6 +11551,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" + integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== + diff@3.5.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -11918,15 +12026,10 @@ elasticsearch@^16.4.0, elasticsearch@^16.5.0: chalk "^1.0.0" lodash "^4.17.10" -electron-to-chromium@^1.3.188: - version "1.3.190" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.190.tgz#5bf599519983bfffd9d4387817039a3ed7ca085f" - integrity sha512-cs9WnTnGBGnYYVFMCtLmr9jXNTOkdp95RLz5VhwzDn7dErg1Lnt9o4d01gEH69XlmRKWUr91Yu1hA+Hi8qW0PA== - -electron-to-chromium@^1.3.191: - version "1.3.246" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.246.tgz#38c30a380398b293f39a19d4346f18e2cb376b72" - integrity sha512-CzR7VM16UmZQVgd5I5qu/rx0e67l6FF17rpJD2kRFX9n1ygHFIS+TV9DO55MSZKBGVuQ0Ph1JLLTFEReCKU6nQ== +electron-to-chromium@^1.3.191, electron-to-chromium@^1.3.338: + version "1.3.340" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.340.tgz#5d4fe78e984d4211194cf5a52e08069543da146f" + integrity sha512-hRFBAglhcj5iVYH+o8QU0+XId1WGoc0VGowJB1cuJAt3exHGrivZvWeAO5BRgBZqwZtwxjm8a5MQeGoT/Su3ww== elegant-spinner@^1.0.1: version "1.0.1" @@ -12641,7 +12744,7 @@ eslint-rule-composer@^0.3.0: resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== -eslint-scope@^4.0.0, eslint-scope@^4.0.3: +eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== @@ -13730,16 +13833,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" - integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA== - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^3.0.0" - -find-cache-dir@^2.1.0: +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -16349,11 +16443,6 @@ iconv-lite@^0.5.0: dependencies: safer-buffer ">= 2.1.2 < 3" -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - icss-utils@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.0.0.tgz#d52cf4bcdcfa1c45c2dbefb4ffdf6b00ef608098" @@ -16361,13 +16450,6 @@ icss-utils@^4.0.0: dependencies: postcss "^7.0.5" -icss-utils@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.0.tgz#339dbbffb9f8729a243b701e1c29d4cc58c52f0e" - integrity sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ== - dependencies: - postcss "^7.0.14" - icss-utils@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" @@ -16813,16 +16895,11 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0, interpret@^1.1.0, interpret@^1.2.0: +interpret@1.2.0, interpret@^1.0.0, interpret@^1.1.0, interpret@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -interpret@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" - integrity sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ= - intl-format-cache@^2.0.5, intl-format-cache@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.1.0.tgz#04a369fecbfad6da6005bae1f14333332dcf9316" @@ -17384,9 +17461,9 @@ is-path-inside@^2.1.0: path-is-inside "^1.0.2" is-path-inside@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.1.tgz#7417049ed551d053ab82bba3fdd6baa6b3a81e89" - integrity sha512-CKstxrctq1kUesU6WhtZDbYKzzYBuRH0UYInAVrkc/EYdB9ltbfE0gOoayG9nhohG6447sOOVGhHqsdmBvkbNg== + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" @@ -17664,7 +17741,7 @@ isstream@0.1.x, isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-instrumenter-loader@3.0.1: +istanbul-instrumenter-loader@3.0.1, istanbul-instrumenter-loader@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz#9957bd59252b373fae5c52b7b5188e6fde2a0949" integrity sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w== @@ -17900,6 +17977,16 @@ jest-diff@^24.0.0: jest-get-type "^24.0.0" pretty-format "^24.0.0" +jest-diff@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" + integrity sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.1.0" + jest-get-type "^25.1.0" + pretty-format "^25.1.0" + jest-docblock@^24.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" @@ -17951,6 +18038,11 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" + integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -18487,11 +18579,6 @@ json3@^3.3.2: resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -18663,6 +18750,11 @@ jszip@^3.1.5: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" +junk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" + integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== + just-curry-it@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/just-curry-it/-/just-curry-it-3.1.0.tgz#ab59daed308a58b847ada166edd0a2d40766fbc5" @@ -19329,12 +19421,12 @@ load-source-map@^1.0.0: semver "^5.3.0" source-map "^0.5.6" -loader-runner@^2.3.0, loader-runner@^2.3.1, loader-runner@^2.4.0: +loader-runner@^2.3.1, loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3, loader-utils@^1.0.4, loader-utils@^1.2.3: +loader-utils@1.2.3, loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -19343,15 +19435,6 @@ loader-utils@1.2.3, loader-utils@^1.0.4, loader-utils@^1.2.3: emojis-list "^2.0.0" json5 "^1.0.1" -loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -20239,10 +20322,10 @@ memoizerific@^1.11.3: memory-fs@^0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" integrity sha1-8rslNovBIeORwlIN6Slpyu4KApA= -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -20346,7 +20429,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -20983,16 +21066,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.13.2: +nan@^2.13.2, nan@^2.9.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== -nan@^2.9.2: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== - nano-css@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332" @@ -21323,7 +21401,7 @@ node-jose@1.1.0: util "^0.11.0" vm-browserify "0.0.4" -node-libs-browser@^2.0.0, node-libs-browser@^2.2.1: +node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== @@ -21384,14 +21462,14 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.25: - version "1.1.25" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.25.tgz#0c2d7dbc7fed30fbe02a9ee3007b8c90bf0133d3" - integrity sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ== +node-releases@^1.1.25, node-releases@^1.1.46: + version "1.1.47" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" + integrity sha512-k4xjVPx5FpwBUj0Gw7uvFOTF4Ep8Hok1I6qjwL3pLfwe7Y0REQSAqOwwv9TWBCUtMHxcXfY4PgRLRozcChvTcA== dependencies: - semver "^5.3.0" + semver "^6.3.0" -node-sass@^4.13.1: +node-sass@^4.13.0, node-sass@^4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== @@ -22188,6 +22266,13 @@ output-file-sync@^2.0.0: is-plain-obj "^1.1.0" mkdirp "^0.5.1" +p-all@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-all/-/p-all-2.1.0.tgz#91419be56b7dee8fe4c5db875d55e0da084244a0" + integrity sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA== + dependencies: + p-map "^2.0.0" + p-any@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-any/-/p-any-1.1.0.tgz#1d03835c7eed1e34b8e539c47b7b60d0d015d4e1" @@ -22217,6 +22302,13 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" +p-event@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" + integrity sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA== + dependencies: + p-timeout "^2.0.1" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -23086,7 +23178,7 @@ postcss-load-config@^2.0.0: cosmiconfig "^4.0.0" import-cwd "^2.0.0" -postcss-loader@3.0.0, postcss-loader@^3.0.0: +postcss-loader@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== @@ -23103,15 +23195,6 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" -postcss-modules-local-by-default@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" - integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - postcss-value-parser "^3.3.1" - postcss-modules-local-by-default@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" @@ -23122,22 +23205,14 @@ postcss-modules-local-by-default@^3.0.2: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.0" -postcss-modules-scope@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" - integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== +postcss-modules-scope@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz#33d4fc946602eb5e9355c4165d68a10727689dba" + integrity sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ== dependencies: postcss "^7.0.6" postcss-selector-parser "^6.0.0" -postcss-modules-values@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" - integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^7.0.6" - postcss-modules-values@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" @@ -23173,7 +23248,7 @@ postcss-url@^8.0.0: postcss "^7.0.2" xxhashjs "^0.2.1" -postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: +postcss-value-parser@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== @@ -23197,19 +23272,10 @@ postcss-values-parser@^1.5.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" - integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.16: - version "7.0.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" - integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== +postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.2, postcss@^7.0.23, postcss@^7.0.26, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587" + integrity sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -23304,6 +23370,16 @@ pretty-format@^24.3.0, pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" + integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== + dependencies: + "@jest/types" "^25.1.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-hrtime@^1.0.0, pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -24351,7 +24427,7 @@ react-is@^16.10.2, react-is@^16.9.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== -react-is@^16.3.1: +react-is@^16.12.0, react-is@^16.3.1: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== @@ -26367,15 +26443,15 @@ sass-lint@^1.12.1: path-is-absolute "^1.0.0" util "^0.10.3" -sass-loader@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f" - integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA== +sass-loader@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" + integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== dependencies: clone-deep "^4.0.1" - loader-utils "^1.0.1" - neo-async "^2.5.0" - pify "^4.0.1" + loader-utils "^1.2.3" + neo-async "^2.6.1" + schema-utils "^2.6.1" semver "^6.3.0" sass-lookup@^3.0.0: @@ -26455,7 +26531,7 @@ schema-utils@^2.0.0, schema-utils@^2.0.1: ajv "^6.1.0" ajv-keywords "^3.1.0" -schema-utils@^2.4.1, schema-utils@^2.6.4: +schema-utils@^2.4.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.1, schema-utils@^2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53" integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ== @@ -28026,7 +28102,7 @@ style-it@^2.1.3: dependencies: react-lib-adler32 "^1.0.3" -style-loader@0.23.1, style-loader@^0.23.1: +style-loader@^0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== @@ -28034,6 +28110,14 @@ style-loader@0.23.1, style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" +style-loader@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.1.3.tgz#9e826e69c683c4d9bf9db924f85e9abb30d5e200" + integrity sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.6.4" + styled-components@^3: version "3.4.10" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.10.tgz#9a654c50ea2b516c36ade57ddcfa296bf85c96e1" @@ -28209,6 +28293,13 @@ supports-color@^7.0.0: dependencies: has-flag "^4.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -28524,36 +28615,50 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" -terser-webpack-plugin@^1.1.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" - integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== +terser-webpack-plugin@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" + integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^2.1.2" + serialize-javascript "^1.7.0" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser-webpack-plugin@^1.2.4, terser-webpack-plugin@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" - integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== +terser-webpack-plugin@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" + integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^1.7.0" + serialize-javascript "^2.1.2" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" +terser-webpack-plugin@^2.1.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.2.tgz#6d3d1b0590c8f729bfbaeb7fb2528b8b62db4c74" + integrity sha512-SmvB/6gtEPv+CJ88MH5zDOsZdKXPS/Uzv2//e90+wM1IHFUhsguPKEILgzqrM1nQ4acRXN/SV4Obr55SXC+0oA== + dependencies: + cacache "^13.0.1" + find-cache-dir "^3.2.0" + jest-worker "^24.9.0" + schema-utils "^2.6.1" + serialize-javascript "^2.1.2" + source-map "^0.6.1" + terser "^4.4.3" + webpack-sources "^1.4.3" + terser-webpack-plugin@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.4.tgz#ac045703bd8da0936ce910d8fb6350d0e1dee5fe" @@ -30197,6 +30302,15 @@ url-loader@2.2.0, url-loader@^2.0.1: mime "^2.4.4" schema-utils "^2.4.1" +url-loader@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.3.0.tgz#e0e2ef658f003efb8ca41b0f3ffbf76bab88658b" + integrity sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog== + dependencies: + loader-utils "^1.2.3" + mime "^2.4.4" + schema-utils "^2.5.0" + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" @@ -30263,7 +30377,7 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@2.3.0: +useragent@2.3.0, useragent@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== @@ -31106,7 +31220,7 @@ warning@^4.0.2, warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watchpack@^1.5.0, watchpack@^1.6.0: +watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== @@ -31146,10 +31260,10 @@ webidl-conversions@^4.0.1, webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -webpack-cli@^3.3.9: - version "3.3.9" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.9.tgz#79c27e71f94b7fe324d594ab64a8e396b9daa91a" - integrity sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A== +webpack-cli@^3.3.10: + version "3.3.10" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.10.tgz#17b279267e9b4fb549023fae170da8e6e766da13" + integrity sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg== dependencies: chalk "2.4.2" cross-spawn "6.0.5" @@ -31251,7 +31365,7 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@4.2.2: +webpack-merge@4.2.2, webpack-merge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== @@ -31266,7 +31380,7 @@ webpack-sources@^1.1.0: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -31274,40 +31388,10 @@ webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack- source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.34.0: - version "4.34.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.34.0.tgz#a4c30129482f7b4ece4c0842002dedf2b56fab58" - integrity sha512-ry2IQy1wJjOefLe1uJLzn5tG/DdIKzQqNlIAd2L84kcaADqNvQDTBlo8UcCNyDaT5FiaB+16jhAkb63YeG3H8Q== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" - json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" - schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" - -webpack@4.41.0, webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.0: - version "4.41.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b" - integrity sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g== +webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.5: + version "4.41.5" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.5.tgz#3210f1886bce5310e62bb97204d18c263341b77c" + integrity sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" @@ -31329,7 +31413,7 @@ webpack@4.41.0, webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.0: node-libs-browser "^2.2.1" schema-utils "^1.0.0" tapable "^1.1.3" - terser-webpack-plugin "^1.4.1" + terser-webpack-plugin "^1.4.3" watchpack "^1.6.0" webpack-sources "^1.4.1" From d49a82e9d3bd007e2424223ec237208f6a96f15f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 13 Feb 2020 03:12:53 +0000 Subject: [PATCH 11/14] fix(NA): support legacy plugins path in plugins (#57472) * fix(NA): support legacy plugins path in plugins * chore(NA): add newly build dist --- packages/kbn-pm/dist/index.js | 4 +++- .../kbn-pm/src/production/prepare_project_dependencies.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 15ea3b68f1182..314bcf31e6d05 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -110627,7 +110627,9 @@ __webpack_require__.r(__webpack_exports__); * to Kibana itself. */ -const isKibanaDep = depVersion => depVersion.includes('../../packages/'); +const isKibanaDep = depVersion => // For ../kibana-extra/ directory (legacy only) +depVersion.includes('../../kibana/packages/') || // For plugins/ directory +depVersion.includes('../../packages/'); /** * This prepares the dependencies for an _external_ project. */ diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.ts index af0575b95e51a..9817770166480 100644 --- a/packages/kbn-pm/src/production/prepare_project_dependencies.ts +++ b/packages/kbn-pm/src/production/prepare_project_dependencies.ts @@ -25,7 +25,11 @@ import { Project } from '../utils/project'; * to the Kibana root directory or `../kibana-extra/{plugin}` relative * to Kibana itself. */ -const isKibanaDep = (depVersion: string) => depVersion.includes('../../packages/'); +const isKibanaDep = (depVersion: string) => + // For ../kibana-extra/ directory (legacy only) + depVersion.includes('../../kibana/packages/') || + // For plugins/ directory + depVersion.includes('../../packages/'); /** * This prepares the dependencies for an _external_ project. From c5a60b94a9114c41afe34568df6e8c30ad884026 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 13 Feb 2020 17:35:18 +1300 Subject: [PATCH 12/14] =?UTF-8?q?address=20flaky=20test=20where=20instance?= =?UTF-8?q?s=20might=20have=20different=20start=E2=80=A6=20(#57506)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/triggers_actions_ui/details.ts | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index ce9160abdb086..95371b5b501f5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; -import { omit } from 'lodash'; +import { omit, mapValues } from 'lodash'; import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -210,59 +210,60 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertInstancesList'); - const { - alertInstances: { - ['us-central']: { - meta: { - lastScheduledActions: { date }, - }, - }, - }, - } = await alerting.alerts.getAlertState(alert.id); + const { alertInstances } = await alerting.alerts.getAlertState(alert.id); - const dateOnAllInstances = moment(date) - .utc() - .format('D MMM YYYY @ HH:mm:ss'); + const dateOnAllInstances = mapValues( + alertInstances, + ({ + meta: { + lastScheduledActions: { date }, + }, + }) => moment(date).utc() + ); const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList(); expect(instancesList.map(instance => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-central'].format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-east'].format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-west'].format('D MMM YYYY @ HH:mm:ss'), }, ]); - const durationFromInstanceTillPageLoad = moment.duration( - testBeganAt.diff(moment(date).utc()) + const durationFromInstanceTillPageLoad = mapValues(dateOnAllInstances, date => + moment.duration(testBeganAt.diff(moment(date).utc())) ); instancesList - .map(alertInstance => alertInstance.duration.split(':').map(part => parseInt(part, 10))) - .map(([hours, minutes, seconds]) => - moment.duration({ + .map(alertInstance => ({ + id: alertInstance.instance, + duration: alertInstance.duration.split(':').map(part => parseInt(part, 10)), + })) + .map(({ id, duration: [hours, minutes, seconds] }) => ({ + id, + duration: moment.duration({ hours, minutes, seconds, - }) - ) - .forEach(alertInstanceDuration => { + }), + })) + .forEach(({ id, duration }) => { // make sure the duration is within a 10 second range which is // good enough as the alert interval is 1m, so we know it is a fresh value - expect(alertInstanceDuration.as('milliseconds')).to.greaterThan( - durationFromInstanceTillPageLoad.subtract(1000 * 10).as('milliseconds') + expect(duration.as('milliseconds')).to.greaterThan( + durationFromInstanceTillPageLoad[id].subtract(1000 * 10).as('milliseconds') ); - expect(alertInstanceDuration.as('milliseconds')).to.lessThan( - durationFromInstanceTillPageLoad.add(1000 * 10).as('milliseconds') + expect(duration.as('milliseconds')).to.lessThan( + durationFromInstanceTillPageLoad[id].add(1000 * 10).as('milliseconds') ); }); }); From 06df2b0db65a97888d47fed024597e627a3290e0 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Thu, 13 Feb 2020 09:44:01 +0200 Subject: [PATCH 13/14] [Telemetry] Migrate public to NP (#56285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * NP telemetry plugin barebones * fully migrate telemetry sender * license plugin to use NP telemetry * fully migrated public to NP * finish components testing * fix all tests * self code review * remove commented code * bracket notication for private methods * bracket notication for private methods * update license management tests * afharo code review fixes * type safe private method access in tests * fix typecheck * more type check fixes * i18n check * fix welcome page tests * i18n optedInNoticeBanner title * fix advanced settings field settings * field name * fix home snapshots * listen to app id change * NP code review fixes * NP code review fixes * update telemetry configs with np deprecations * pass telemetry from setup instead of npStart * type check * update core snapshots with new api exposed * remove debugging logs * update home contract * update home contract * fix test eslint import * navigate back to dashboard before start of next case for reporting * gitignore reporting failure_debug generated dir * use gotoDashboardEditMode instead of switch * = instead of : * merge master * escape unused forced types in Field * rename mock to mocks for eslint * Update src/plugins/telemetry/public/components/telemetry_management_section.tsx Co-Authored-By: Alejandro Fernández Haro * fix save/clear type Co-authored-by: Alejandro Fernández Haro Co-authored-by: Elastic Machine --- .i18nrc.json | 5 +- ...n-public.applicationstart.currentappid_.md | 13 + .../kibana-plugin-public.applicationstart.md | 1 + .../application/application_service.mock.ts | 17 +- src/core/public/application/types.ts | 11 +- src/core/public/legacy/legacy_service.ts | 1 + src/core/public/plugins/plugin_context.ts | 1 + src/core/public/public.api.md | 1 + .../config/deprecation/core_deprecations.ts | 3 + .../core_plugins/kibana/public/home/index.ts | 31 +- .../kibana/public/home/kibana_services.ts | 4 +- .../__snapshots__/home.test.js.snap | 1 - .../__snapshots__/welcome.test.tsx.snap | 46 +- .../public/home/np_ready/components/home.js | 11 +- .../home/np_ready/components/home_app.js | 5 +- .../home/np_ready/components/welcome.test.tsx | 24 +- .../home/np_ready/components/welcome.tsx | 56 +- .../core_plugins/kibana/public/home/plugin.ts | 17 +- .../telemetry/common/constants.ts | 5 - .../get_xpack_config_with_deprecated.ts | 41 -- src/legacy/core_plugins/telemetry/index.ts | 36 +- .../__snapshots__/telemetry_form.test.js.snap | 80 --- .../public/components/telemetry_form.test.js | 83 --- .../public/hacks/__tests__/fetch_telemetry.js | 55 -- .../public/hacks/__tests__/telemetry.js | 29 - .../telemetry/public/hacks/fetch_telemetry.js | 44 -- .../telemetry/public/hacks/telemetry.js | 120 ---- .../telemetry/public/hacks/telemetry.test.js | 306 ---------- .../telemetry/public/hacks/telemetry_init.ts | 53 -- .../hacks/welcome_banner/click_banner.js | 77 --- .../hacks/welcome_banner/click_banner.test.js | 128 ---- .../welcome_banner/handle_old_settings.js | 85 --- .../handle_old_settings.test.js | 208 ------- .../hacks/welcome_banner/inject_banner.js | 76 --- .../hacks/welcome_banner/render_banner.js | 46 -- .../welcome_banner/render_notice_banner.js | 38 -- .../welcome_banner/should_show_banner.js | 40 -- .../welcome_banner/should_show_banner.test.js | 91 --- .../public/services/telemetry_opt_in.test.js | 148 ----- .../services/telemetry_opt_in.test.mocks.js | 60 -- .../public/services/telemetry_opt_in.ts | 154 ----- .../views/management/{index.js => index.ts} | 0 .../{management.js => management.tsx} | 40 +- .../usage/telemetry_usage_collector.ts | 3 +- .../core_plugins/telemetry/server/fetcher.ts | 3 +- .../handle_old_settings.ts | 59 ++ .../handle_old_settings/index.ts} | 2 +- .../core_plugins/telemetry/server/index.ts | 1 + .../ui/public/new_platform/new_platform.ts | 3 + .../query_string_input.test.tsx.snap | 66 ++ .../telemetry/common/constants.ts} | 27 +- src/plugins/telemetry/kibana.json | 6 + .../__snapshots__/opt_in_banner.test.tsx.snap | 54 ++ .../opt_in_example_flyout.test.tsx.snap} | 0 .../opt_in_message.test.tsx.snap | 1 + .../opted_in_notice_banner.test.tsx.snap | 0 .../telemetry/public/components/index.ts | 8 +- .../public/components/opt_in_banner.test.tsx | 64 ++ .../public/components/opt_in_banner.tsx} | 13 +- .../opt_in_example_flyout.test.tsx} | 7 +- .../components/opt_in_example_flyout.tsx} | 31 +- .../public/components/opt_in_message.test.tsx | 4 +- .../public/components/opt_in_message.tsx | 25 +- .../opted_in_notice_banner.test.tsx | 6 +- .../components/opted_in_notice_banner.tsx | 32 +- .../telemetry_management_section.tsx} | 102 ++-- .../telemetry/public/index.ts} | 9 +- src/plugins/telemetry/public/mocks.ts | 85 +++ src/plugins/telemetry/public/plugin.ts | 118 ++++ .../telemetry/public/services/index.ts | 22 + .../telemetry_notifications}/index.ts | 3 +- .../render_opt_in_banner.test.ts} | 27 +- .../render_opt_in_banner.tsx | 35 ++ .../render_opted_in_notice_banner.test.ts} | 31 +- .../render_opted_in_notice_banner.tsx} | 17 +- .../telemetry_notifications.test.ts | 55 ++ .../telemetry_notifications.ts | 88 +++ .../public/services/telemetry_sender.test.ts | 272 +++++++++ .../public/services/telemetry_sender.ts | 100 +++ .../public/services/telemetry_service.test.ts | 139 +++++ .../public/services/telemetry_service.ts | 165 +++++ test/functional/config.ie.js | 7 +- test/functional/config.js | 7 +- x-pack/.gitignore | 1 + .../telemetry_opt_in.test.js.snap | 576 ------------------ .../upload_license.test.tsx.snap | 5 - .../__jest__/telemetry_opt_in.test.js | 43 -- .../public/np_ready/application/app.js | 7 +- .../public/np_ready/application/boot.tsx | 8 +- .../telemetry_opt_in/{index.js => index.ts} | 0 ...lemetry_opt_in.js => telemetry_opt_in.tsx} | 51 +- .../np_ready/application/lib/telemetry.js | 36 -- .../np_ready/application/lib/telemetry.ts | 24 + .../license_dashboard/license_dashboard.js | 4 +- .../start_trial/{index.js => index.ts} | 1 + .../{start_trial.js => start_trial.tsx} | 66 +- .../sections/upload_license/upload_license.js | 29 +- .../public/np_ready/plugin.ts | 6 +- .../public/register_route.ts | 21 +- x-pack/legacy/plugins/xpack_main/index.js | 18 - .../translations/translations/ja-JP.json | 6 +- .../translations/translations/zh-CN.json | 6 +- x-pack/test/functional/config.ie.js | 7 +- x-pack/test/reporting/functional/reporting.js | 7 +- 104 files changed, 1815 insertions(+), 3095 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md delete mode 100644 src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts delete mode 100644 src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap delete mode 100644 src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js delete mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js delete mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js delete mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts rename src/legacy/core_plugins/telemetry/public/views/management/{index.js => index.ts} (100%) rename src/legacy/core_plugins/telemetry/public/views/management/{management.js => management.tsx} (52%) create mode 100644 src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts rename src/legacy/core_plugins/telemetry/{public/hacks/welcome_banner/index.js => server/handle_old_settings/index.ts} (93%) rename src/{legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js => plugins/telemetry/common/constants.ts} (61%) create mode 100644 src/plugins/telemetry/kibana.json create mode 100644 src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap rename src/{legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap => plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap} (100%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap (97%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap (100%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/index.ts (77%) create mode 100644 src/plugins/telemetry/public/components/opt_in_banner.test.tsx rename src/{legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx => plugins/telemetry/public/components/opt_in_banner.tsx} (84%) rename src/{legacy/core_plugins/telemetry/public/components/opt_in_details_component.test.tsx => plugins/telemetry/public/components/opt_in_example_flyout.test.tsx} (84%) rename src/{legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx => plugins/telemetry/public/components/opt_in_example_flyout.tsx} (91%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opt_in_message.test.tsx (89%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opt_in_message.tsx (81%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opted_in_notice_banner.test.tsx (84%) rename src/{legacy/core_plugins => plugins}/telemetry/public/components/opted_in_notice_banner.tsx (75%) rename src/{legacy/core_plugins/telemetry/public/components/telemetry_form.js => plugins/telemetry/public/components/telemetry_management_section.tsx} (70%) rename src/{legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js => plugins/telemetry/public/index.ts} (81%) create mode 100644 src/plugins/telemetry/public/mocks.ts create mode 100644 src/plugins/telemetry/public/plugin.ts create mode 100644 src/plugins/telemetry/public/services/index.ts rename src/{legacy/core_plugins/telemetry/public/services => plugins/telemetry/public/services/telemetry_notifications}/index.ts (88%) rename src/{legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js => plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts} (56%) create mode 100644 src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx rename src/{legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js => plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts} (52%) rename src/{legacy/core_plugins/telemetry/public/services/path.ts => plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx} (59%) create mode 100644 src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_sender.test.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_sender.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_service.test.ts create mode 100644 src/plugins/telemetry/public/services/telemetry_service.ts delete mode 100644 x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap delete mode 100644 x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js rename x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/{index.js => index.ts} (100%) rename x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/{telemetry_opt_in.js => telemetry_opt_in.tsx} (84%) delete mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js create mode 100644 x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts rename x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/{index.js => index.ts} (95%) rename x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/{start_trial.js => start_trial.tsx} (87%) diff --git a/.i18nrc.json b/.i18nrc.json index c171b842254ee..6874d02304e49 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -37,7 +37,10 @@ "savedObjects": "src/plugins/saved_objects", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", - "telemetry": "src/legacy/core_plugins/telemetry", + "telemetry": [ + "src/legacy/core_plugins/telemetry", + "src/plugins/telemetry" + ], "tileMap": "src/legacy/core_plugins/tile_map", "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], "uiActions": "src/plugins/ui_actions", diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md new file mode 100644 index 0000000000000..d3ceeabcd81f4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) + +## ApplicationStart.currentAppId$ property + +An observable that emits the current application id and each subsequent id update. + +Signature: + +```typescript +currentAppId$: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 3ad7e3b1656d8..433ce87419ae8 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -16,6 +16,7 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +| [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | ## Methods diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index dee47315fc322..d2a827d381be5 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -43,12 +43,17 @@ const createInternalSetupContractMock = (): jest.Mocked => ({ - capabilities: capabilitiesServiceMock.createStartContract().capabilities, - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - registerMountContext: jest.fn(), -}); +const createStartContractMock = (): jest.Mocked => { + const currentAppId$ = new Subject(); + + return { + currentAppId$: currentAppId$.asObservable(), + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + registerMountContext: jest.fn(), + }; +}; const createInternalStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 17fdfc627187e..493afd1fec9db 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -612,11 +612,19 @@ export interface ApplicationStart { contextName: T, provider: IContextProvider ): void; + + /** + * An observable that emits the current application id and each subsequent id update. + */ + currentAppId$: Observable; } /** @internal */ export interface InternalApplicationStart - extends Pick { + extends Pick< + ApplicationStart, + 'capabilities' | 'navigateToApp' | 'getUrlForApp' | 'currentAppId$' + > { /** * Apps available based on the current capabilities. * Should be used to show navigation links and make routing decisions. @@ -640,7 +648,6 @@ export interface InternalApplicationStart ): void; // Internal APIs - currentAppId$: Observable; getComponent(): JSX.Element | null; } diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index e4788e686dd45..1b7e25f585566 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -121,6 +121,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreStart = { ...core, application: { + currentAppId$: core.application.currentAppId$, capabilities: core.application.capabilities, getUrlForApp: core.application.getUrlForApp, navigateToApp: core.application.navigateToApp, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 48100cba4f26e..19cfadf70be1b 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -134,6 +134,7 @@ export function createPluginStartContext< ): CoreStart { return { application: { + currentAppId$: deps.application.currentAppId$, capabilities: deps.application.capabilities, navigateToApp: deps.application.navigateToApp, getUrlForApp: deps.application.getUrlForApp, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aa7ca4fee675e..aab88b0befba3 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -98,6 +98,7 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { capabilities: RecursiveReadonly; + currentAppId$: Observable; getUrlForApp(appId: string, options?: { path?: string; }): string; diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 3aa7f9e2aa8ad..4fa51dcd5a082 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -115,6 +115,9 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), + renameFromRoot('xpack.xpack_main.telemetry.config', 'telemetry.config'), + renameFromRoot('xpack.xpack_main.telemetry.url', 'telemetry.url'), + renameFromRoot('xpack.xpack_main.telemetry.enabled', 'telemetry.enabled'), renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'), renameFromRoot('xpack.telemetry.config', 'telemetry.config'), renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'), diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index c4e58e1a5e1ae..768e1a96de935 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -18,30 +18,7 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import chrome from 'ui/chrome'; -import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -import { IPrivate } from '../../../../../plugins/kibana_legacy/public'; - -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await chrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - return { - telemetryOptInProvider, - shouldShowTelemetryOptIn: - telemetryEnabled && telemetryBanner && !telemetryOptInProvider.getOptIn(), - }; -} +import { HomePlugin } from './plugin'; (async () => { const instance = new HomePlugin(); @@ -49,10 +26,8 @@ async function getAngularDependencies(): Promise unknown; chrome: ChromeStart; - telemetryOptInProvider: any; uiSettings: IUiSettingsClient; config: KibanaLegacySetup['config']; homeConfig: HomePublicPluginSetup['config']; @@ -64,10 +64,10 @@ export interface HomeKibanaServices { banners: OverlayStart['banners']; trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; getBasePath: () => string; - shouldShowTelemetryOptIn: boolean; docLinks: DocLinksStart; addBasePath: (url: string) => string; environment: Environment; + telemetry?: TelemetryPluginStart; } let services: HomeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap index 4563b633c3dfc..9d27362e62739 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap @@ -1054,7 +1054,6 @@ exports[`home welcome should show the normal home page if welcome screen is disa exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap index 6f76ceecbba13..df7cc7bcbaed0 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap @@ -67,44 +67,6 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` - - - - - - - - - - -
@@ -200,16 +162,16 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` /> 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 0c09c6c3c74fc..617a1810028fc 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 @@ -51,7 +51,6 @@ export class Home extends Component { getServices().homeConfig.disableWelcomeScreen || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); - const currentOptInStatus = this.props.getOptInStatus(); this.state = { // If welcome is enabled, we wait for loading to complete // before rendering. This prevents an annoying flickering @@ -60,7 +59,6 @@ export class Home extends Component { isLoading: isWelcomeEnabled, isNewKibanaInstance: false, isWelcomeEnabled, - currentOptInStatus, }; } @@ -224,8 +222,7 @@ export class Home extends Component { ); } @@ -264,6 +261,8 @@ Home.propTypes = { localStorage: PropTypes.object.isRequired, urlBasePath: PropTypes.string.isRequired, mlEnabled: PropTypes.bool.isRequired, - onOptInSeen: PropTypes.func.isRequired, - getOptInStatus: PropTypes.func.isRequired, + telemetry: PropTypes.shape({ + telemetryService: PropTypes.any, + telemetryNotifications: PropTypes.any, + }), }; 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 f6c91b412381c..d7531864582a3 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 @@ -35,7 +35,7 @@ export function HomeApp({ directories }) { getBasePath, addBasePath, environment, - telemetryOptInProvider: { setOptInNoticeSeen, getOptIn }, + telemetry, } = getServices(); const isCloudEnabled = environment.cloud; const mlEnabled = environment.ml; @@ -84,8 +84,7 @@ export function HomeApp({ directories }) { find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} - onOptInSeen={setOptInNoticeSeen} - getOptInStatus={getOptIn} + telemetry={telemetry} /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 55c469fa58fc6..d9da47a2b43da 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Welcome } from './welcome'; +import { telemetryPluginMock } from '../../../../../../../plugins/telemetry/public/mocks'; jest.mock('../../kibana_services', () => ({ getServices: () => ({ @@ -29,27 +30,32 @@ jest.mock('../../kibana_services', () => ({ })); test('should render a Welcome screen with the telemetry disclaimer', () => { + const telemetry = telemetryPluginMock.createSetupContract(); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { + const telemetry = telemetryPluginMock.createSetupContract(); + telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} currentOptInStatus={true} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { + const telemetry = telemetryPluginMock.createSetupContract(); + telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} currentOptInStatus={false} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); @@ -59,19 +65,21 @@ test('should render a Welcome screen with no telemetry disclaimer', () => { // @ts-ignore const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} /> + {}} telemetry={null} /> ); expect(component).toMatchSnapshot(); }); test('fires opt-in seen when mounted', () => { - const seen = jest.fn(); - + const telemetry = telemetryPluginMock.createSetupContract(); + const mockSetOptedInNoticeSeen = jest.fn(); + // @ts-ignore + telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; shallow( // @ts-ignore - {}} onOptInSeen={seen} /> + {}} telemetry={telemetry} /> ); - expect(seen).toHaveBeenCalled(); + expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); }); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 6983aabc4c7b1..7906caeda1b38 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -38,13 +38,14 @@ import { import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../../kibana_services'; +import { TelemetryPluginStart } from '../../../../../../../plugins/telemetry/public'; +import { PRIVACY_STATEMENT_URL } from '../../../../../../../plugins/telemetry/common/constants'; import { SampleDataCard } from './sample_data'; interface Props { urlBasePath: string; onSkip: () => void; - onOptInSeen: () => any; - currentOptInStatus: boolean; + telemetry?: TelemetryPluginStart; } /** @@ -75,8 +76,11 @@ export class Welcome extends React.Component { }; componentDidMount() { + const { telemetry } = this.props; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - this.props.onOptInSeen(); + if (telemetry) { + telemetry.telemetryNotifications.setOptedInNoticeSeen(); + } document.addEventListener('keydown', this.hideOnEsc); } @@ -85,7 +89,13 @@ export class Welcome extends React.Component { } private renderTelemetryEnabledOrDisabledText = () => { - if (this.props.currentOptInStatus) { + const { telemetry } = this.props; + if (!telemetry) { + return null; + } + + const isOptedIn = telemetry.telemetryService.getIsOptedIn(); + if (isOptedIn) { return ( { }; render() { - const { urlBasePath } = this.props; + const { urlBasePath, telemetry } = this.props; return (
@@ -154,24 +164,24 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - + {!!telemetry && ( + + + + + + + {this.renderTelemetryEnabledOrDisabledText()} + + + + )}
diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index e530906d5698e..5cc7c9c11dd2f 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -20,6 +20,7 @@ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { TelemetryPluginStart } from 'src/plugins/telemetry/public'; import { setServices } from './kibana_services'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; @@ -30,14 +31,10 @@ import { FeatureCatalogueEntry, } from '../../../../../plugins/home/public'; -export interface LegacyAngularInjectedDependencies { - telemetryOptInProvider: any; - shouldShowTelemetryOptIn: boolean; -} - export interface HomePluginStartDependencies { data: DataPublicPluginStart; home: HomePublicPluginStart; + telemetry?: TelemetryPluginStart; } export interface HomePluginSetupDependencies { @@ -55,7 +52,6 @@ export interface HomePluginSetupDependencies { devMode: boolean; uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; }; - getAngularDependencies: () => Promise; }; usageCollection: UsageCollectionSetup; kibanaLegacy: KibanaLegacySetup; @@ -67,6 +63,7 @@ export class HomePlugin implements Plugin { private savedObjectsClient: any = null; private environment: Environment | null = null; private directories: readonly FeatureCatalogueEntry[] | null = null; + private telemetry?: TelemetryPluginStart; setup( core: CoreSetup, @@ -74,7 +71,7 @@ export class HomePlugin implements Plugin { home, kibanaLegacy, usageCollection, - __LEGACY: { getAngularDependencies, ...legacyServices }, + __LEGACY: { ...legacyServices }, }: HomePluginSetupDependencies ) { kibanaLegacy.registerLegacyApp({ @@ -82,7 +79,6 @@ export class HomePlugin implements Plugin { title: 'Home', mount: async ({ core: contextCore }, params) => { const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); - const angularDependencies = await getAngularDependencies(); setServices({ ...legacyServices, trackUiMetric, @@ -92,6 +88,7 @@ export class HomePlugin implements Plugin { getInjected: core.injectedMetadata.getInjectedVar, docLinks: contextCore.docLinks, savedObjectsClient: this.savedObjectsClient!, + telemetry: this.telemetry, chrome: contextCore.chrome, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, @@ -101,7 +98,6 @@ export class HomePlugin implements Plugin { config: kibanaLegacy.config, homeConfig: home.config, directories: this.directories!, - ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); return await renderApp(params.element); @@ -109,10 +105,11 @@ export class HomePlugin implements Plugin { }); } - start(core: CoreStart, { data, home }: HomePluginStartDependencies) { + start(core: CoreStart, { data, home, telemetry }: HomePluginStartDependencies) { this.environment = home.environment.get(); this.directories = home.featureCatalogue.get(); this.dataStart = data; + this.telemetry = telemetry; this.savedObjectsClient = core.savedObjects.client; } diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index cf2c9c883871b..52981c04ad34a 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -43,11 +43,6 @@ export const getConfigTelemetryDesc = () => { */ export const REPORT_INTERVAL_MS = 86400000; -/* - * Key for the localStorage service - */ -export const LOCALSTORAGE_KEY = 'telemetry.data'; - /** * Link to the Elastic Telemetry privacy statement. */ diff --git a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts deleted file mode 100644 index 3f7a8d3410993..0000000000000 --- a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts +++ /dev/null @@ -1,41 +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 { KibanaConfig } from 'src/legacy/server/kbn_server'; - -export function getXpackConfigWithDeprecated(config: KibanaConfig, configPath: string) { - try { - const deprecatedXpackmainConfig = config.get(`xpack.xpack_main.${configPath}`); - if (typeof deprecatedXpackmainConfig !== 'undefined') { - return deprecatedXpackmainConfig; - } - } catch (err) { - // swallow error - } - try { - const deprecatedXpackConfig = config.get(`xpack.${configPath}`); - if (typeof deprecatedXpackConfig !== 'undefined') { - return deprecatedXpackConfig; - } - } catch (err) { - // swallow error - } - - return config.get(configPath); -} diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 2a81e3fa05c6c..ec70380d83a0a 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -22,14 +22,17 @@ import { resolve } from 'path'; import JoiNamespace from 'joi'; import { Server } from 'hapi'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getConfigPath } from '../../../core/server/path'; // @ts-ignore import mappings from './mappings.json'; -import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; -import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server'; +import { + telemetryPlugin, + replaceTelemetryInjectedVars, + FetcherTask, + PluginsSetup, + handleOldSettings, +} from './server'; const ENDPOINT_VERSION = 'v2'; @@ -76,16 +79,6 @@ const telemetry = (kibana: any) => { }, uiExports: { managementSections: ['plugins/telemetry/views/management'], - uiSettingDefaults: { - [CONFIG_TELEMETRY]: { - name: i18n.translate('telemetry.telemetryConfigTitle', { - defaultMessage: 'Telemetry opt-in', - }), - description: getConfigTelemetryDesc(), - value: false, - readonly: true, - }, - }, savedObjectSchemas: { telemetry: { isNamespaceAgnostic: true, @@ -98,11 +91,11 @@ const telemetry = (kibana: any) => { injectDefaultVars(server: Server) { const config = server.config(); return { - telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), - telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), + telemetryEnabled: config.get('telemetry.enabled'), + telemetryUrl: config.get('telemetry.url'), telemetryBanner: config.get('telemetry.allowChangingOptInStatus') !== false && - getXpackConfigWithDeprecated(config, 'telemetry.banner'), + config.get('telemetry.banner'), telemetryOptedIn: config.get('telemetry.optIn'), telemetryOptInStatusUrl: config.get('telemetry.optInStatusUrl'), allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), @@ -110,14 +103,13 @@ const telemetry = (kibana: any) => { telemetryNotifyUserAboutOptInDefault: false, }; }, - hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, postInit(server: Server) { const fetcherTask = new FetcherTask(server); fetcherTask.start(); }, - init(server: Server) { + async init(server: Server) { const { usageCollection } = server.newPlatform.setup.plugins; const initializerContext = { env: { @@ -145,6 +137,12 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; + try { + await handleOldSettings(server); + } catch (err) { + server.log(['warning', 'telemetry'], 'Unable to update legacy telemetry configs.'); + } + const pluginsSetup: PluginsSetup = { usageCollection, }; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap deleted file mode 100644 index 079a43e77616d..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`; - -exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = ` - - - - - - -

- -

-
-
-
- - -

- - - , - } - } - /> -

-

- - - -

- , - "type": "boolean", - "value": false, - } - } - /> -
-
-
-`; diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js deleted file mode 100644 index fe0c2c3449af1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js +++ /dev/null @@ -1,83 +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 { mockInjectedMetadata } from '../services/telemetry_opt_in.test.mocks'; -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { TelemetryForm } from './telemetry_form'; -import { TelemetryOptInProvider } from '../services'; - -const buildTelemetryOptInProvider = () => { - const mockHttp = { - post: jest.fn(), - }; - - const mockInjector = { - get: key => { - switch (key) { - case '$http': - return mockHttp; - case 'allowChangingOptInStatus': - return true; - default: - return null; - } - }, - }; - - const chrome = { - addBasePath: url => url, - }; - - return new TelemetryOptInProvider(mockInjector, chrome); -}; - -describe('TelemetryForm', () => { - it('renders as expected when allows to change optIn status', () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); - - it(`doesn't render form when not allowed to change optIn status`, () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js deleted file mode 100644 index ad9ee0998e3bb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { fetchTelemetry } from '../fetch_telemetry'; - -describe('fetch_telemetry', () => { - it('fetchTelemetry calls expected URL with 20 minutes - now', () => { - const response = Promise.resolve(); - const $http = { - post: sinon.stub(), - }; - const basePath = 'fake'; - const moment = { - subtract: sinon.stub(), - toISOString: () => 'max123', - }; - - moment.subtract.withArgs(20, 'minutes').returns({ - toISOString: () => 'min456', - }); - - $http.post - .withArgs(`fake/api/telemetry/v2/clusters/_stats`, { - unencrypted: true, - timeRange: { - min: 'min456', - max: 'max123', - }, - }) - .returns(response); - - expect(fetchTelemetry($http, { basePath, _moment: () => moment, unencrypted: true })).to.be( - response - ); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js deleted file mode 100644 index 74f1de4934a78..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js +++ /dev/null @@ -1,29 +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 { uiModules } from 'ui/modules'; - -// This overrides settings for other UI tests -uiModules - .get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryEnabled', false) - .constant('telemetryOptedIn', null) - .constant('telemetryUrl', 'not.a.valid.url.0'); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js deleted file mode 100644 index ede81f638a3fc..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js +++ /dev/null @@ -1,44 +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 uiChrome from 'ui/chrome'; -import moment from 'moment'; - -/** - * Fetch Telemetry data by calling the Kibana API. - * - * @param {Object} $http The HTTP handler - * @param {String} basePath The base URI - * @param {Function} _moment moment.js, but injectable for tests - * @return {Promise} An array of cluster Telemetry objects. - */ -export function fetchTelemetry( - $http, - { basePath = uiChrome.getBasePath(), _moment = moment, unencrypted = false } = {} -) { - return $http.post(`${basePath}/api/telemetry/v2/clusters/_stats`, { - unencrypted, - timeRange: { - min: _moment() - .subtract(20, 'minutes') - .toISOString(), - max: _moment().toISOString(), - }, - }); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js deleted file mode 100644 index 8fa777ead3e4b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js +++ /dev/null @@ -1,120 +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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; - -export class Telemetry { - /** - * @param {Object} $injector - AngularJS injector service - * @param {Function} fetchTelemetry Method used to fetch telemetry data (expects an array response) - */ - constructor($injector, fetchTelemetry) { - this._storage = $injector.get('localStorage'); - this._$http = $injector.get('$http'); - this._telemetryUrl = $injector.get('telemetryUrl'); - this._telemetryOptedIn = $injector.get('telemetryOptedIn'); - this._fetchTelemetry = fetchTelemetry; - this._sending = false; - - // try to load the local storage data - const attributes = this._storage.get(LOCALSTORAGE_KEY) || {}; - this._lastReport = attributes.lastReport; - } - - _saveToBrowser() { - // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object - this._storage.set(LOCALSTORAGE_KEY, { lastReport: this._lastReport }); - } - - /** - * Determine if we are due to send a new report. - * - * @returns {Boolean} true if a new report should be sent. false otherwise. - */ - _checkReportStatus() { - // check if opt-in for telemetry is enabled - if (this._telemetryOptedIn) { - // returns NaN for any malformed or unset (null/undefined) value - const lastReport = parseInt(this._lastReport, 10); - // If it's been a day since we last sent telemetry - if (isNaN(lastReport) || Date.now() - lastReport > REPORT_INTERVAL_MS) { - return true; - } - } - - return false; - } - - /** - * Check report permission and if passes, send the report - * - * @returns {Promise} Always. - */ - _sendIfDue() { - if (this._sending || !this._checkReportStatus()) { - return Promise.resolve(false); - } - - // mark that we are working so future requests are ignored until we're done - this._sending = true; - - return ( - this._fetchTelemetry() - .then(response => { - const clusters = [].concat(response.data); - return Promise.all( - clusters.map(cluster => { - const req = { - method: 'POST', - url: this._telemetryUrl, - data: cluster, - }; - // if passing data externally, then suppress kbnXsrfToken - if (this._telemetryUrl.match(/^https/)) { - req.kbnXsrfToken = false; - } - return this._$http(req); - }) - ); - }) - // the response object is ignored because we do not check it - .then(() => { - // we sent a report, so we need to record and store the current timestamp - this._lastReport = Date.now(); - this._saveToBrowser(); - }) - // no ajaxErrorHandlers for telemetry - .catch(() => null) - .then(() => { - this._sending = false; - return true; // sent, but not necessarilly successfully - }) - ); - } - - /** - * Public method - * - * @returns {Number} `window.setInterval` response to allow cancelling the interval. - */ - start() { - // continuously check if it's due time for a report - return window.setInterval(() => this._sendIfDue(), 60000); - } -} // end class diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js deleted file mode 100644 index 45a0653cd7a54..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js +++ /dev/null @@ -1,306 +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 { Telemetry } from './telemetry'; -import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; - -describe('telemetry class', () => { - const clusters = [{ cluster_uuid: 'fake-123' }, { cluster_uuid: 'fake-456' }]; - const telemetryUrl = 'https://not.a.valid.url.0'; - const mockFetchTelemetry = () => Promise.resolve({ data: clusters }); - // returns a function that behaves like the injector by fetching the requested key from the object directly - // for example: - // { '$http': jest.fn() } would be how to mock the '$http' injector value - const mockInjectorFromObject = object => { - return { get: key => object[key] }; - }; - - describe('constructor', () => { - test('defaults lastReport if unset', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), - }, - $http: jest.fn(), - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._storage).toBe(injector.localStorage); - expect(telemetry._$http).toBe(injector.$http); - expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn); - expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl); - expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry); - expect(telemetry._sending).toBe(false); - expect(telemetry._lastReport).toBeUndefined(); - - expect(injector.localStorage.get).toHaveBeenCalledTimes(1); - expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY); - }); - - test('uses lastReport if set', () => { - const lastReport = Date.now(); - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport }), - }, - $http: jest.fn(), - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._storage).toBe(injector.localStorage); - expect(telemetry._$http).toBe(injector.$http); - expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn); - expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl); - expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry); - expect(telemetry._sending).toBe(false); - expect(telemetry._lastReport).toBe(lastReport); - - expect(injector.localStorage.get).toHaveBeenCalledTimes(1); - expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY); - }); - }); - - test('_saveToBrowser uses _lastReport', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ random: 'junk', gets: 'thrown away' }), - set: jest.fn(), - }, - }; - const lastReport = Date.now(); - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - telemetry._lastReport = lastReport; - - telemetry._saveToBrowser(); - - expect(injector.localStorage.set).toHaveBeenCalledTimes(1); - expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport }); - }); - - describe('_checkReportStatus', () => { - // send the report if we get to check the time - const lastReportShouldSendNow = Date.now() - REPORT_INTERVAL_MS - 1; - - test('returns false whenever telemetryOptedIn is null', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: null, // not yet opted in - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - test('returns false whenever telemetryOptedIn is false', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: false, // opted out explicitly - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/27922 - test.skip('returns false if last report is too recent', () => { - const injector = { - localStorage: { - // we expect '>', not '>=' - get: jest.fn().mockReturnValueOnce({ lastReport: Date.now() - REPORT_INTERVAL_MS }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - test('returns true if last report is not defined', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({}), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and old enough', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and old enough as a string', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow.toString() }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and malformed', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: { not: { a: 'number' } } }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - }); - - describe('_sendIfDue', () => { - test('ignores and returns false if already sending', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), // never sent - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - telemetry._sending = true; - - return expect(telemetry._sendIfDue()).resolves.toBe(false); - }); - - test('ignores and returns false if _checkReportStatus says so', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), // never sent, so it would try if opted in - }, - telemetryOptedIn: false, // opted out - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - return expect(telemetry._sendIfDue()).resolves.toBe(false); - }); - - test('sends telemetry when requested', () => { - const now = Date.now(); - const injector = { - $http: jest.fn().mockResolvedValue({}), // ignored response - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: now - REPORT_INTERVAL_MS - 1 }), - set: jest.fn(), - }, - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect.hasAssertions(); - - return telemetry._sendIfDue().then(result => { - expect(result).toBe(true); - expect(telemetry._sending).toBe(false); - - // should be updated - const lastReport = telemetry._lastReport; - - // if the test runs fast enough it should be exactly equal, but probably a few ms greater - expect(lastReport).toBeGreaterThanOrEqual(now); - - expect(injector.$http).toHaveBeenCalledTimes(2); - // assert that it sent every cluster's telemetry - clusters.forEach(cluster => { - expect(injector.$http).toHaveBeenCalledWith({ - method: 'POST', - url: telemetryUrl, - data: cluster, - kbnXsrfToken: false, - }); - }); - - expect(injector.localStorage.set).toHaveBeenCalledTimes(1); - expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport }); - }); - }); - - test('sends telemetry when requested and catches exceptions', () => { - const lastReport = Date.now() - REPORT_INTERVAL_MS - 1; - const injector = { - $http: jest.fn().mockRejectedValue(new Error('TEST - expected')), // caught failure - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport }), - set: jest.fn(), - }, - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect.hasAssertions(); - - return telemetry._sendIfDue().then(result => { - expect(result).toBe(true); // attempted to send - expect(telemetry._sending).toBe(false); - - // should be unchanged - expect(telemetry._lastReport).toBe(lastReport); - expect(injector.localStorage.set).toHaveBeenCalledTimes(0); - - expect(injector.$http).toHaveBeenCalledTimes(2); - // assert that it sent every cluster's telemetry - clusters.forEach(cluster => { - expect(injector.$http).toHaveBeenCalledWith({ - method: 'POST', - url: telemetryUrl, - data: cluster, - kbnXsrfToken: false, - }); - }); - }); - }); - }); - - test('start', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), - }, - telemetryOptedIn: false, // opted out - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - clearInterval(telemetry.start()); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts deleted file mode 100644 index 1930d65d5c09b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts +++ /dev/null @@ -1,53 +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 { npStart } from 'ui/new_platform'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { isUnauthenticated } from '../services'; -// @ts-ignore -import { Telemetry } from './telemetry'; -// @ts-ignore -import { fetchTelemetry } from './fetch_telemetry'; -// @ts-ignore -import { isOptInHandleOldSettings } from './welcome_banner/handle_old_settings'; -import { TelemetryOptInProvider } from '../services'; - -function telemetryInit($injector: any) { - const $http = $injector.get('$http'); - const Private = $injector.get('Private'); - const config = $injector.get('config'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryOptedIn = isOptInHandleOldSettings(config, telemetryOptInProvider); - const sendUsageFrom = npStart.core.injectedMetadata.getInjectedVar('telemetrySendUsageFrom'); - - if (telemetryEnabled && telemetryOptedIn && sendUsageFrom === 'browser') { - // no telemetry for non-logged in users - if (isUnauthenticated()) { - return; - } - - const sender = new Telemetry($injector, () => fetchTelemetry($http)); - sender.start(); - } -} - -uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js deleted file mode 100644 index 44971e2466794..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js +++ /dev/null @@ -1,77 +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 React from 'react'; - -import { banners, toastNotifications } from 'ui/notify'; -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -/** - * Handle clicks from the user on the opt-in banner. - * - * @param {Object} telemetryOptInProvider the telemetry opt-in provider - * @param {Boolean} optIn {@code true} to opt into telemetry. - * @param {Object} _banners Singleton banners. Can be overridden for tests. - * @param {Object} _toastNotifications Singleton toast notifications. Can be overridden for tests. - */ -export async function clickBanner( - telemetryOptInProvider, - optIn, - { _banners = banners, _toastNotifications = toastNotifications } = {} -) { - const bannerId = telemetryOptInProvider.getBannerId(); - let set = false; - - try { - set = await telemetryOptInProvider.setOptIn(optIn); - } catch (err) { - // set is already false - console.log('Unexpected error while trying to save setting.', err); - } - - if (set) { - _banners.remove(bannerId); - } else { - _toastNotifications.addDanger({ - title: ( - - ), - text: ( - -

- -

- - - -
- ), - }); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js deleted file mode 100644 index 0caabe826ae57..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; -import { uiModules } from 'ui/modules'; - -uiModules - .get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryOptedIn', null); - -import { clickBanner } from './click_banner'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; - -const getMockInjector = ({ simulateFailure }) => { - const get = sinon.stub(); - - const mockHttp = { - post: sinon.stub(), - }; - - if (simulateFailure) { - mockHttp.post.returns(Promise.reject(new Error('something happened'))); - } else { - mockHttp.post.returns(Promise.resolve({})); - } - - get.withArgs('$http').returns(mockHttp); - - return { get }; -}; - -const getTelemetryOptInProvider = ({ simulateFailure = false, simulateError = false } = {}) => { - const injector = getMockInjector({ simulateFailure }); - const chrome = { - addBasePath: url => url, - }; - - const provider = new TelemetryOptInProvider(injector, chrome, false); - - if (simulateError) { - provider.setOptIn = () => Promise.reject('unhandled error'); - } - - return provider; -}; - -describe('click_banner', () => { - it('sets setting successfully and removes banner', async () => { - const banners = { - remove: sinon.spy(), - }; - - const optIn = true; - const bannerId = 'bruce-banner'; - mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider(); - - telemetryOptInProvider.setBannerId(bannerId); - - await clickBanner(telemetryOptInProvider, optIn, { _banners: banners }); - - expect(telemetryOptInProvider.getOptIn()).toBe(optIn); - expect(banners.remove.calledOnce).toBe(true); - expect(banners.remove.calledWith(bannerId)).toBe(true); - }); - - it('sets setting unsuccessfully, adds toast, and does not touch banner', async () => { - const toastNotifications = { - addDanger: sinon.spy(), - }; - const banners = { - remove: sinon.spy(), - }; - const optIn = true; - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true }); - - await clickBanner(telemetryOptInProvider, optIn, { - _banners: banners, - _toastNotifications: toastNotifications, - }); - - expect(telemetryOptInProvider.getOptIn()).toBe(null); - expect(toastNotifications.addDanger.calledOnce).toBe(true); - expect(banners.remove.notCalled).toBe(true); - }); - - it('sets setting unsuccessfully with error, adds toast, and does not touch banner', async () => { - const toastNotifications = { - addDanger: sinon.spy(), - }; - const banners = { - remove: sinon.spy(), - }; - const optIn = false; - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true }); - - await clickBanner(telemetryOptInProvider, optIn, { - _banners: banners, - _toastNotifications: toastNotifications, - }); - - expect(telemetryOptInProvider.getOptIn()).toBe(null); - expect(toastNotifications.addDanger.calledOnce).toBe(true); - expect(banners.remove.notCalled).toBe(true); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js deleted file mode 100644 index c03fdb85c4d1c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js +++ /dev/null @@ -1,85 +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 { CONFIG_TELEMETRY } from '../../../common/constants'; - -/** - * Clean up any old, deprecated settings and determine if we should continue. - * - * This will update the latest telemetry setting if necessary. - * - * @param {Object} config The advanced settings config object. - * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. - */ -const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; - -export async function handleOldSettings(config, telemetryOptInProvider) { - const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner'; - const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); - const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); - - let legacyOptInValue = null; - - if (typeof oldTelemetrySetting === 'boolean') { - legacyOptInValue = oldTelemetrySetting; - } else if (typeof oldAllowReportSetting === 'boolean') { - legacyOptInValue = oldAllowReportSetting; - } - - if (legacyOptInValue !== null) { - try { - await telemetryOptInProvider.setOptIn(legacyOptInValue); - - // delete old keys once we've successfully changed the setting (if it fails, we just wait until next time) - config.remove(CONFIG_ALLOW_REPORT); - config.remove(CONFIG_SHOW_BANNER); - config.remove(CONFIG_TELEMETRY); - } finally { - return false; - } - } - - const oldShowSetting = config.get(CONFIG_SHOW_BANNER, null); - - if (oldShowSetting !== null) { - config.remove(CONFIG_SHOW_BANNER); - } - - return true; -} - -export async function isOptInHandleOldSettings(config, telemetryOptInProvider) { - const currentOptInSettting = telemetryOptInProvider.getOptIn(); - - if (typeof currentOptInSettting === 'boolean') { - return currentOptInSettting; - } - - const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); - if (typeof oldTelemetrySetting === 'boolean') { - return oldTelemetrySetting; - } - - const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); - if (typeof oldAllowReportSetting === 'boolean') { - return oldAllowReportSetting; - } - - return null; -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js deleted file mode 100644 index 8f05675565a5e..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ /dev/null @@ -1,208 +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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; - -import { CONFIG_TELEMETRY } from '../../../common/constants'; -import { handleOldSettings } from './handle_old_settings'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; - -const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => { - const $http = { - post: async () => { - if (simulateFailure) { - return Promise.reject(new Error('something happened')); - } - return {}; - }, - }; - - const chrome = { - addBasePath: url => url, - }; - mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true }); - - const $injector = { - get: key => { - if (key === '$http') { - return $http; - } - throw new Error(`unexpected mock injector usage for ${key}`); - }, - }; - - return new TelemetryOptInProvider($injector, chrome, false); -}; - -describe('handle_old_settings', () => { - it('re-uses old "allowReport" setting and stays opted in', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(true); - config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(true)); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(true); - }); - - it('re-uses old "telemetry:optIn" setting and stays opted in', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - config.get.withArgs(CONFIG_TELEMETRY, null).returns(true); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(true); - }); - - it('re-uses old "allowReport" setting and stays opted out', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(true)); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(false); - }); - - it('re-uses old "telemetry:optIn" setting and stays opted out', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs(CONFIG_TELEMETRY, null).returns(false); - config.get.withArgs('xPackMonitoring:allowReport', null).returns(true); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(false); - }); - - it('acknowledges users old setting even if re-setting fails', async () => { - const config = { - get: sinon.stub(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null, { simulateFailure: true }); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - //todo: make the new version of this fail! - config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(false)); - - // note: because it doesn't remove the old settings _and_ returns false, there's no risk of suddenly being opted in - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - }); - - it('removes show banner setting and presents user with choice', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(null); - config.get.withArgs('xPackMonitoring:showBanner', null).returns(false); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true); - - expect(config.get.calledThrice).toBe(true); - expect(config.remove.calledOnce).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:showBanner'); - }); - - it('is effectively ignored on fresh installs', async () => { - const config = { - get: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(null); - config.get.withArgs('xPackMonitoring:showBanner', null).returns(null); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true); - - expect(config.get.calledThrice).toBe(true); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js deleted file mode 100644 index c4c5c3e9e0aa2..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js +++ /dev/null @@ -1,76 +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 chrome from 'ui/chrome'; - -import { fetchTelemetry } from '../fetch_telemetry'; -import { renderBanner } from './render_banner'; -import { renderOptedInBanner } from './render_notice_banner'; -import { shouldShowBanner } from './should_show_banner'; -import { shouldShowOptInBanner } from './should_show_opt_in_banner'; -import { TelemetryOptInProvider, isUnauthenticated } from '../../services'; -import { npStart } from 'ui/new_platform'; - -/** - * Add the Telemetry opt-in banner if the user has not already made a decision. - * - * Note: this is an async function, but Angular fails to use it as one. Its usage does not need to be awaited, - * and thus it can be wrapped in the run method to just be a normal, non-async function. - * - * @param {Object} $injector The Angular injector - */ -async function asyncInjectBanner($injector) { - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - const config = $injector.get('config'); - - // and no banner for non-logged in users - if (isUnauthenticated()) { - return; - } - - // and no banner on status page - if (chrome.getApp().id === 'status_page') { - return; - } - - const $http = $injector.get('$http'); - - // determine if the banner should be displayed - if (await shouldShowBanner(telemetryOptInProvider, config)) { - renderBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true })); - } - - if (await shouldShowOptInBanner(telemetryOptInProvider, config)) { - renderOptedInBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true })); - } -} - -/** - * Add the Telemetry opt-in banner when appropriate. - * - * @param {Object} $injector The Angular injector - */ -export function injectBanner($injector) { - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner'); - if (telemetryEnabled && telemetryBanner) { - asyncInjectBanner($injector); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js deleted file mode 100644 index 70b5030866620..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js +++ /dev/null @@ -1,46 +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 React from 'react'; - -import { banners } from 'ui/notify'; - -import { clickBanner } from './click_banner'; -import { OptInBanner } from '../../components/opt_in_banner_component'; - -/** - * Render the Telemetry Opt-in banner. - * - * @param {Object} telemetryOptInProvider The telemetry opt-in provider. - * @param {Function} fetchTelemetry Function to pull telemetry on demand. - * @param {Object} _banners Banners singleton, which can be overridden for tests. - */ -export function renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners = banners } = {}) { - const bannerId = _banners.add({ - component: ( - clickBanner(telemetryOptInProvider, optIn)} - fetchTelemetry={fetchTelemetry} - /> - ), - priority: 10000, - }); - - telemetryOptInProvider.setBannerId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js deleted file mode 100644 index 2aa53db11c1d9..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js +++ /dev/null @@ -1,38 +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 React from 'react'; - -import { banners } from 'ui/notify'; -import { OptedInBanner } from '../../components/opted_in_notice_banner'; - -/** - * Render the Telemetry Opt-in notice banner. - * - * @param {Object} telemetryOptInProvider The telemetry opt-in provider. - * @param {Object} _banners Banners singleton, which can be overridden for tests. - */ -export function renderOptedInBanner(telemetryOptInProvider, { _banners = banners } = {}) { - const bannerId = _banners.add({ - component: , - priority: 10000, - }); - - telemetryOptInProvider.setOptInBannerNoticeId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js deleted file mode 100644 index ee55f6cc76266..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js +++ /dev/null @@ -1,40 +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 { handleOldSettings } from './handle_old_settings'; - -/** - * Determine if the banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} config The advanced settings config object. - * @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. - */ -export async function shouldShowBanner( - telemetryOptInProvider, - config, - { _handleOldSettings = handleOldSettings } = {} -) { - return ( - telemetryOptInProvider.getOptIn() === null && - (await _handleOldSettings(config, telemetryOptInProvider)) - ); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js deleted file mode 100644 index 9578d462bc85c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ /dev/null @@ -1,91 +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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; - -import { CONFIG_TELEMETRY } from '../../../common/constants'; -import { shouldShowBanner } from './should_show_banner'; -import { TelemetryOptInProvider } from '../../services'; - -const getMockInjector = () => { - const get = sinon.stub(); - - const mockHttp = { - post: sinon.stub(), - }; - - get.withArgs('$http').returns(mockHttp); - - return { get }; -}; - -const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => { - mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true }); - const injector = getMockInjector(); - const chrome = { - addBasePath: url => url, - }; - - return new TelemetryOptInProvider(injector, chrome); -}; - -describe('should_show_banner', () => { - it('returns whatever handleOldSettings does when telemetry opt-in setting is unset', async () => { - const config = { get: sinon.stub() }; - const telemetryOptInProvider = getTelemetryOptInProvider(); - const handleOldSettingsTrue = sinon.stub(); - const handleOldSettingsFalse = sinon.stub(); - - config.get.withArgs(CONFIG_TELEMETRY, null).returns(null); - handleOldSettingsTrue.returns(Promise.resolve(true)); - handleOldSettingsFalse.returns(Promise.resolve(false)); - - const showBannerTrue = await shouldShowBanner(telemetryOptInProvider, config, { - _handleOldSettings: handleOldSettingsTrue, - }); - const showBannerFalse = await shouldShowBanner(telemetryOptInProvider, config, { - _handleOldSettings: handleOldSettingsFalse, - }); - - expect(showBannerTrue).toBe(true); - expect(showBannerFalse).toBe(false); - - expect(config.get.callCount).toBe(0); - expect(handleOldSettingsTrue.calledOnce).toBe(true); - expect(handleOldSettingsFalse.calledOnce).toBe(true); - }); - - it('returns false if telemetry opt-in setting is set to true', async () => { - const config = { get: sinon.stub() }; - - const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: true }); - - expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false); - }); - - it('returns false if telemetry opt-in setting is set to false', async () => { - const config = { get: sinon.stub() }; - - const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: false }); - - expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js deleted file mode 100644 index 494ed24bcc1cb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js +++ /dev/null @@ -1,148 +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 { mockInjectedMetadata } from './telemetry_opt_in.test.mocks'; -import { TelemetryOptInProvider } from './telemetry_opt_in'; - -describe('TelemetryOptInProvider', () => { - const setup = ({ optedIn, simulatePostError, simulatePutError }) => { - const mockHttp = { - post: jest.fn(async () => { - if (simulatePostError) { - return Promise.reject('Something happened'); - } - }), - put: jest.fn(async () => { - if (simulatePutError) { - return Promise.reject('Something happened'); - } - }), - }; - - const mockChrome = { - addBasePath: url => url, - }; - - mockInjectedMetadata({ - telemetryOptedIn: optedIn, - allowChangingOptInStatus: true, - telemetryNotifyUserAboutOptInDefault: true, - }); - - const mockInjector = { - get: key => { - switch (key) { - case '$http': { - return mockHttp; - } - default: - throw new Error('unexpected injector request: ' + key); - } - }, - }; - - const provider = new TelemetryOptInProvider(mockInjector, mockChrome, false); - return { - provider, - mockHttp, - }; - }; - - it('should return the current opt-in status', () => { - const { provider: optedInProvider } = setup({ optedIn: true }); - expect(optedInProvider.getOptIn()).toEqual(true); - - const { provider: optedOutProvider } = setup({ optedIn: false }); - expect(optedOutProvider.getOptIn()).toEqual(false); - }); - - it('should allow an opt-out to take place', async () => { - const { provider, mockHttp } = setup({ optedIn: true }); - await provider.setOptIn(false); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: false }); - - expect(provider.getOptIn()).toEqual(false); - }); - - it('should allow an opt-in to take place', async () => { - const { provider, mockHttp } = setup({ optedIn: false }); - await provider.setOptIn(true); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true }); - - expect(provider.getOptIn()).toEqual(true); - }); - - it('should gracefully handle errors', async () => { - const { provider, mockHttp } = setup({ optedIn: false, simulatePostError: true }); - await provider.setOptIn(true); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true }); - - // opt-in change should not be reflected - expect(provider.getOptIn()).toEqual(false); - }); - - it('should return the current bannerId', () => { - const { provider } = setup({}); - const bannerId = 'bruce-banner'; - provider.setBannerId(bannerId); - expect(provider.getBannerId()).toEqual(bannerId); - }); - - describe('Notice Banner', () => { - it('should return the current bannerId', () => { - const { provider } = setup({}); - const bannerId = 'bruce-wayne'; - provider.setOptInBannerNoticeId(bannerId); - - expect(provider.getOptInBannerNoticeId()).toEqual(bannerId); - expect(provider.getBannerId()).not.toEqual(bannerId); - }); - - it('should persist that a user has seen the notice', async () => { - const { provider, mockHttp } = setup({}); - await provider.setOptInNoticeSeen(); - - expect(mockHttp.put).toHaveBeenCalledWith(`/api/telemetry/v2/userHasSeenNotice`); - - expect(provider.notifyUserAboutOptInDefault()).toEqual(false); - }); - - it('should only call the API once', async () => { - const { provider, mockHttp } = setup({}); - await provider.setOptInNoticeSeen(); - await provider.setOptInNoticeSeen(); - - expect(mockHttp.put).toHaveBeenCalledTimes(1); - - expect(provider.notifyUserAboutOptInDefault()).toEqual(false); - }); - - it('should gracefully handle errors', async () => { - const { provider } = setup({ simulatePutError: true }); - - await provider.setOptInNoticeSeen(); - - // opt-in change should not be reflected - expect(provider.notifyUserAboutOptInDefault()).toEqual(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js deleted file mode 100644 index 4543266be46df..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ /dev/null @@ -1,60 +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 { - injectedMetadataServiceMock, - notificationServiceMock, - overlayServiceMock, -} from '../../../../../core/public/mocks'; -const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); - -export function mockInjectedMetadata({ - telemetryOptedIn, - allowChangingOptInStatus, - telemetryNotifyUserAboutOptInDefault, -}) { - const mockGetInjectedVar = jest.fn().mockImplementation(key => { - switch (key) { - case 'telemetryOptedIn': - return telemetryOptedIn; - case 'allowChangingOptInStatus': - return allowChangingOptInStatus; - case 'telemetryNotifyUserAboutOptInDefault': - return telemetryNotifyUserAboutOptInDefault; - default: - throw new Error(`unexpected injectedVar ${key}`); - } - }); - - injectedMetadataMock.getInjectedVar = mockGetInjectedVar; -} - -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - notifications: notificationServiceMock.createSetupContract(), - }, - }, - npStart: { - core: { - injectedMetadata: injectedMetadataMock, - overlays: overlayServiceMock.createStartContract(), - }, - }, -})); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts deleted file mode 100644 index af908bea7f4b1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts +++ /dev/null @@ -1,154 +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 moment from 'moment'; -// @ts-ignore -import { banners, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; - -let bannerId: string | null = null; -let optInBannerNoticeId: string | null = null; -let currentOptInStatus = false; -let telemetryNotifyUserAboutOptInDefault = true; - -async function sendOptInStatus($injector: any, chrome: any, enabled: boolean) { - const telemetryOptInStatusUrl = npStart.core.injectedMetadata.getInjectedVar( - 'telemetryOptInStatusUrl' - ) as string; - const $http = $injector.get('$http'); - - try { - const optInStatus = await $http.post( - chrome.addBasePath('/api/telemetry/v2/clusters/_opt_in_stats'), - { - enabled, - unencrypted: false, - } - ); - - if (optInStatus.data && optInStatus.data.length) { - return await fetch(telemetryOptInStatusUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(optInStatus.data), - }); - } - } catch (err) { - // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. - // swallow any errors - } -} -export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInStatusChange = true) { - currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean; - - const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar( - 'allowChangingOptInStatus' - ) as boolean; - - telemetryNotifyUserAboutOptInDefault = npStart.core.injectedMetadata.getInjectedVar( - 'telemetryNotifyUserAboutOptInDefault' - ) as boolean; - - const provider = { - getBannerId: () => bannerId, - getOptInBannerNoticeId: () => optInBannerNoticeId, - getOptIn: () => currentOptInStatus, - canChangeOptInStatus: () => allowChangingOptInStatus, - notifyUserAboutOptInDefault: () => telemetryNotifyUserAboutOptInDefault, - setBannerId(id: string) { - bannerId = id; - }, - setOptInBannerNoticeId(id: string) { - optInBannerNoticeId = id; - }, - setOptInNoticeSeen: async () => { - const $http = $injector.get('$http'); - - // If they've seen the notice don't spam the API - if (!telemetryNotifyUserAboutOptInDefault) { - return telemetryNotifyUserAboutOptInDefault; - } - - if (optInBannerNoticeId) { - banners.remove(optInBannerNoticeId); - } - - try { - await $http.put(chrome.addBasePath('/api/telemetry/v2/userHasSeenNotice')); - telemetryNotifyUserAboutOptInDefault = false; - } catch (error) { - toastNotifications.addError(error, { - title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', { - defaultMessage: 'Error', - }), - toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', { - defaultMessage: 'An error occurred dismissing the notice', - }), - }); - telemetryNotifyUserAboutOptInDefault = true; - } - - return telemetryNotifyUserAboutOptInDefault; - }, - setOptIn: async (enabled: boolean) => { - if (!allowChangingOptInStatus) { - return; - } - const $http = $injector.get('$http'); - - try { - await $http.post(chrome.addBasePath('/api/telemetry/v2/optIn'), { enabled }); - if (sendOptInStatusChange) { - await sendOptInStatus($injector, chrome, enabled); - } - currentOptInStatus = enabled; - } catch (error) { - toastNotifications.addError(error, { - title: i18n.translate('telemetry.optInErrorToastTitle', { - defaultMessage: 'Error', - }), - toastMessage: i18n.translate('telemetry.optInErrorToastText', { - defaultMessage: - 'An error occurred while trying to set the usage statistics preference.', - }), - }); - return false; - } - - return true; - }, - fetchExample: async () => { - const $http = $injector.get('$http'); - return $http.post(chrome.addBasePath(`/api/telemetry/v2/clusters/_stats`), { - unencrypted: true, - timeRange: { - min: moment() - .subtract(20, 'minutes') - .toISOString(), - max: moment().toISOString(), - }, - }); - }, - }; - - return provider; -} diff --git a/src/legacy/core_plugins/telemetry/public/views/management/index.js b/src/legacy/core_plugins/telemetry/public/views/management/index.ts similarity index 100% rename from src/legacy/core_plugins/telemetry/public/views/management/index.js rename to src/legacy/core_plugins/telemetry/public/views/management/index.ts diff --git a/src/legacy/core_plugins/telemetry/public/views/management/management.js b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx similarity index 52% rename from src/legacy/core_plugins/telemetry/public/views/management/management.js rename to src/legacy/core_plugins/telemetry/public/views/management/management.tsx index 7032775e391bb..c8ae410e0aa57 100644 --- a/src/legacy/core_plugins/telemetry/public/views/management/management.js +++ b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx @@ -18,30 +18,32 @@ */ import React from 'react'; import routes from 'ui/routes'; - -import { npSetup } from 'ui/new_platform'; -import { TelemetryOptInProvider } from '../../services'; -import { TelemetryForm } from '../../components'; +import { npStart, npSetup } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TelemetryManagementSection } from '../../../../../../plugins/telemetry/public/components'; routes.defaults(/\/management/, { resolve: { - telemetryManagementSection: function(Private) { - const telemetryOptInProvider = Private(TelemetryOptInProvider); - const componentRegistry = npSetup.plugins.advancedSettings.component; + telemetryManagementSection() { + const { telemetry } = npStart.plugins as any; + const { advancedSettings } = npSetup.plugins as any; - const Component = props => ( - - ); + if (telemetry && advancedSettings) { + const componentRegistry = advancedSettings.component; + const Component = (props: any) => ( + + ); - componentRegistry.register( - componentRegistry.componentType.PAGE_FOOTER_COMPONENT, - Component, - true - ); + componentRegistry.register( + componentRegistry.componentType.PAGE_FOOTER_COMPONENT, + Component, + true + ); + } }, }, }); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index 99090cb2fb7ef..6919b6959aa8c 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -24,7 +24,6 @@ import { dirname, join } from 'path'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; -import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; /** @@ -85,7 +84,7 @@ export function createTelemetryUsageCollector( isReady: () => true, fetch: async () => { const config = server.config(); - const configPath = getXpackConfigWithDeprecated(config, 'telemetry.config') as string; + const configPath = config.get('telemetry.config') as string; const telemetryPath = join(dirname(configPath), 'telemetry.yml'); return await readTelemetryFile(telemetryPath); }, diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts index 9edd8457f2b89..6e16328c4abd8 100644 --- a/src/legacy/core_plugins/telemetry/server/fetcher.ts +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -24,7 +24,6 @@ import { telemetryCollectionManager } from './collection_manager'; import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository'; import { REPORT_INTERVAL_MS } from '../common/constants'; -import { getXpackConfigWithDeprecated } from '../common/get_xpack_config_with_deprecated'; export class FetcherTask { private readonly checkDurationMs = 60 * 1000 * 5; @@ -52,7 +51,7 @@ export class FetcherTask { const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom'); const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); const configTelemetryOptIn = config.get('telemetry.optIn'); - const telemetryUrl = getXpackConfigWithDeprecated(config, 'telemetry.url') as string; + const telemetryUrl = config.get('telemetry.url') as string; return { telemetryOptIn: getTelemetryOptIn({ diff --git a/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts new file mode 100644 index 0000000000000..b28a01bffa44d --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Clean up any old, deprecated settings and determine if we should continue. + * + * This will update the latest telemetry setting if necessary. + * + * @param {Object} config The advanced settings config object. + * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. + */ + +import { Server } from 'hapi'; +import { CONFIG_TELEMETRY } from '../../common/constants'; +import { updateTelemetrySavedObject } from '../telemetry_repository'; + +const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; + +export async function handleOldSettings(server: Server) { + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const savedObjectsClient = getSavedObjectsRepository(callWithInternalUser); + const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient }); + + const oldTelemetrySetting = await uiSettings.get(CONFIG_TELEMETRY); + const oldAllowReportSetting = await uiSettings.get(CONFIG_ALLOW_REPORT); + let legacyOptInValue = null; + + if (typeof oldTelemetrySetting === 'boolean') { + legacyOptInValue = oldTelemetrySetting; + } else if ( + typeof oldAllowReportSetting === 'boolean' && + uiSettings.isOverridden(CONFIG_ALLOW_REPORT) + ) { + legacyOptInValue = oldAllowReportSetting; + } + + if (legacyOptInValue !== null) { + await updateTelemetrySavedObject(savedObjectsClient, { + enabled: legacyOptInValue, + }); + } +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts similarity index 93% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js rename to src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts index ffb0e88c60a0d..77eae0d80db61 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { injectBanner } from './inject_banner'; +export { handleOldSettings } from './handle_old_settings'; diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 6c62d03adf25c..85d7d80234ffc 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -23,6 +23,7 @@ import * as constants from '../common/constants'; export { FetcherTask } from './fetcher'; export { replaceTelemetryInjectedVars } from './telemetry_config'; +export { handleOldSettings } from './handle_old_settings'; export { telemetryCollectionManager } from './collection_manager'; export { PluginsSetup } from './plugin'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index e300ce4a0caf8..ff8fc9b07879c 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -39,6 +39,7 @@ import { import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; +import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; import { NavigationPublicPluginSetup, NavigationPublicPluginStart, @@ -60,6 +61,7 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; + telemetry?: TelemetryPluginSetup; } export interface PluginsStart { @@ -77,6 +79,7 @@ export interface PluginsStart { share: SharePluginStart; management: ManagementStart; advancedSettings: AdvancedSettingsStart; + telemetry?: TelemetryPluginStart; } export const npSetup = { diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 2f2332bb06e3c..eebbc63f6f1e4 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -97,6 +97,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -738,6 +749,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -1361,6 +1383,17 @@ exports[`QueryStringInput Should pass the query language to the language switche "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -1999,6 +2032,17 @@ exports[`QueryStringInput Should pass the query language to the language switche "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -2622,6 +2666,17 @@ exports[`QueryStringInput Should render the given query 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -3260,6 +3315,17 @@ exports[`QueryStringInput Should render the given query 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js b/src/plugins/telemetry/common/constants.ts similarity index 61% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js rename to src/plugins/telemetry/common/constants.ts index 45539c4eea46c..7b7694ed9aed7 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js +++ b/src/plugins/telemetry/common/constants.ts @@ -18,13 +18,22 @@ */ /** - * Determine if the notice banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} telemetryOptInProvider The Telemetry opt-in provider singleton. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. + * The amount of time, in milliseconds, to wait between reports when enabled. + * Currently 24 hours. + */ +export const REPORT_INTERVAL_MS = 86400000; + +/* + * Key for the localStorage service + */ +export const LOCALSTORAGE_KEY = 'telemetry.data'; + +/** + * Link to Advanced Settings. + */ +export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings'; + +/** + * Link to the Elastic Telemetry privacy statement. */ -export async function shouldShowOptInBanner(telemetryOptInProvider) { - return telemetryOptInProvider.notifyUserAboutOptInDefault(); -} +export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`; diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json new file mode 100644 index 0000000000000..3a28149276c3e --- /dev/null +++ b/src/plugins/telemetry/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "telemetry", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap new file mode 100644 index 0000000000000..87e60869f6c21 --- /dev/null +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptInDetailsComponent renders as expected 1`] = ` + + } +> + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap similarity index 97% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap index c80485332fa8a..7fa69a7409c6a 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap @@ -9,6 +9,7 @@ exports[`OptInMessage renders as expected 1`] = ` Object { "privacyStatementLink": { + it('renders as expected', () => { + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + }); + + it('fires the "onChangeOptInClick" prop with true when a enable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + const enableButton = component.findWhere(n => { + const props = n.props(); + return n.type() === EuiButton && props['data-test-subj'] === 'enable'; + }); + + if (!enableButton) { + throw new Error(`Couldn't find any opt in enable button.`); + } + + enableButton.simulate('click'); + expect(onClick).toHaveBeenCalled(); + expect(onClick).toBeCalledWith(true); + }); + + it('fires the "onChangeOptInClick" with false when a disable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + const disableButton = component.findWhere(n => { + const props = n.props(); + return n.type() === EuiButton && props['data-test-subj'] === 'disable'; + }); + + if (!disableButton) { + throw new Error(`Couldn't find any opt in disable button.`); + } + + disableButton.simulate('click'); + expect(onClick).toHaveBeenCalled(); + expect(onClick).toBeCalledWith(false); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx b/src/plugins/telemetry/public/components/opt_in_banner.tsx similarity index 84% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx rename to src/plugins/telemetry/public/components/opt_in_banner.tsx index 2813af9c499e7..adf7b8bc84719 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_banner.tsx @@ -23,15 +23,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { OptInMessage } from './opt_in_message'; interface Props { - fetchTelemetry: () => Promise; - optInClick: (optIn: boolean) => void; + onChangeOptInClick: (isOptIn: boolean) => void; } -/** - * React component for displaying the Telemetry opt-in banner. - */ export class OptInBanner extends React.PureComponent { render() { + const { onChangeOptInClick } = this.props; const title = ( { ); return ( - + - this.props.optInClick(true)}> + onChangeOptInClick(true)}> { - this.props.optInClick(false)}> + onChangeOptInClick(false)}> { it('renders as expected', () => { expect( shallowWithIntl( - ({ data: [] }))} - onClose={jest.fn()} - /> + [])} onClose={jest.fn()} /> ) ).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx similarity index 91% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx rename to src/plugins/telemetry/public/components/opt_in_example_flyout.tsx index 12ab780e75990..9ecbd4df20560 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx @@ -37,7 +37,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; interface Props { - fetchTelemetry: () => Promise; + fetchExample: () => Promise; onClose: () => void; } @@ -57,22 +57,21 @@ export class OptInExampleFlyout extends React.PureComponent { hasPrivilegeToRead: false, }; - componentDidMount() { - this.props - .fetchTelemetry() - .then(response => - this.setState({ - data: Array.isArray(response.data) ? response.data : null, - isLoading: false, - hasPrivilegeToRead: true, - }) - ) - .catch(err => { - this.setState({ - isLoading: false, - hasPrivilegeToRead: err.status !== 403, - }); + async componentDidMount() { + try { + const { fetchExample } = this.props; + const clusters = await fetchExample(); + this.setState({ + data: Array.isArray(clusters) ? clusters : null, + isLoading: false, + hasPrivilegeToRead: true, }); + } catch (err) { + this.setState({ + isLoading: false, + hasPrivilegeToRead: err.status !== 403, + }); + } } renderBody({ data, isLoading, hasPrivilegeToRead }: State) { diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx b/src/plugins/telemetry/public/components/opt_in_message.test.tsx similarity index 89% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx rename to src/plugins/telemetry/public/components/opt_in_message.test.tsx index 1a9fabceda907..dbe0941345a02 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.test.tsx @@ -22,8 +22,6 @@ import { OptInMessage } from './opt_in_message'; describe('OptInMessage', () => { it('renders as expected', () => { - expect( - shallowWithIntl( [])} />) - ).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx b/src/plugins/telemetry/public/components/opt_in_message.tsx similarity index 81% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx rename to src/plugins/telemetry/public/components/opt_in_message.tsx index 4221d78516e10..590a115b2bb6c 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.tsx @@ -20,30 +20,9 @@ import * as React from 'react'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - import { PRIVACY_STATEMENT_URL } from '../../common/constants'; -interface Props { - fetchTelemetry: () => Promise; -} - -interface State { - showDetails: boolean; - showExample: boolean; -} - -export class OptInMessage extends React.PureComponent { - public readonly state: State = { - showDetails: false, - showExample: false, - }; - - toggleShowExample = () => { - this.setState(prevState => ({ - showExample: !prevState.showExample, - })); - }; - +export class OptInMessage extends React.PureComponent { render() { return ( @@ -52,7 +31,7 @@ export class OptInMessage extends React.PureComponent { defaultMessage="Want to help us improve the Elastic Stack? Data usage collection is currently disabled. Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details." values={{ privacyStatementLink: ( - + { it('renders as expected', () => { - expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); }); it('fires the "onSeenBanner" prop when a link is clicked', () => { const onLinkClick = jest.fn(); - const component = shallowWithIntl(); + const component = shallowWithIntl(); const button = component.findWhere(n => n.type() === EuiButton); diff --git a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx similarity index 75% rename from src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx rename to src/plugins/telemetry/public/components/opted_in_notice_banner.tsx index e37fa73ebe7b8..090893964c881 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx +++ b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx @@ -20,35 +20,32 @@ /* eslint @elastic/eui/href-or-on-click:0 */ import * as React from 'react'; -import chrome from 'ui/chrome'; import { EuiButton, EuiLink, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PATH_TO_ADVANCED_SETTINGS } from '../../common/constants'; +import { i18n } from '@kbn/i18n'; +import { PATH_TO_ADVANCED_SETTINGS, PRIVACY_STATEMENT_URL } from '../../common/constants'; interface Props { onSeenBanner: () => any; } -/** - * React component for displaying the Telemetry opt-in notice. - */ -export class OptedInBanner extends React.PureComponent { - onLinkClick = () => { - this.props.onSeenBanner(); - return; - }; - +export class OptedInNoticeBanner extends React.PureComponent { render() { + const { onSeenBanner } = this.props; + const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', { + defaultMessage: 'Help us improve the Elastic Stack', + }); + return ( - + @@ -59,10 +56,7 @@ export class OptedInBanner extends React.PureComponent { ), disableLink: ( - + { }} /> - + void; + showAppliesSettingMessage: boolean; + enableSaving: boolean; + query?: any; +} + +interface State { + processing: boolean; + showExample: boolean; + queryMatches: boolean | null; +} - state = { +export class TelemetryManagementSection extends Component { + state: State = { processing: false, showExample: false, queryMatches: null, }; - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { const { query } = nextProps; const searchTerm = (query.text || '').toLowerCase(); @@ -71,11 +78,10 @@ export class TelemetryForm extends Component { } render() { - const { telemetryOptInProvider } = this.props; - + const { telemetryService } = this.props; const { showExample, queryMatches } = this.state; - if (!telemetryOptInProvider.canChangeOptInStatus()) { + if (!telemetryService.getCanChangeOptInStatus()) { return null; } @@ -87,7 +93,7 @@ export class TelemetryForm extends Component { {showExample && ( telemetryOptInProvider.fetchExample()} + fetchExample={telemetryService.fetchExample} onClose={this.toggleExample} /> )} @@ -106,15 +112,23 @@ export class TelemetryForm extends Component { {this.maybeGetAppliesSettingMessage()} ); - toggleOptIn = async () => { - const newOptInValue = !this.props.telemetryOptInProvider.getOptIn(); + toggleOptIn = async (): Promise => { + const { telemetryService } = this.props; + const newOptInValue = !telemetryService.getIsOptedIn(); return new Promise((resolve, reject) => { - this.setState( - { - enabled: newOptInValue, - processing: true, - }, - () => { - this.props.telemetryOptInProvider.setOptIn(newOptInValue).then( - () => { - this.setState({ processing: false }); - resolve(); - }, - e => { - // something went wrong - this.setState({ processing: false }); - reject(e); - } - ); + this.setState({ processing: true }, async () => { + try { + await telemetryService.setOptIn(newOptInValue); + this.setState({ processing: false }); + resolve(true); + } catch (err) { + this.setState({ processing: false }); + reject(err); } - ); + }); }); }; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js b/src/plugins/telemetry/public/index.ts similarity index 81% rename from src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js rename to src/plugins/telemetry/public/index.ts index 4e53c7ecd7030..2f86d7749bb9b 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js +++ b/src/plugins/telemetry/public/index.ts @@ -17,8 +17,9 @@ * under the License. */ -import { uiModules } from 'ui/modules'; +import { TelemetryPlugin } from './plugin'; +export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; -import { injectBanner } from './welcome_banner'; - -uiModules.get('telemetry/hacks').run(injectBanner); +export function plugin() { + return new TelemetryPlugin(); +} diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts new file mode 100644 index 0000000000000..93dc13c327509 --- /dev/null +++ b/src/plugins/telemetry/public/mocks.ts @@ -0,0 +1,85 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../core/public/overlays/overlay_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { httpServiceMock } from '../../../core/public/http/http_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { injectedMetadataServiceMock } from '../../../core/public/injected_metadata/injected_metadata_service.mock'; +import { TelemetryService } from './services/telemetry_service'; +import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications'; +import { TelemetryPluginStart } from './plugin'; + +export function mockTelemetryService({ + reportOptInStatusChange, +}: { reportOptInStatusChange?: boolean } = {}) { + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getInjectedVar.mockImplementation((key: string) => { + switch (key) { + case 'telemetryNotifyUserAboutOptInDefault': + return true; + case 'allowChangingOptInStatus': + return true; + case 'telemetryOptedIn': + return true; + default: { + throw Error(`Unhandled getInjectedVar key "${key}".`); + } + } + }); + + return new TelemetryService({ + injectedMetadata, + http: httpServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + reportOptInStatusChange, + }); +} + +export function mockTelemetryNotifications({ + telemetryService, +}: { + telemetryService: TelemetryService; +}) { + return new TelemetryNotifications({ + overlays: overlayServiceMock.createStartContract(), + telemetryService, + }); +} + +export type Setup = jest.Mocked; + +export const telemetryPluginMock = { + createSetupContract, +}; + +function createSetupContract(): Setup { + const telemetryService = mockTelemetryService(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + + const setupContract: Setup = { + telemetryService, + telemetryNotifications, + }; + + return setupContract; +} diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts new file mode 100644 index 0000000000000..7ba51cacd1949 --- /dev/null +++ b/src/plugins/telemetry/public/plugin.ts @@ -0,0 +1,118 @@ +/* + * 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 { Plugin, CoreStart, CoreSetup, HttpStart } from '../../../core/public'; + +import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; + +export interface TelemetryPluginSetup { + telemetryService: TelemetryService; +} + +export interface TelemetryPluginStart { + telemetryService: TelemetryService; + telemetryNotifications: TelemetryNotifications; +} + +export class TelemetryPlugin implements Plugin { + private telemetrySender?: TelemetrySender; + private telemetryNotifications?: TelemetryNotifications; + private telemetryService?: TelemetryService; + + public setup({ http, injectedMetadata, notifications }: CoreSetup): TelemetryPluginSetup { + this.telemetryService = new TelemetryService({ + http, + injectedMetadata, + notifications, + }); + + this.telemetrySender = new TelemetrySender(this.telemetryService); + + return { + telemetryService: this.telemetryService, + }; + } + + public start({ injectedMetadata, http, overlays, application }: CoreStart): TelemetryPluginStart { + if (!this.telemetryService) { + throw Error('Telemetry plugin failed to initialize properly.'); + } + + const telemetryBanner = injectedMetadata.getInjectedVar('telemetryBanner') as boolean; + const sendUsageFrom = injectedMetadata.getInjectedVar('telemetrySendUsageFrom') as + | 'browser' + | 'server'; + + this.telemetryNotifications = new TelemetryNotifications({ + overlays, + telemetryService: this.telemetryService, + }); + + application.currentAppId$.subscribe(appId => { + const isUnauthenticated = this.getIsUnauthenticated(http); + if (isUnauthenticated) { + return; + } + + this.maybeStartTelemetryPoller({ sendUsageFrom }); + if (telemetryBanner) { + this.maybeShowOptedInNotificationBanner(); + this.maybeShowOptInBanner(); + } + }); + + return { + telemetryService: this.telemetryService, + telemetryNotifications: this.telemetryNotifications, + }; + } + + private getIsUnauthenticated(http: HttpStart) { + const { anonymousPaths } = http; + return anonymousPaths.isAnonymous(window.location.pathname); + } + + private maybeStartTelemetryPoller({ sendUsageFrom }: { sendUsageFrom: string }) { + if (!this.telemetrySender) { + return; + } + if (sendUsageFrom === 'browser') { + this.telemetrySender.startChecking(); + } + } + + private maybeShowOptedInNotificationBanner() { + if (!this.telemetryNotifications) { + return; + } + const shouldShowBanner = this.telemetryNotifications.shouldShowOptedInNoticeBanner(); + if (shouldShowBanner) { + this.telemetryNotifications.renderOptedInNoticeBanner(); + } + } + + private maybeShowOptInBanner() { + if (!this.telemetryNotifications) { + return; + } + const shouldShowBanner = this.telemetryNotifications.shouldShowOptInBanner(); + if (shouldShowBanner) { + this.telemetryNotifications.renderOptInBanner(); + } + } +} diff --git a/src/plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/index.ts new file mode 100644 index 0000000000000..ff4404c626fe0 --- /dev/null +++ b/src/plugins/telemetry/public/services/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { TelemetrySender } from './telemetry_sender'; +export { TelemetryService } from './telemetry_service'; +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/legacy/core_plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts similarity index 88% rename from src/legacy/core_plugins/telemetry/public/services/index.ts rename to src/plugins/telemetry/public/services/telemetry_notifications/index.ts index 8b02f8ce4c5b0..c6ba2cce1edb0 100644 --- a/src/legacy/core_plugins/telemetry/public/services/index.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { TelemetryOptInProvider } from './telemetry_opt_in'; -export { isUnauthenticated } from './path'; +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts similarity index 56% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts index f40e0b188c198..020d8023b6003 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts @@ -17,24 +17,27 @@ * under the License. */ -import '../../services/telemetry_opt_in.test.mocks'; -import { renderOptedInBanner } from './render_notice_banner'; +import { renderOptInBanner } from './render_opt_in_banner'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock'; -describe('render_notice_banner', () => { +describe('renderOptInBanner', () => { it('adds a banner to banners with priority of 10000', () => { const bannerID = 'brucer-wayne'; + const overlays = overlayServiceMock.createStartContract(); + overlays.banners.add.mockReturnValue(bannerID); - const telemetryOptInProvider = { setOptInBannerNoticeId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; + const returnedBannerId = renderOptInBanner({ + setOptIn: jest.fn(), + overlays, + }); - renderOptedInBanner(telemetryOptInProvider, { _banners: banners }); + expect(overlays.banners.add).toBeCalledTimes(1); - expect(banners.add).toBeCalledTimes(1); - expect(telemetryOptInProvider.setOptInBannerNoticeId).toBeCalledWith(bannerID); + expect(returnedBannerId).toBe(bannerID); + const bannerConfig = overlays.banners.add.mock.calls[0]; - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); + expect(bannerConfig[0]).not.toBe(undefined); + expect(bannerConfig[1]).toBe(10000); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx new file mode 100644 index 0000000000000..6e0164df6403a --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { CoreStart } from 'kibana/public'; +import { OptInBanner } from '../../components/opt_in_banner'; +import { toMountPoint } from '../../../../kibana_react/public'; + +interface RenderBannerConfig { + overlays: CoreStart['overlays']; + setOptIn: (isOptIn: boolean) => Promise; +} + +export function renderOptInBanner({ setOptIn, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts similarity index 52% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts index b4a86b36d922f..2d175024a74fb 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts @@ -17,26 +17,27 @@ * under the License. */ -import '../../services/telemetry_opt_in.test.mocks'; -import { renderBanner } from './render_banner'; +import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock'; -describe('render_banner', () => { +describe('renderOptedInNoticeBanner', () => { it('adds a banner to banners with priority of 10000', () => { - const bannerID = 'brucer-banner'; + const bannerID = 'brucer-wayne'; + const overlays = overlayServiceMock.createStartContract(); + overlays.banners.add.mockReturnValue(bannerID); - const telemetryOptInProvider = { setBannerId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; - const fetchTelemetry = jest.fn(); + const returnedBannerId = renderOptedInNoticeBanner({ + onSeen: jest.fn(), + overlays, + }); - renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners: banners }); + expect(overlays.banners.add).toBeCalledTimes(1); - expect(banners.add).toBeCalledTimes(1); - expect(fetchTelemetry).toBeCalledTimes(0); - expect(telemetryOptInProvider.setBannerId).toBeCalledWith(bannerID); + expect(returnedBannerId).toBe(bannerID); + const bannerConfig = overlays.banners.add.mock.calls[0]; - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); + expect(bannerConfig[0]).not.toBe(undefined); + expect(bannerConfig[1]).toBe(10000); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/services/path.ts b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx similarity index 59% rename from src/legacy/core_plugins/telemetry/public/services/path.ts rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx index 4af545e982eaa..e63e46af6e8ca 100644 --- a/src/legacy/core_plugins/telemetry/public/services/path.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx @@ -17,9 +17,18 @@ * under the License. */ -import chrome from 'ui/chrome'; +import React from 'react'; +import { CoreStart } from 'kibana/public'; +import { OptedInNoticeBanner } from '../../components/opted_in_notice_banner'; +import { toMountPoint } from '../../../../kibana_react/public'; -export function isUnauthenticated() { - const path = (chrome as any).removeBasePath(window.location.pathname); - return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status'; +interface RenderBannerConfig { + overlays: CoreStart['overlays']; + onSeen: () => void; +} +export function renderOptedInNoticeBanner({ onSeen, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; } diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts new file mode 100644 index 0000000000000..f767615d25253 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts @@ -0,0 +1,55 @@ +/* + * 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. + */ + +/* eslint-disable dot-notation */ +import { mockTelemetryNotifications, mockTelemetryService } from '../../mocks'; + +describe('onSetOptInClick', () => { + it('sets setting successfully and removes banner', async () => { + const optIn = true; + const bannerId = 'bruce-banner'; + + const telemetryService = mockTelemetryService(); + telemetryService.setOptIn = jest.fn(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optInBannerId'] = bannerId; + + await telemetryNotifications['onSetOptInClick'](optIn); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId); + expect(telemetryService.setOptIn).toBeCalledTimes(1); + expect(telemetryService.setOptIn).toBeCalledWith(optIn); + }); +}); + +describe('setOptedInNoticeSeen', () => { + it('sets setting successfully and removes banner', async () => { + const bannerId = 'bruce-banner'; + + const telemetryService = mockTelemetryService(); + telemetryService.setUserHasSeenNotice = jest.fn(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optedInNoticeBannerId'] = bannerId; + await telemetryNotifications.setOptedInNoticeSeen(); + + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId); + expect(telemetryService.setUserHasSeenNotice).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts new file mode 100644 index 0000000000000..bf25bb592db82 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts @@ -0,0 +1,88 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner'; +import { renderOptInBanner } from './render_opt_in_banner'; +import { TelemetryService } from '../telemetry_service'; + +interface TelemetryNotificationsConstructor { + overlays: CoreStart['overlays']; + telemetryService: TelemetryService; +} + +export class TelemetryNotifications { + private readonly overlays: CoreStart['overlays']; + private readonly telemetryService: TelemetryService; + private optedInNoticeBannerId?: string; + private optInBannerId?: string; + + constructor({ overlays, telemetryService }: TelemetryNotificationsConstructor) { + this.telemetryService = telemetryService; + this.overlays = overlays; + } + + public shouldShowOptedInNoticeBanner = (): boolean => { + const userHasSeenOptedInNotice = this.telemetryService.getUserHasSeenOptedInNotice(); + const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined'; + return !bannerOnScreen && userHasSeenOptedInNotice; + }; + + public renderOptedInNoticeBanner = (): void => { + const bannerId = renderOptedInNoticeBanner({ + onSeen: this.setOptedInNoticeSeen, + overlays: this.overlays, + }); + + this.optedInNoticeBannerId = bannerId; + }; + + public shouldShowOptInBanner = (): boolean => { + const isOptedIn = this.telemetryService.getIsOptedIn(); + const bannerOnScreen = typeof this.optInBannerId !== 'undefined'; + return !bannerOnScreen && isOptedIn === null; + }; + + public renderOptInBanner = (): void => { + const bannerId = renderOptInBanner({ + setOptIn: this.onSetOptInClick, + overlays: this.overlays, + }); + + this.optInBannerId = bannerId; + }; + + private onSetOptInClick = async (isOptIn: boolean) => { + if (this.optInBannerId) { + this.overlays.banners.remove(this.optInBannerId); + this.optInBannerId = undefined; + } + + await this.telemetryService.setOptIn(isOptIn); + }; + + public setOptedInNoticeSeen = async (): Promise => { + if (this.optedInNoticeBannerId) { + this.overlays.banners.remove(this.optedInNoticeBannerId); + this.optedInNoticeBannerId = undefined; + } + + await this.telemetryService.setUserHasSeenNotice(); + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_sender.test.ts b/src/plugins/telemetry/public/services/telemetry_sender.test.ts new file mode 100644 index 0000000000000..e9f5765c10412 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.test.ts @@ -0,0 +1,272 @@ +/* + * 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. + */ + +/* eslint-disable dot-notation */ +import { TelemetrySender } from './telemetry_sender'; +import { mockTelemetryService } from '../mocks'; +import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; + +class LocalStorageMock implements Partial { + getItem = jest.fn(); + setItem = jest.fn(); +} + +describe('TelemetrySender', () => { + let originalLocalStorage: Storage; + let mockLocalStorage: LocalStorageMock; + beforeAll(() => { + originalLocalStorage = window.localStorage; + }); + + // @ts-ignore + beforeEach(() => (window.localStorage = mockLocalStorage = new LocalStorageMock())); + // @ts-ignore + afterAll(() => (window.localStorage = originalLocalStorage)); + + describe('constructor', () => { + it('defaults lastReport if unset', () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(mockLocalStorage.getItem).toBeCalledTimes(1); + expect(mockLocalStorage.getItem).toHaveBeenCalledWith(LOCALSTORAGE_KEY); + }); + + it('uses lastReport if set', () => { + const lastReport = `${Date.now()}`; + mockLocalStorage.getItem.mockReturnValueOnce(JSON.stringify({ lastReport })); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + expect(telemetrySender['lastReported']).toBe(lastReport); + }); + }); + + describe('saveToBrowser', () => { + it('uses lastReport', () => { + const lastReport = `${Date.now()}`; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = lastReport; + telemetrySender['saveToBrowser'](); + + expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + LOCALSTORAGE_KEY, + JSON.stringify({ lastReport }) + ); + }); + }); + + describe('shouldSendReport', () => { + it('returns false whenever optIn is false', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); + const telemetrySender = new TelemetrySender(telemetryService); + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + + expect(telemetryService.getIsOptedIn).toBeCalledTimes(1); + expect(shouldSendRerpot).toBe(false); + }); + + it('returns true if lastReported is undefined', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(shouldSendRerpot).toBe(true); + }); + + it('returns true if lastReported passed REPORT_INTERVAL_MS', () => { + const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000); + + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `${lastReported}`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(true); + }); + + it('returns false if lastReported is within REPORT_INTERVAL_MS', () => { + const lastReported = Date.now() + 1000; + + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `${lastReported}`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(false); + }); + + it('returns true if lastReported is malformed', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `random_malformed_string`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(true); + }); + + describe('sendIfDue', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; + + beforeAll(() => { + originalFetch = window.fetch; + }); + + // @ts-ignore + beforeEach(() => (window.fetch = mockFetch = jest.fn())); + // @ts-ignore + afterAll(() => (window.fetch = originalFetch)); + + it('does not send if already sending', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn(); + telemetrySender['isSending'] = true; + await telemetrySender['sendIfDue'](); + + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(0); + expect(mockFetch).toBeCalledTimes(0); + }); + + it('does not send if shouldSendReport returns false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(0); + }); + + it('sends report if due', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(1); + expect(mockFetch).toBeCalledWith(mockTelemetryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: mockTelemetryPayload[0], + }); + }); + + it('sends report separately for every cluster', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + }); + + it('updates last lastReported and calls saveToBrowser', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); + + await telemetrySender['sendIfDue'](); + + expect(mockFetch).toBeCalledTimes(1); + expect(telemetrySender['lastReported']).toBeDefined(); + expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); + expect(telemetrySender['isSending']).toBe(false); + }); + + it('catches fetchTelemetry errors and sets isSending to false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); + }); + await telemetrySender['sendIfDue'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(telemetrySender['isSending']).toBe(false); + }); + + it('catches fetch errors and sets isSending to false', async () => { + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + mockFetch.mockImplementation(() => { + throw Error('Error sending usage'); + }); + await telemetrySender['sendIfDue'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(telemetrySender['isSending']).toBe(false); + }); + }); + }); + describe('startChecking', () => { + let originalSetInterval: typeof window['setInterval']; + let mockSetInterval: jest.Mock; + + beforeAll(() => { + originalSetInterval = window.setInterval; + }); + + // @ts-ignore + beforeEach(() => (window.setInterval = mockSetInterval = jest.fn())); + // @ts-ignore + afterAll(() => (window.setInterval = originalSetInterval)); + + it('calls sendIfDue every 60000 ms', () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender.startChecking(); + expect(mockSetInterval).toBeCalledTimes(1); + expect(mockSetInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts new file mode 100644 index 0000000000000..fec2db0506eb7 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.ts @@ -0,0 +1,100 @@ +/* + * 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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; +import { TelemetryService } from './telemetry_service'; +import { Storage } from '../../../kibana_utils/public'; + +export class TelemetrySender { + private readonly telemetryService: TelemetryService; + private isSending: boolean = false; + private lastReported?: string; + private readonly storage: Storage; + private intervalId?: number; + + constructor(telemetryService: TelemetryService) { + this.telemetryService = telemetryService; + this.storage = new Storage(window.localStorage); + + const attributes = this.storage.get(LOCALSTORAGE_KEY); + if (attributes) { + this.lastReported = attributes.lastReport; + } + } + + private saveToBrowser = () => { + // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object + this.storage.set(LOCALSTORAGE_KEY, { lastReport: this.lastReported }); + }; + + private shouldSendReport = (): boolean => { + // check if opt-in for telemetry is enabled + if (this.telemetryService.getIsOptedIn()) { + if (!this.lastReported) { + return true; + } + // returns NaN for any malformed or unset (null/undefined) value + const lastReported = parseInt(this.lastReported, 10); + // If it's been a day since we last sent telemetry + if (isNaN(lastReported) || Date.now() - lastReported > REPORT_INTERVAL_MS) { + return true; + } + } + + return false; + }; + + private sendIfDue = async (): Promise => { + if (this.isSending || !this.shouldSendReport()) { + return; + } + + // mark that we are working so future requests are ignored until we're done + this.isSending = true; + try { + const telemetryUrl = this.telemetryService.getTelemetryUrl(); + const telemetryData: any | any[] = await this.telemetryService.fetchTelemetry(); + const clusters: string[] = [].concat(telemetryData); + await Promise.all( + clusters.map( + async cluster => + await fetch(telemetryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: cluster, + }) + ) + ); + this.lastReported = `${Date.now()}`; + this.saveToBrowser(); + } catch (err) { + // ignore err + } finally { + this.isSending = false; + } + }; + + public startChecking = () => { + if (typeof this.intervalId === 'undefined') { + this.intervalId = window.setInterval(this.sendIfDue, 60000); + } + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts new file mode 100644 index 0000000000000..0ebcd52f1423c --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -0,0 +1,139 @@ +/* + * 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. + */ + +/* eslint-disable dot-notation */ +import { mockTelemetryService } from '../mocks'; + +const mockSubtract = jest.fn().mockImplementation(() => { + return { + toISOString: jest.fn(), + }; +}); + +jest.mock('moment', () => { + return jest.fn().mockImplementation(() => { + return { + subtract: mockSubtract, + toISOString: jest.fn(), + }; + }); +}); + +describe('TelemetryService', () => { + describe('fetchTelemetry', () => { + it('calls expected URL with 20 minutes - now', async () => { + const telemetryService = mockTelemetryService(); + await telemetryService.fetchTelemetry(); + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { + body: JSON.stringify({ unencrypted: false, timeRange: {} }), + }); + expect(mockSubtract).toBeCalledWith(20, 'minutes'); + }); + }); + + describe('fetchExample', () => { + it('calls fetchTelemetry with unencrupted: true', async () => { + const telemetryService = mockTelemetryService(); + telemetryService.fetchTelemetry = jest.fn(); + await telemetryService.fetchExample(); + expect(telemetryService.fetchTelemetry).toBeCalledWith({ unencrypted: true }); + }); + }); + + describe('setOptIn', () => { + it('calls api if canChangeOptInStatus', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + await telemetryService.setOptIn(true); + + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('sends enabled true if optedIn: true', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const optedIn = true; + await telemetryService.setOptIn(optedIn); + + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + }); + + it('sends enabled false if optedIn: false', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const optedIn = false; + await telemetryService.setOptIn(optedIn); + + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + }); + + it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + await telemetryService.setOptIn(true); + + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('calls reportOptInStatus if reportOptInStatusChange is true', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + await telemetryService.setOptIn(true); + + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('adds an error toast on api error', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { + if (url === '/api/telemetry/v2/optIn') { + throw Error('failed to update opt in.'); + } + }); + + await telemetryService.setOptIn(true); + expect(telemetryService['http'].post).toBeCalledTimes(1); + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + }); + + it('adds an error toast on reportOptInStatus error', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => { + throw Error('failed to report OptIn Status.'); + }); + + await telemetryService.setOptIn(true); + expect(telemetryService['http'].post).toBeCalledTimes(1); + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts new file mode 100644 index 0000000000000..073886e7d1327 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -0,0 +1,165 @@ +/* + * 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 moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +interface TelemetryServiceConstructor { + http: CoreStart['http']; + injectedMetadata: CoreStart['injectedMetadata']; + notifications: CoreStart['notifications']; + reportOptInStatusChange?: boolean; +} + +export class TelemetryService { + private readonly http: CoreStart['http']; + private readonly injectedMetadata: CoreStart['injectedMetadata']; + private readonly reportOptInStatusChange: boolean; + private readonly notifications: CoreStart['notifications']; + private isOptedIn: boolean | null; + private userHasSeenOptedInNotice: boolean; + + constructor({ + http, + injectedMetadata, + notifications, + reportOptInStatusChange = true, + }: TelemetryServiceConstructor) { + const isOptedIn = injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean | null; + const userHasSeenOptedInNotice = injectedMetadata.getInjectedVar( + 'telemetryNotifyUserAboutOptInDefault' + ) as boolean; + this.reportOptInStatusChange = reportOptInStatusChange; + this.injectedMetadata = injectedMetadata; + this.notifications = notifications; + this.http = http; + + this.isOptedIn = isOptedIn; + this.userHasSeenOptedInNotice = userHasSeenOptedInNotice; + } + + public getCanChangeOptInStatus = () => { + const allowChangingOptInStatus = this.injectedMetadata.getInjectedVar( + 'allowChangingOptInStatus' + ) as boolean; + return allowChangingOptInStatus; + }; + + public getOptInStatusUrl = () => { + const telemetryOptInStatusUrl = this.injectedMetadata.getInjectedVar( + 'telemetryOptInStatusUrl' + ) as string; + return telemetryOptInStatusUrl; + }; + + public getTelemetryUrl = () => { + const telemetryUrl = this.injectedMetadata.getInjectedVar('telemetryUrl') as string; + return telemetryUrl; + }; + + public getUserHasSeenOptedInNotice = () => { + return this.userHasSeenOptedInNotice; + }; + + public getIsOptedIn = () => { + return this.isOptedIn; + }; + + public fetchExample = async () => { + return await this.fetchTelemetry({ unencrypted: true }); + }; + + public fetchTelemetry = async ({ unencrypted = false } = {}) => { + const now = moment(); + return this.http.post('/api/telemetry/v2/clusters/_stats', { + body: JSON.stringify({ + unencrypted, + timeRange: { + min: now.subtract(20, 'minutes').toISOString(), + max: now.toISOString(), + }, + }), + }); + }; + + public setOptIn = async (optedIn: boolean): Promise => { + const canChangeOptInStatus = this.getCanChangeOptInStatus(); + if (!canChangeOptInStatus) { + return false; + } + + try { + await this.http.post('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + if (this.reportOptInStatusChange) { + await this.reportOptInStatus(optedIn); + } + this.isOptedIn = optedIn; + } catch (err) { + this.notifications.toasts.addError(err, { + title: i18n.translate('telemetry.optInErrorToastTitle', { + defaultMessage: 'Error', + }), + toastMessage: i18n.translate('telemetry.optInErrorToastText', { + defaultMessage: 'An error occurred while trying to set the usage statistics preference.', + }), + }); + + return false; + } + + return true; + }; + + public setUserHasSeenNotice = async (): Promise => { + try { + await this.http.put('/api/telemetry/v2/userHasSeenNotice'); + this.userHasSeenOptedInNotice = true; + } catch (error) { + this.notifications.toasts.addError(error, { + title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', { + defaultMessage: 'Error', + }), + toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', { + defaultMessage: 'An error occurred dismissing the notice', + }), + }); + this.userHasSeenOptedInNotice = false; + } + }; + + private reportOptInStatus = async (OptInStatus: boolean): Promise => { + const telemetryOptInStatusUrl = this.getOptInStatusUrl(); + + try { + await fetch(telemetryOptInStatusUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled: OptInStatus }), + }); + } catch (err) { + // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. + // swallow any errors + } + }; +} diff --git a/test/functional/config.ie.js b/test/functional/config.ie.js index 5e8ea56a848dc..2c32ccb69db03 100644 --- a/test/functional/config.ie.js +++ b/test/functional/config.ie.js @@ -35,7 +35,6 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, 'notifications:lifetime:info': 10000, }, @@ -43,7 +42,11 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/test/functional/config.js b/test/functional/config.js index 134ddf4e84b2d..155e844578c54 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -44,14 +44,17 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...commonConfig.get('kbnTestServer'), - serverArgs: [...commonConfig.get('kbnTestServer.serverArgs'), '--oss'], + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + '--oss', + '--telemetry.optIn=false', + ], }, uiSettings: { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, }, }, diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 40a52f88dbbba..6bac5e181861d 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -4,6 +4,7 @@ /test/functional/failure_debug /test/functional/screenshots /test/functional/apps/reporting/reports/session +/test/reporting/configs/failure_debug/ /legacy/plugins/reporting/.chromium/ /legacy/plugins/reporting/.phantom/ /.aws-config.json diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap deleted file mode 100644 index 575c47205f9c0..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap +++ /dev/null @@ -1,576 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryOptIn should display when telemetry not opted in 1`] = ` - - -
- - -

- - Help Elastic support provide better service - -

-
- -
- - - - - - } - className="eui-AlignBaseline" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="readMorePopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - -

- - - , - "telemetryPrivacyStatementLink": - - , - } - } - /> -

-
- , - } - } - /> - - } - onChange={[Function]} - > -
- -
- -
- - -`; - -exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = ` - -`; - -exports[`TelemetryOptIn shouldn't display when telemetry optIn status can't change 1`] = ` - -`; 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 353dc58e6d401..3bb8e4f8608a7 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 @@ -965,7 +965,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem className="euiSpacer euiSpacer--m" /> - @@ -1434,7 +1433,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 className="euiSpacer euiSpacer--m" /> - @@ -1903,7 +1901,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 className="euiSpacer euiSpacer--m" /> - @@ -2368,7 +2365,6 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] className="euiSpacer euiSpacer--m" /> - @@ -2837,7 +2833,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` className="euiSpacer euiSpacer--m" /> - diff --git a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js deleted file mode 100644 index 1b03ce869e52b..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js +++ /dev/null @@ -1,43 +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 React from 'react'; -import { - setTelemetryEnabled, - setTelemetryOptInService, -} from '../public/np_ready/application/lib/telemetry'; -import { TelemetryOptIn } from '../public/np_ready/application/components/telemetry_opt_in'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; - -jest.mock('ui/new_platform'); - -setTelemetryEnabled(true); - -describe('TelemetryOptIn', () => { - test('should display when telemetry not opted in', () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); - test('should not display when telemetry is opted in', () => { - setTelemetryOptInService({ - getOptIn: () => true, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); - test(`shouldn't display when telemetry optIn status can't change`, () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => false, - }); - const rendered = mountWithIntl(); - expect(rendered).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js index 7c497518b9df5..6a6c38fa6abb6 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js @@ -18,7 +18,7 @@ export class App extends Component { } render() { - const { hasPermission, permissionsLoading, permissionsError } = this.props; + const { hasPermission, permissionsLoading, permissionsError, telemetry } = this.props; if (permissionsLoading) { return ( @@ -85,11 +85,12 @@ export class App extends Component { ); } + const withTelemetry = Component => props => ; return ( - - + + ); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx index 2780b54230eba..49bb4ce984e48 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import * as history from 'history'; import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public'; +import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; // @ts-ignore import { App } from './app.container'; // @ts-ignore @@ -34,10 +35,11 @@ interface AppDependencies { toasts: ToastsSetup; docLinks: DocLinksStart; http: HttpSetup; + telemetry?: TelemetryPluginSetup; } export const boot = (deps: AppDependencies) => { - const { I18nContext, element, legacy, toasts, docLinks, http, chrome } = deps; + const { I18nContext, element, legacy, toasts, docLinks, http, chrome, telemetry } = deps; const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; const securityDocumentationLink = `${esBase}/security-settings.html`; @@ -56,15 +58,17 @@ export const boot = (deps: AppDependencies) => { toasts, http, chrome, + telemetry, MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB, }; const store = licenseManagementStore(initialState, services); + render( - + , diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx similarity index 84% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx index 5e570ae955dbf..eff5c6cc21c43 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx @@ -6,26 +6,31 @@ import React, { Fragment } from 'react'; import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { - shouldShowTelemetryOptIn, - getTelemetryFetcher, - PRIVACY_STATEMENT_URL, OptInExampleFlyout, + PRIVACY_STATEMENT_URL, + TelemetryPluginSetup, } from '../../lib/telemetry'; -import { FormattedMessage } from '@kbn/i18n/react'; -export class TelemetryOptIn extends React.Component { - constructor() { - super(); - this.state = { - showMoreTelemetryInfo: false, - isOptingInToTelemetry: false, - showExample: false, - }; - } - isOptingInToTelemetry = () => { - return this.state.isOptingInToTelemetry; +interface State { + showMoreTelemetryInfo: boolean; + showExample: boolean; +} + +interface Props { + onOptInChange: (isOptingInToTelemetry: boolean) => void; + isOptingInToTelemetry: boolean; + isStartTrial: boolean; + telemetry: TelemetryPluginSetup; +} + +export class TelemetryOptIn extends React.Component { + state: State = { + showMoreTelemetryInfo: false, + showExample: false, }; + closeReadMorePopover = () => { this.setState({ showMoreTelemetryInfo: false }); }; @@ -37,20 +42,22 @@ export class TelemetryOptIn extends React.Component { this.setState({ showExample: true }); this.closeReadMorePopover(); }; - onChangeOptIn = event => { + onChangeOptIn = (event: any) => { const isOptingInToTelemetry = event.target.checked; - this.setState({ isOptingInToTelemetry }); + const { onOptInChange } = this.props; + onOptInChange(isOptingInToTelemetry); }; + render() { - const { showMoreTelemetryInfo, isOptingInToTelemetry, showExample } = this.state; - const { isStartTrial } = this.props; + const { showMoreTelemetryInfo, showExample } = this.state; + const { isStartTrial, isOptingInToTelemetry, telemetry } = this.props; let example = null; if (showExample) { example = ( this.setState({ showExample: false })} - fetchTelemetry={getTelemetryFetcher} + fetchExample={telemetry.telemetryService.fetchExample} /> ); } @@ -123,7 +130,7 @@ export class TelemetryOptIn extends React.Component { ); - return shouldShowTelemetryOptIn() ? ( + return ( {example} {toCurrentCustomers} @@ -144,6 +151,6 @@ export class TelemetryOptIn extends React.Component { onChange={this.onChangeOptIn} /> - ) : null; + ); } } diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js deleted file mode 100644 index 10da5d7705a8c..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js +++ /dev/null @@ -1,36 +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 { fetchTelemetry } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry'; -export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/legacy/core_plugins/telemetry/common/constants'; -export { TelemetryOptInProvider } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/services'; -export { OptInExampleFlyout } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/components'; - -let telemetryEnabled; -let httpClient; -let telemetryOptInService; -export const setTelemetryEnabled = isTelemetryEnabled => { - telemetryEnabled = isTelemetryEnabled; -}; -export const setHttpClient = anHttpClient => { - httpClient = anHttpClient; -}; -export const setTelemetryOptInService = aTelemetryOptInService => { - telemetryOptInService = aTelemetryOptInService; -}; -export const optInToTelemetry = async enableTelemetry => { - await telemetryOptInService.setOptIn(enableTelemetry); -}; -export const shouldShowTelemetryOptIn = () => { - return ( - telemetryEnabled && - !telemetryOptInService.getOptIn() && - telemetryOptInService.canChangeOptInStatus() - ); -}; -export const getTelemetryFetcher = () => { - return fetchTelemetry(httpClient, { unencrypted: true }); -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts new file mode 100644 index 0000000000000..9cc4ec5978fdc --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.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 { TelemetryPluginSetup } from '../../../../../../../../src/plugins/telemetry/public'; + +export { OptInExampleFlyout } from '../../../../../../../../src/plugins/telemetry/public/components'; +export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/plugins/telemetry/common/constants'; +export { TelemetryPluginSetup, shouldShowTelemetryOptIn }; + +function shouldShowTelemetryOptIn( + telemetry?: TelemetryPluginSetup +): telemetry is TelemetryPluginSetup { + if (telemetry) { + const { telemetryService } = telemetry; + const isOptedIn = telemetryService.getIsOptedIn(); + const canChangeOptInStatus = telemetryService.getCanChangeOptInStatus(); + return canChangeOptInStatus && !isOptedIn; + } + + return false; +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js index e14d392fe6706..56c307a0d76e5 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js @@ -12,7 +12,7 @@ import { AddLicense } from './add_license'; import { RequestTrialExtension } from './request_trial_extension'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} }) => { +export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb: () => {} }) => { useEffect(() => { setBreadcrumb('dashboard'); }); @@ -25,7 +25,7 @@ export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} } - + diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts similarity index 95% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts index b9b33e7e3f2cb..1b3c956edc3ab 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore export { StartTrial } from './start_trial.container'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx similarity index 87% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx index 532c1d5e1a32f..e0f8ade8e45da 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import { EuiButtonEmpty, @@ -22,32 +22,56 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; -import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; -import { optInToTelemetry } from '../../../lib/telemetry'; import { FormattedMessage } from '@kbn/i18n/react'; +import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; import { EXTERNAL_LINKS } from '../../../../../../common/constants'; import { getDocLinks } from '../../../lib/docs_links'; +import { TelemetryPluginSetup, shouldShowTelemetryOptIn } from '../../../lib/telemetry'; + +interface Props { + loadTrialStatus: () => void; + startLicenseTrial: () => void; + telemetry?: TelemetryPluginSetup; + shouldShowStartTrial: boolean; +} + +interface State { + showConfirmation: boolean; + isOptingInToTelemetry: boolean; +} + +export class StartTrial extends Component { + cancelRef: any; + confirmRef: any; + + state: State = { + showConfirmation: false, + isOptingInToTelemetry: false, + }; -export class StartTrial extends React.PureComponent { - constructor(props) { - super(props); - this.state = { showConfirmation: false }; - } UNSAFE_componentWillMount() { this.props.loadTrialStatus(); } - startLicenseTrial = () => { - const { startLicenseTrial } = this.props; - if (this.telemetryOptIn.isOptingInToTelemetry()) { - optInToTelemetry(true); + + onOptInChange = (isOptingInToTelemetry: boolean) => { + this.setState({ isOptingInToTelemetry }); + }; + + onStartLicenseTrial = () => { + const { telemetry, startLicenseTrial } = this.props; + if (this.state.isOptingInToTelemetry && telemetry) { + telemetry.telemetryService.setOptIn(true); } startLicenseTrial(); }; + cancel = () => { this.setState({ showConfirmation: false }); }; acknowledgeModal() { - const { showConfirmation } = this.state; + const { showConfirmation, isOptingInToTelemetry } = this.state; + const { telemetry } = this.props; + if (!showConfirmation) { return null; } @@ -158,12 +182,14 @@ export class StartTrial extends React.PureComponent { - { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + + )} @@ -182,7 +208,7 @@ export class StartTrial extends React.PureComponent { { + this.setState({ isOptingInToTelemetry }); + }; send = acknowledge => { const file = this.file; const fr = new FileReader(); + fr.onload = ({ target: { result } }) => { - if (this.telemetryOptIn.isOptingInToTelemetry()) { - optInToTelemetry(true); + if (this.state.isOptingInToTelemetry) { + this.props.telemetry?.telemetryService.setOptIn(true); } this.props.uploadLicense(result, this.props.currentLicenseType, acknowledge); }; @@ -116,7 +124,8 @@ export class UploadLicense extends React.PureComponent { } }; render() { - const { currentLicenseType, applying } = this.props; + const { currentLicenseType, applying, telemetry } = this.props; + return ( @@ -170,11 +179,13 @@ export class UploadLicense extends React.PureComponent { - { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + + )} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts index 1da3c942830ca..60876c9b638d1 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts @@ -5,11 +5,12 @@ */ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { PLUGIN } from '../../common/constants'; import { Breadcrumb } from './application/breadcrumbs'; - export interface Plugins { + telemetry: TelemetryPluginSetup; __LEGACY: { xpackInfo: XPackMainPlugin; refreshXpack: () => void; @@ -18,7 +19,7 @@ export interface Plugins { } export class LicenseManagementUIPlugin implements Plugin { - setup({ application, notifications, http }: CoreSetup, { __LEGACY }: Plugins) { + setup({ application, notifications, http }: CoreSetup, { __LEGACY, telemetry }: Plugins) { application.register({ id: PLUGIN.ID, title: PLUGIN.TITLE, @@ -41,6 +42,7 @@ export class LicenseManagementUIPlugin implements Plugin { http, element, chrome, + telemetry, }); }, }); diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts index fc1678a866ad3..a8f27a7236a47 100644 --- a/x-pack/legacy/plugins/license_management/public/register_route.ts +++ b/x-pack/legacy/plugins/license_management/public/register_route.ts @@ -15,15 +15,6 @@ import routes from 'ui/routes'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { plugin } from './np_ready'; - -import { - setTelemetryOptInService, - setTelemetryEnabled, - setHttpClient, - TelemetryOptInProvider, - // @ts-ignore -} from './np_ready/application/lib/telemetry'; - import { BASE_PATH } from '../common/constants'; const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); @@ -51,15 +42,6 @@ if (licenseManagementUiEnabled) { }); }; - const initializeTelemetry = ($injector: any) => { - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - setTelemetryOptInService(telemetryOptInProvider); - setTelemetryEnabled(telemetryEnabled); - setHttpClient($injector.get('$http')); - }; - const template = `
`; @@ -69,8 +51,6 @@ if (licenseManagementUiEnabled) { controllerAs: 'licenseManagement', controller: class LicenseManagementController { constructor($injector: any, $rootScope: any, $scope: any, $route: any) { - initializeTelemetry($injector); - $scope.$$postDigest(() => { const element = document.getElementById('licenseReactRoot')!; @@ -94,6 +74,7 @@ if (licenseManagementUiEnabled) { }, }, { + telemetry: (npSetup.plugins as any).telemetry, __LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB }, } ); diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index f3994f7ebcc34..809d90d58d796 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -11,8 +11,6 @@ import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; -import { has } from 'lodash'; - export { callClusterFactory } from './server/lib/call_cluster_factory'; import { registerMonitoringCollection } from './server/telemetry_collection'; @@ -82,21 +80,5 @@ export const xpackMain = kibana => { xpackInfoRoute(server); settingsRoute(server, this.kbnServer); }, - deprecations: () => { - function movedToTelemetry(configPath) { - return (settings, log) => { - if (has(settings, configPath)) { - log( - `Config key "xpack.xpack_main.${configPath}" is deprecated. Use "telemetry.${configPath}" instead.` - ); - } - }; - } - return [ - movedToTelemetry('telemetry.config'), - movedToTelemetry('telemetry.url'), - movedToTelemetry('telemetry.enabled'), - ]; - }, }); }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dbc6a015f9c97..6bcf61b53fd5f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "収集されるデータの例を見る", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", - "telemetry.telemetryConfigTitle": "遠隔測定オプトイン", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "Kibana と Elasticsearch が現在も実行中であることを確認し、再試行してください。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "遠隔測定設定を保存できません。", - "telemetry.telemetryErrorNotificationMessageTitle": "遠隔測定エラー", "telemetry.telemetryOptedInDisableUsage": "ここで使用状況データを無効にする", "telemetry.telemetryOptedInDismissMessage": "閉じる", "telemetry.telemetryOptedInNoticeDescription": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については、{privacyStatementLink}をご覧ください。収集を停止するには、{disableLink}。", @@ -13187,4 +13183,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4a2c33eba79da..25382221716dd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "查看我们收集的内容示例", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用数据使用情况收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", - "telemetry.telemetryConfigTitle": "遥测选择加入", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "确认 Kibana 和 Elasticsearch 仍在运行,然后重试。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "无法保存遥测首选项。", - "telemetry.telemetryErrorNotificationMessageTitle": "遥测错误", "telemetry.telemetryOptedInDisableUsage": "请在此禁用使用情况数据", "telemetry.telemetryOptedInDismissMessage": "关闭", "telemetry.telemetryOptedInNoticeDescription": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的{privacyStatementLink}。要停止收集,{disableLink}。", @@ -13186,4 +13182,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/functional/config.ie.js b/x-pack/test/functional/config.ie.js index 081bab4b80457..bac4547b4aa5c 100644 --- a/x-pack/test/functional/config.ie.js +++ b/x-pack/test/functional/config.ie.js @@ -58,14 +58,17 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, }, }, kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 0e1078a2a4c8b..012f0922c28cf 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -94,8 +94,8 @@ export default function({ getService, getPageObjects }) { // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs // function is taking about 15 seconds per comparison in jenkins. this.timeout(300000); - - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PDF Dashboard'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); @@ -135,7 +135,8 @@ export default function({ getService, getPageObjects }) { it('matches baseline report', async function() { this.timeout(300000); - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PNG Dash'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); From 26ad75659680e738faad5f605dbac67fd139950d Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 13 Feb 2020 10:49:12 +0100 Subject: [PATCH 14/14] add `absolute` option to `getUrlForApp` (#57193) --- ...lugin-public.applicationstart.geturlforapp.md | 7 +++++-- .../kibana-plugin-public.applicationstart.md | 2 +- .../application/application_service.test.ts | 11 ++++++++++- .../public/application/application_service.tsx | 16 ++++++++++++++-- src/core/public/application/types.ts | 10 ++++++++-- src/core/public/public.api.md | 1 + 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md index 7eadd4d4e9d44..1ae368a11674f 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md @@ -4,13 +4,16 @@ ## ApplicationStart.getUrlForApp() method -Returns a relative URL to a given app, including the global base path. +Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + +Note that when generating absolute urls, the protocol, host and port are determined from the browser location. Signature: ```typescript getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; ``` @@ -19,7 +22,7 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | | appId | string | | -| options | {
path?: string;
} | | +| options | {
path?: string;
absolute?: boolean;
} | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 433ce87419ae8..d5a0bef9470f7 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -22,7 +22,7 @@ export interface ApplicationStart | Method | Description | | --- | --- | -| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | +| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the absolute option to generate an absolute url (http://host:port/basePath/app/my-app)Note that when generating absolute urls, the protocol, host and port are determined from the browser location. | | [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigate to a given app | | [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 18716bd872842..5487ca53170dd 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -580,7 +580,6 @@ describe('#start()', () => { it('creates URLs with path parameter', async () => { service.setup(setupDeps); - const { getUrlForApp } = await service.start(startDeps); expect(getUrlForApp('app1', { path: 'deep/link' })).toBe('/base-path/app/app1/deep/link'); @@ -588,6 +587,16 @@ describe('#start()', () => { expect(getUrlForApp('app1', { path: '//deep/link//' })).toBe('/base-path/app/app1/deep/link'); expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link'); }); + + it('creates absolute URLs when `absolute` parameter is true', async () => { + service.setup(setupDeps); + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { absolute: true })).toBe('http://localhost/base-path/app/app1'); + expect(getUrlForApp('app2', { path: 'deep/link', absolute: true })).toBe( + 'http://localhost/base-path/app/app2/deep/link' + ); + }); }); describe('navigateToApp', () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d100457f4027f..77f06e316c0aa 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -272,8 +272,13 @@ export class ApplicationService { takeUntil(this.stop$) ), registerMountContext: this.mountContext.registerContext, - getUrlForApp: (appId, { path }: { path?: string } = {}) => - http.basePath.prepend(getAppUrl(availableMounters, appId, path)), + getUrlForApp: ( + appId, + { path, absolute = false }: { path?: string; absolute?: boolean } = {} + ) => { + const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); + return absolute ? relativeToAbsolute(relUrl) : relUrl; + }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { this.appLeaveHandlers.delete(this.currentAppId$.value!); @@ -364,3 +369,10 @@ const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapp ...changes, }; }; + +function relativeToAbsolute(url: string) { + // convert all link urls to absolute urls + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 493afd1fec9db..977bb7a52da22 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -593,11 +593,17 @@ export interface ApplicationStart { navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; /** - * Returns a relative URL to a given app, including the global base path. + * Returns an URL to a given app, including the global base path. + * By default, the URL is relative (/basePath/app/my-app). + * Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + * + * Note that when generating absolute urls, the protocol, host and port are determined from the browser location. + * * @param appId * @param options.path - optional path inside application to deep link to + * @param options.absolute - if true, will returns an absolute url instead of a relative one */ - getUrlForApp(appId: string, options?: { path?: string }): string; + getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; /** * Register a context provider for application mounting. Will only be available to applications that depend on the diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aab88b0befba3..5e9b609bde916 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -101,6 +101,7 @@ export interface ApplicationStart { currentAppId$: Observable; getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; navigateToApp(appId: string, options?: { path?: string;