From 1464741e54b91d2013fa366c65b630b18fe927f9 Mon Sep 17 00:00:00 2001 From: Brian Gaddis Date: Tue, 4 Sep 2018 12:32:03 -0400 Subject: [PATCH 01/68] TypeScript Reporting Layouts (#22454) * wip WIP * Changed any ypes to actual types Made sure all types are set on the new layout classes. * Changes recommended from code review Changed location of type interfaces and fixed naming errors * Latest Code Review Changes Fix naming on properties and methods as well as a few other fixes * Name Changes and spacing Name Changes and spacing * Name Change Name Change * Changes for typescript import and direct reference to layout_factory Changes for typescript import and direct reference to layout_factory * Move types locally * Evaluate function changes for puppeteer * Removed String as a type and renamed index.d.ts to types.d.ts for consistency Removed String as a type and renamed index.d.ts to types.d.ts for consistency * Changed layout_factoy to create_layout --- .../common/{constants.js => constants.ts} | 2 +- .../printable_pdf/server/lib/generate_pdf.js | 7 +- .../server/lib/layouts/create_layout.ts | 24 ++++ .../printable_pdf/server/lib/layouts/index.js | 20 ---- .../printable_pdf/server/lib/layouts/index.ts | 9 ++ .../server/lib/layouts/layout.ts | 43 ++++++++ .../server/lib/layouts/preserve_layout.js | 69 ------------ .../server/lib/layouts/preserve_layout.ts | 81 ++++++++++++++ .../printable_pdf/server/lib/layouts/print.js | 89 --------------- .../server/lib/layouts/print_layout.ts | 103 ++++++++++++++++++ .../server/lib/layouts/types.d.ts | 17 +++ x-pack/plugins/reporting/types.d.ts | 23 ++++ 12 files changed, 305 insertions(+), 182 deletions(-) rename x-pack/plugins/reporting/export_types/printable_pdf/common/{constants.js => constants.ts} (99%) create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/create_layout.ts delete mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.js create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.ts create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/layout.ts delete mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.js create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.ts delete mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print.js create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print_layout.ts create mode 100644 x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/types.d.ts create mode 100644 x-pack/plugins/reporting/types.d.ts diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/common/constants.js b/x-pack/plugins/reporting/export_types/printable_pdf/common/constants.ts similarity index 99% rename from x-pack/plugins/reporting/export_types/printable_pdf/common/constants.js rename to x-pack/plugins/reporting/export_types/printable_pdf/common/constants.ts index 8044c7c40ba2f..ddc678592760a 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/common/constants.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/common/constants.ts @@ -7,4 +7,4 @@ export const LayoutTypes = { PRESERVE_LAYOUT: 'preserve_layout', PRINT: 'print', -}; \ No newline at end of file +}; diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js index b68a7c24e1627..198a4d7dde0f1 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.js @@ -11,7 +11,7 @@ import { pdf } from './pdf'; import { groupBy } from 'lodash'; import { oncePerServer } from '../../../../server/lib/once_per_server'; import { screenshotsObservableFactory } from './screenshots'; -import { getLayoutFactory } from './layouts'; +import { createLayout } from './layouts'; const getTimeRange = (urlScreenshots) => { const grouped = groupBy(urlScreenshots.map(u => u.timeRange)); @@ -31,7 +31,6 @@ const formatDate = (date, timezone) => { function generatePdfObservableFn(server) { const screenshotsObservable = screenshotsObservableFactory(server); const captureConcurrency = 1; - const getLayout = getLayoutFactory(server); const urlScreenshotsObservable = (urls, headers, layout) => { return Rx.from(urls).pipe( @@ -68,7 +67,9 @@ function generatePdfObservableFn(server) { return function generatePdfObservable(title, urls, browserTimezone, headers, layoutParams, logo) { - const layout = getLayout(layoutParams); + + const layout = createLayout(server, layoutParams); + const screenshots$ = urlScreenshotsObservable(urls, headers, layout); return screenshots$.pipe( diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/create_layout.ts new file mode 100644 index 0000000000000..1863c2050fb44 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/create_layout.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 { KbnServer, Size } from '../../../../../types'; +import { LayoutTypes } from '../../../common/constants'; +import { Layout } from './layout'; +import { PreserveLayout } from './preserve_layout'; +import { PrintLayout } from './print_layout'; + +interface LayoutParams { + id: string; + dimensions: Size; +} + +export function createLayout(server: KbnServer, layoutParams: LayoutParams): Layout { + if (layoutParams && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { + return new PreserveLayout(layoutParams.id, layoutParams.dimensions); + } + + // this is the default because some jobs won't have anything specified + return new PrintLayout(server, layoutParams.id); +} diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.js deleted file mode 100644 index 14c593071b617..0000000000000 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.js +++ /dev/null @@ -1,20 +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 { LayoutTypes } from '../../../common/constants'; -import { preserveLayoutFactory } from './preserve_layout'; -import { printLayoutFactory } from './print'; - -export function getLayoutFactory(server) { - return function getLayout(layoutParams) { - if (layoutParams && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { - return preserveLayoutFactory(server, layoutParams); - } - - // this is the default because some jobs won't have anything specified - return printLayoutFactory(server, layoutParams); - }; -} diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.ts b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.ts new file mode 100644 index 0000000000000..fd35485779ba0 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createLayout } from './create_layout'; +export { PrintLayout } from './print_layout'; +export { PreserveLayout } from './preserve_layout'; diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/layout.ts b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/layout.ts new file mode 100644 index 0000000000000..eb7caf10307f3 --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/layout.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Size } from '../../../../../types'; +import { ViewZoomWidthHeight } from './types'; + +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export interface PdfImageSize { + width: number; + height?: number; +} + +export abstract class Layout { + public id: string = ''; + + constructor(id: string) { + this.id = id; + } + + public abstract getPdfImageSize(): PdfImageSize; + + public abstract getPdfPageOrientation(): string | undefined; + + public abstract getPdfPageSize(pageSizeParams: PageSizeParams): string | Size; + + public abstract getViewport(itemsCount: number): ViewZoomWidthHeight; + + public abstract getBrowserZoom(): number; + + public abstract getBrowserViewport(): Size; + + public abstract getCssOverridesPath(): string; +} diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.js deleted file mode 100644 index cee717553e101..0000000000000 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.js +++ /dev/null @@ -1,69 +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 path from 'path'; - -// you'll notice that we aren't passing the zoom at this time, while it'd be possible to use -// window.pixelDensity to figure out what the current user is seeing, if they're going to send the -// PDF to someone else, I can see there being benefit to using a higher pixel density, so we're -// going to leave this hard-coded for the time being -export function preserveLayoutFactory(server, { dimensions: { height, width }, zoom = 2 }) { - const scaledHeight = height * zoom; - const scaledWidth = width * zoom; - - return { - getCssOverridesPath() { - return path.join(__dirname, 'preserve_layout.css'); - }, - - getBrowserViewport() { - return { - height: scaledHeight, - width: scaledWidth, - }; - }, - - getBrowserZoom() { - return zoom; - }, - - getViewport() { - return { - height: scaledHeight, - width: scaledWidth, - zoom - }; - }, - - getPdfImageSize() { - return { - height: height, - width: width, - }; - }, - - getPdfPageOrientation() { - return undefined; - }, - - getPdfPageSize({ pageMarginTop, pageMarginBottom, pageMarginWidth, tableBorderWidth, headingHeight, subheadingHeight }) { - return { - height: height + pageMarginTop + pageMarginBottom + (tableBorderWidth * 2) + headingHeight + subheadingHeight, - width: width + (pageMarginWidth * 2) + (tableBorderWidth * 2), - }; - }, - - groupCount: 1, - - selectors: { - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterFromAttribute: 'data-shared-timefilter-from', - timefilterToAttribute: 'data-shared-timefilter-to', - } - }; -} \ No newline at end of file diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.ts new file mode 100644 index 0000000000000..ea58a09c5550e --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/preserve_layout.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import path from 'path'; +import { Size } from '../../../../../types'; +import { Layout, PageSizeParams } from './layout'; + +const ZOOM: number = 2; + +export class PreserveLayout extends Layout { + public readonly selectors = { + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterFromAttribute: 'data-shared-timefilter-from', + timefilterToAttribute: 'data-shared-timefilter-to', + }; + + public readonly groupCount = 1; + private readonly height: number; + private readonly width: number; + private readonly scaledHeight: number; + private readonly scaledWidth: number; + + constructor(id: string, size: Size) { + super(id); + this.height = size.height; + this.width = size.width; + this.scaledHeight = size.height * ZOOM; + this.scaledWidth = size.width * ZOOM; + } + + public getCssOverridesPath() { + return path.join(__dirname, 'preserve_layout.css'); + } + + public getBrowserViewport() { + return { + height: this.scaledHeight, + width: this.scaledWidth, + }; + } + + public getBrowserZoom() { + return ZOOM; + } + + public getViewport() { + return { + height: this.scaledHeight, + width: this.scaledWidth, + zoom: ZOOM, + }; + } + + public getPdfImageSize() { + return { + height: this.height, + width: this.width, + }; + } + + public getPdfPageOrientation() { + return undefined; + } + + public getPdfPageSize(pageSizeParams: PageSizeParams) { + return { + height: + this.height + + pageSizeParams.pageMarginTop + + pageSizeParams.pageMarginBottom + + pageSizeParams.tableBorderWidth * 2 + + pageSizeParams.headingHeight + + pageSizeParams.subheadingHeight, + width: this.width + pageSizeParams.pageMarginWidth * 2 + pageSizeParams.tableBorderWidth * 2, + }; + } +} diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print.js deleted file mode 100644 index 1986a2e5a8889..0000000000000 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print.js +++ /dev/null @@ -1,89 +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 path from 'path'; - -export function printLayoutFactory(server) { - const config = server.config(); - const captureConfig = config.get('xpack.reporting.capture'); - - const selectors = { - screenshot: '[data-shared-item]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterFromAttribute: 'data-shared-timefilter-from', - timefilterToAttribute: 'data-shared-timefilter-to', - }; - - return { - - getCssOverridesPath() { - return path.join(__dirname, 'print.css'); - }, - - getBrowserViewport() { - return captureConfig.viewport; - }, - - getBrowserZoom() { - return captureConfig.zoom; - }, - - getViewport(itemsCount) { - return { - zoom: captureConfig.zoom, - width: captureConfig.viewport.width, - height: captureConfig.viewport.height * itemsCount, - }; - }, - - async positionElements(browser) { - const elementSize = { - width: captureConfig.viewport.width / captureConfig.zoom, - height: captureConfig.viewport.height / captureConfig.zoom - }; - - await browser.evaluate({ - fn: function (selector, height, width) { - const visualizations = document.querySelectorAll(selector); - const visualizationsLength = visualizations.length; - - for (let i = 0; i < visualizationsLength; i++) { - const visualization = visualizations[i]; - const style = visualization.style; - style.position = 'fixed'; - style.top = `${height * i}px`; - style.left = 0; - style.width = `${width}px`; - style.height = `${height}px`; - style.zIndex = 1; - style.backgroundColor = 'inherit'; - } - }, - args: [selectors.screenshot, elementSize.height, elementSize.width], - }); - }, - - getPdfImageSize() { - return { - width: 500, - }; - }, - - getPdfPageOrientation() { - return 'portrait'; - }, - - getPdfPageSize() { - return 'A4'; - }, - - groupCount: 2, - - selectors - - }; -} \ No newline at end of file diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print_layout.ts new file mode 100644 index 0000000000000..44f65d35e6d9c --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/print_layout.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import path from 'path'; +import { KbnServer, Size } from '../../../../../types'; +import { Layout } from './layout'; +import { CaptureConfig } from './types'; + +type EvalArgs = any[]; + +interface EvaluateOptions { + // 'fn' is a function in string form to avoid tslint from auto formatting it into a version not + // underfood by transform_fn safeWrap. + fn: ((...evalArgs: EvalArgs) => any); + args: EvalArgs; // Arguments to be passed into the function defined by fn. +} + +interface BrowserClient { + evaluate: (evaluateOptions: EvaluateOptions) => void; +} + +export class PrintLayout extends Layout { + public selectors = { + screenshot: '[data-shared-item]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterFromAttribute: 'data-shared-timefilter-from', + timefilterToAttribute: 'data-shared-timefilter-to', + }; + + public readonly groupCount = 2; + + private captureConfig: CaptureConfig; + + constructor(server: KbnServer, id: string) { + super(id); + this.captureConfig = server.config().get('xpack.reporting.capture'); + } + + public getCssOverridesPath() { + return path.join(__dirname, 'print.css'); + } + + public getBrowserViewport() { + return this.captureConfig.viewport; + } + + public getBrowserZoom() { + return this.captureConfig.zoom; + } + + public getViewport(itemsCount: number) { + return { + zoom: this.captureConfig.zoom, + width: this.captureConfig.viewport.width, + height: this.captureConfig.viewport.height * itemsCount, + }; + } + + public async positionElements(browser: BrowserClient): Promise { + const elementSize: Size = { + width: this.captureConfig.viewport.width / this.captureConfig.zoom, + height: this.captureConfig.viewport.height / this.captureConfig.zoom, + }; + const evalOptions: EvaluateOptions = { + fn: (selector: string, height: number, width: number) => { + const visualizations = document.querySelectorAll(selector) as NodeListOf; + const visualizationsLength = visualizations.length; + + for (let i = 0; i < visualizationsLength; i++) { + const visualization = visualizations[i]; + const style = visualization.style; + style.position = 'fixed'; + style.top = `${height * i}px`; + style.left = '0'; + style.width = `${width}px`; + style.height = `${height}px`; + style.zIndex = '1'; + style.backgroundColor = 'inherit'; + } + }, + args: [this.selectors.screenshot, elementSize.height, elementSize.width], + }; + + await browser.evaluate(evalOptions); + } + + public getPdfImageSize() { + return { + width: 500, + }; + } + + public getPdfPageOrientation() { + return 'portrait'; + } + + public getPdfPageSize() { + return 'A4'; + } +} diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/types.d.ts b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/types.d.ts new file mode 100644 index 0000000000000..0ade1093312ea --- /dev/null +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/layouts/types.d.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Size } from '../../../../../types'; + +export interface CaptureConfig { + zoom: number; + viewport: Size; +} + +export interface ViewZoomWidthHeight { + zoom: number; + width: number; + height: number; +} diff --git a/x-pack/plugins/reporting/types.d.ts b/x-pack/plugins/reporting/types.d.ts new file mode 100644 index 0000000000000..3e5f243fc8252 --- /dev/null +++ b/x-pack/plugins/reporting/types.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export interface KbnServer { + config: () => ConfigObject; +} + +export interface ConfigObject { + get: (path: string) => any; +} + +export interface Size { + width: number; + height: number; +} + +export interface Logger { + debug: (message: string) => void; + error: (message: string) => void; + warning: (message: string) => void; +} From 005be474a226f07e12a38f30805183aa96b9e6b1 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 4 Sep 2018 18:08:20 +0100 Subject: [PATCH 02/68] [ML] Fixing links to results for obs with no results (#22650) --- .../public/jobs/jobs_list/components/job_actions/results.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js index fe81291165bac..3565f510645da 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js @@ -30,6 +30,11 @@ function getLink(location, jobs) { to = tos[0].string; } + // if either of the dates are empty, set them to undefined + // moment will convert undefined to now. + from = (from === '') ? undefined : from; + to = (to === '') ? undefined : to; + const jobIds = jobs.map(j => j.id); const url = mlJobService.createResultsUrl(jobIds, from, to, location); return `${chrome.getBasePath()}/app/${url}`; From 85bee360c7ca2fae8a88f6d0321f6f9e443be273 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 4 Sep 2018 18:09:47 +0100 Subject: [PATCH 03/68] [ML] Adding milliseconds to watch start and end times (#22659) --- .../ml/public/jobs/new_job/simple/components/watcher/watch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js index 33de20299cba5..b193f0cc55ed1 100644 --- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js +++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js @@ -83,7 +83,7 @@ export const watch = { script: { lang: 'painless', inline: `LocalDateTime.ofEpochSecond((doc["timestamp"].date.getMillis()-((doc["bucket_span"].value * 1000) - * params.padding)) / 1000, 0, ZoneOffset.UTC).toString()`, + * params.padding)) / 1000, 0, ZoneOffset.UTC).toString()+\":00.000Z\"`, params: { 'padding': 10 } @@ -93,7 +93,7 @@ export const watch = { script: { lang: 'painless', inline: `LocalDateTime.ofEpochSecond((doc["timestamp"].date.getMillis()+((doc["bucket_span"].value * 1000) - * params.padding)) / 1000, 0, ZoneOffset.UTC).toString()`, + * params.padding)) / 1000, 0, ZoneOffset.UTC).toString()+\":00.000Z\"`, params: { 'padding': 10 } From 6c896134ed6e1feaaefc2d5253bc3a4af327479c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 4 Sep 2018 19:07:07 +0100 Subject: [PATCH 04/68] [ML] Removing calendars from job when cloning (#22667) --- x-pack/plugins/ml/public/services/job_service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/ml/public/services/job_service.js b/x-pack/plugins/ml/public/services/job_service.js index 825141a586a01..00d3f1bc3a434 100644 --- a/x-pack/plugins/ml/public/services/job_service.js +++ b/x-pack/plugins/ml/public/services/job_service.js @@ -520,6 +520,7 @@ class JobService { delete tempJob.model_snapshot_id; delete tempJob.open_time; delete tempJob.established_model_memory; + delete tempJob.calendars; delete tempJob.analysis_config.use_per_partition_normalization; From bcb793857b7eb1cb01e3c262dc89896c150377bf Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 4 Sep 2018 11:26:58 -0700 Subject: [PATCH 05/68] Extract index pattern validation rules into ui/public (#22606) * Rename containsInvalidCharacters to containsIllegalCharacters and return a value which accurately reflects the name. * Move illegal index pattern characters from Management into ui/public/index_patterns. --- .../__tests__/render.test.js | 6 ++++++ .../__tests__/step_index_pattern.test.js | 6 ++++++ .../step_index_pattern/step_index_pattern.js | 7 ++++--- .../constants/index.js | 1 - .../contains_invalid_characters.test.js | 16 +++++++------- ...ters.js => contains_illegal_characters.js} | 4 ++-- .../create_index_pattern_wizard/lib/index.js | 2 +- .../public/index_patterns/constants/index.js | 21 +++++++++++++++++++ src/ui/public/index_patterns/index.js | 6 ++++++ 9 files changed, 54 insertions(+), 15 deletions(-) rename src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/{contains_invalid_characters.js => contains_illegal_characters.js} (86%) create mode 100644 src/ui/public/index_patterns/constants/index.js diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js index 16da9009f5d5f..9ef4a3e38ee9b 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/__tests__/render.test.js @@ -22,6 +22,12 @@ const unmountComponentAtNode = jest.fn(); jest.doMock('react-dom', () => ({ render, unmountComponentAtNode })); +// If we don't mock this, Jest fails with the error `TypeError: Cannot redefine property: prototype +// at Function.defineProperties`. +jest.mock('ui/index_patterns', () => ({ + INDEX_PATTERN_ILLEGAL_CHARACTERS: ['\\', '/', '?', '"', '<', '>', '|', ' '], +})); + jest.mock('ui/chrome', () => ({ getUiSettingsClient: () => ({ get: () => '', diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js index 3b05ec4b71ec4..37df9762b54df 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/__tests__/step_index_pattern.test.js @@ -26,6 +26,12 @@ jest.mock('../../../lib/ensure_minimum_time', () => ({ ensureMinimumTime: async (promises) => Array.isArray(promises) ? await Promise.all(promises) : await promises })); +// If we don't mock this, Jest fails with the error `TypeError: Cannot redefine property: prototype +// at Function.defineProperties`. +jest.mock('ui/index_patterns', () => ({ + INDEX_PATTERN_ILLEGAL_CHARACTERS: ['\\', '/', '?', '"', '<', '>', '|', ' '], +})); + jest.mock('ui/chrome', () => ({ getUiSettingsClient: () => ({ get: () => '', diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js index bf6468c3bf15b..133154de52619 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.js @@ -19,10 +19,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ILLEGAL_CHARACTERS, MAX_SEARCH_SIZE } from '../../constants'; +import { INDEX_PATTERN_ILLEGAL_CHARACTERS as ILLEGAL_CHARACTERS } from 'ui/index_patterns'; +import { MAX_SEARCH_SIZE } from '../../constants'; import { getIndices, - containsInvalidCharacters, + containsIllegalCharacters, getMatchedIndices, canAppendWildcard, ensureMinimumTime @@ -240,7 +241,7 @@ export class StepIndexPatternComponent extends Component { // This is an error scenario but do not report an error containsErrors = true; } - else if (!containsInvalidCharacters(query, ILLEGAL_CHARACTERS)) { + else if (containsIllegalCharacters(query, ILLEGAL_CHARACTERS)) { const errorMessage = intl.formatMessage( { id: 'kbn.management.createIndexPattern.step.invalidCharactersErrorMessage', diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js index 79bdbaed0d732..86246903b4440 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/constants/index.js @@ -26,4 +26,3 @@ export const MAX_NUMBER_OF_MATCHING_INDICES = 100; export const MAX_SEARCH_SIZE = MAX_NUMBER_OF_MATCHING_INDICES + ESTIMATED_NUMBER_OF_SYSTEM_INDICES; export const PER_PAGE_INCREMENTS = [5, 10, 20, 50]; -export const ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', ' ']; diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js index 2385f3baec6bc..05c4aba2571bd 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/__tests__/contains_invalid_characters.test.js @@ -17,16 +17,16 @@ * under the License. */ -import { containsInvalidCharacters } from '../contains_invalid_characters'; +import { containsIllegalCharacters } from '../contains_illegal_characters'; -describe('containsInvalidCharacters', () => { - it('should fail with illegal characters', () => { - const valid = containsInvalidCharacters('abc', ['a']); - expect(valid).toBeFalsy(); +describe('containsIllegalCharacters', () => { + it('returns true with illegal characters', () => { + const isInvalid = containsIllegalCharacters('abc', ['a']); + expect(isInvalid).toBe(true); }); - it('should pass with no illegal characters', () => { - const valid = containsInvalidCharacters('abc', ['%']); - expect(valid).toBeTruthy(); + it('returns false with no illegal characters', () => { + const isInvalid = containsIllegalCharacters('abc', ['%']); + expect(isInvalid).toBe(false); }); }); diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_invalid_characters.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_illegal_characters.js similarity index 86% rename from src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_invalid_characters.js rename to src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_illegal_characters.js index 5dbe3d7111061..31485bb3daaa2 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_invalid_characters.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/contains_illegal_characters.js @@ -17,6 +17,6 @@ * under the License. */ -export function containsInvalidCharacters(pattern, illegalCharacters) { - return !illegalCharacters.some(char => pattern.includes(char)); +export function containsIllegalCharacters(pattern, illegalCharacters) { + return illegalCharacters.some(char => pattern.includes(char)); } diff --git a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js index 22efa498c84ab..0930eb82514e1 100644 --- a/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js +++ b/src/core_plugins/kibana/public/management/sections/indices/create_index_pattern_wizard/lib/index.js @@ -25,6 +25,6 @@ export { getIndices } from './get_indices'; export { getMatchedIndices } from './get_matched_indices'; -export { containsInvalidCharacters } from './contains_invalid_characters'; +export { containsIllegalCharacters } from './contains_illegal_characters'; export { extractTimeFields } from './extract_time_fields'; diff --git a/src/ui/public/index_patterns/constants/index.js b/src/ui/public/index_patterns/constants/index.js new file mode 100644 index 0000000000000..b22c1682173ca --- /dev/null +++ b/src/ui/public/index_patterns/constants/index.js @@ -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 const INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE = ['\\', '/', '?', '"', '<', '>', '|']; +export const INDEX_PATTERN_ILLEGAL_CHARACTERS = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.concat(' '); diff --git a/src/ui/public/index_patterns/index.js b/src/ui/public/index_patterns/index.js index 025c0248b0848..5df0b7bb8a614 100644 --- a/src/ui/public/index_patterns/index.js +++ b/src/ui/public/index_patterns/index.js @@ -18,6 +18,12 @@ */ export { IndexPatternsProvider } from './index_patterns'; + export { IndexPatternsApiClientProvider, } from './index_patterns_api_client_provider'; + +export { + INDEX_PATTERN_ILLEGAL_CHARACTERS, + INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, +} from './constants'; From d874c21eeea5887cba672ab8b2063da4e732fb9d Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 4 Sep 2018 14:08:25 -0700 Subject: [PATCH 06/68] added assertion for the email field. --- x-pack/test/functional/apps/security/users.js | 1 + x-pack/test/functional/page_objects/security_page.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 3d0983dfda921..8f38e8e1d7472 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -42,6 +42,7 @@ export default function ({ getService, getPageObjects }) { log.debug('actualUsers = %j', users); expect(users.Lee.roles).to.eql(['kibana_user']); expect(users.Lee.fullname).to.eql('LeeFirst LeeLast'); + expect(users.Lee.email).to.eql('lee@myEmail.com'); expect(users.Lee.reserved).to.be(false); }); diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 96e30e152689f..f4f3781abc5a2 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -187,12 +187,14 @@ export function SecurityPageProvider({ getService, getPageObjects }) { return mapAsync(users, async user => { const fullnameElement = await user.findByCssSelector('[data-test-subj="userRowFullName"]'); const usernameElement = await user.findByCssSelector('[data-test-subj="userRowUserName"]'); + const emailElement = await user.findByCssSelector('[data-header="Email Address"]'); const rolesElement = await user.findByCssSelector('[data-test-subj="userRowRoles"]'); const isReservedElementVisible = await user.findByCssSelector('td:last-child'); return { username: await usernameElement.getVisibleText(), fullname: await fullnameElement.getVisibleText(), + email: await emailElement.getVisibleText(), roles: (await rolesElement.getVisibleText()).split(',').map(role => role.trim()), reserved: (await isReservedElementVisible.getProperty('innerHTML')).includes('reservedUser') }; @@ -228,7 +230,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await testSubjects.setValue('passwordInput', userObj.password); await testSubjects.setValue('passwordConfirmationInput', userObj.confirmPassword); await testSubjects.setValue('userFormFullNameInput', userObj.fullname); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); + await testSubjects.setValue('userFormEmailInput', userObj.email); log.debug('Add roles: ', userObj.roles); const rolesToAdd = userObj.roles || []; for (let i = 0; i < rolesToAdd.length; i++) { From e89abb3a39a3067cd223b69b36f50a68d76e2936 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 4 Sep 2018 16:52:51 -0500 Subject: [PATCH 07/68] [config] logging.useUTC -> logging.timezone (#21561) * [config] logging.useUTC -> logging.timezone * docs * [env] exit if starting as root * fix import path * add link and timezone example * Revert "[env] exit if starting as root" This reverts commit f6e9090833a5180fe360a9ff54543c37c0ca3a58. --- docs/migration/migrate_7_0.asciidoc | 5 +++++ docs/setup/settings.asciidoc | 2 +- src/server/config/schema.js | 2 +- src/server/config/transform_deprecations.js | 13 ++++++++++++- src/server/logging/configuration.js | 2 +- src/server/logging/log_format.js | 8 ++++---- src/server/logging/log_format_json.test.js | 12 +++++------- src/server/logging/log_format_string.test.js | 10 ++++------ 8 files changed, 33 insertions(+), 21 deletions(-) diff --git a/docs/migration/migrate_7_0.asciidoc b/docs/migration/migrate_7_0.asciidoc index 60272acc8ad28..7a9dd5ebae21e 100644 --- a/docs/migration/migrate_7_0.asciidoc +++ b/docs/migration/migrate_7_0.asciidoc @@ -56,3 +56,8 @@ considered unique based on its persistent UUID, which is written to the path.dat *Details:* The `/shorten` API has been deprecated since 6.5, when it was replaced by the `/api/shorten_url` API. *Impact:* The '/shorten' API has been removed. Use the '/api/shorten_url' API instead. + +=== Deprecated kibana.yml setting logging.useUTC has been replaced with logging.timezone +*Details:* Any timezone can now be specified by canonical id. + +*Impact:* The logging.useUTC flag will have to be replaced with a timezone id. If set to true the id is `UTC`. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 1f09e5dc6e2a6..7a21705336313 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -73,7 +73,7 @@ error messages. [[logging-verbose]]`logging.verbose:`:: *Default: false* Set the value of this setting to `true` to log all events, including system usage information and all requests. Supported on Elastic Cloud Enterprise. -`logging.useUTC`:: *Default: true* Set the value of this setting to `false` to log events using the timezone of the server, rather than UTC. +`logging.timezone`:: *Default: UTC* Set to the canonical timezone id (e.g. `US/Pacific`) to log events using that timezone. A list of timezones can be referenced at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. `map.includeElasticMapsService:`:: *Default: true* Turns on or off whether layers from the Elastic Maps Service should be included in the vector and tile layer option list. By turning this off, only the layers that are configured here will be included. diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 6fa09a400cc09..3648e88fac2e8 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -186,7 +186,7 @@ export default () => Joi.object({ then: Joi.default(!process.stdout.isTTY), otherwise: Joi.default(true) }), - useUTC: Joi.boolean().default(true), + timezone: Joi.string().allow(false).default('UTC') }).default(), ops: Joi.object({ diff --git a/src/server/config/transform_deprecations.js b/src/server/config/transform_deprecations.js index 44571cee6c58d..d15e171f031ff 100644 --- a/src/server/config/transform_deprecations.js +++ b/src/server/config/transform_deprecations.js @@ -17,8 +17,9 @@ * under the License. */ -import _, { partial } from 'lodash'; +import _, { partial, set } from 'lodash'; import { createTransform, Deprecations } from '../../deprecation'; +import { unset } from '../../utils'; const { rename, unused } = Deprecations; @@ -55,6 +56,15 @@ const rewriteBasePath = (settings, log) => { } }; +const loggingTimezone = (settings, log) => { + if (_.has(settings, 'logging.useUTC')) { + const timezone = settings.logging.useUTC ? 'UTC' : false; + set('logging.timezone', timezone); + unset(settings, 'logging.UTC'); + log(`Config key "logging.useUTC" is deprecated. It has been replaced with "logging.timezone"`); + } +}; + const deprecations = [ //server rename('server.ssl.cert', 'server.ssl.certificate'), @@ -68,6 +78,7 @@ const deprecations = [ serverSslEnabled, savedObjectsIndexCheckTimeout, rewriteBasePath, + loggingTimezone, ]; export const transformDeprecations = createTransform(deprecations); diff --git a/src/server/logging/configuration.js b/src/server/logging/configuration.js index beec30b54bccb..59019ad873129 100644 --- a/src/server/logging/configuration.js +++ b/src/server/logging/configuration.js @@ -61,7 +61,7 @@ export default function loggingConfiguration(config) { config: { json: config.get('logging.json'), dest: config.get('logging.dest'), - useUTC: config.get('logging.useUTC'), + timezone: config.get('logging.timezone'), // I'm adding the default here because if you add another filter // using the commandline it will remove authorization. I want users diff --git a/src/server/logging/log_format.js b/src/server/logging/log_format.js index 43ffca7fd39c6..994b5af8b89f4 100644 --- a/src/server/logging/log_format.js +++ b/src/server/logging/log_format.js @@ -18,7 +18,7 @@ */ import Stream from 'stream'; -import moment from 'moment'; +import moment from 'moment-timezone'; import { get, _ } from 'lodash'; import numeral from '@elastic/numeral'; import chalk from 'chalk'; @@ -66,10 +66,10 @@ export default class TransformObjStream extends Stream.Transform { } extractAndFormatTimestamp(data, format) { - const { useUTC } = this.config; + const { timezone } = this.config; const date = moment(data['@timestamp']); - if (useUTC) { - date.utc(); + if (timezone) { + date.tz(timezone); } return date.format(format); } diff --git a/src/server/logging/log_format_json.test.js b/src/server/logging/log_format_json.test.js index b9878e63f0898..1632b2b401c8a 100644 --- a/src/server/logging/log_format_json.test.js +++ b/src/server/logging/log_format_json.test.js @@ -196,10 +196,10 @@ describe('KbnLoggerJsonFormat', () => { }); }); - describe('useUTC', () => { - it('logs in UTC when useUTC is true', async () => { + describe('timezone', () => { + it('logs in UTC', async () => { const format = new KbnLoggerJsonFormat({ - useUTC: true + timezone: 'UTC' }); const result = await createPromiseFromStreams([ @@ -211,10 +211,8 @@ describe('KbnLoggerJsonFormat', () => { expect(timestamp).toBe(moment.utc(time).format()); }); - it('logs in local timezone when useUTC is false', async () => { - const format = new KbnLoggerJsonFormat({ - useUTC: false - }); + it('logs in local timezone timezone is undefined', async () => { + const format = new KbnLoggerJsonFormat({}); const result = await createPromiseFromStreams([ createListStream([makeEvent('log')]), diff --git a/src/server/logging/log_format_string.test.js b/src/server/logging/log_format_string.test.js index ca572f8c03e66..e20b5eb59b76c 100644 --- a/src/server/logging/log_format_string.test.js +++ b/src/server/logging/log_format_string.test.js @@ -37,9 +37,9 @@ const makeEvent = () => ({ }); describe('KbnLoggerStringFormat', () => { - it('logs in UTC when useUTC is true', async () => { + it('logs in UTC', async () => { const format = new KbnLoggerStringFormat({ - useUTC: true + timezone: 'UTC' }); const result = await createPromiseFromStreams([ @@ -51,10 +51,8 @@ describe('KbnLoggerStringFormat', () => { .toContain(moment.utc(time).format('HH:mm:ss.SSS')); }); - it('logs in local timezone when useUTC is false', async () => { - const format = new KbnLoggerStringFormat({ - useUTC: false - }); + it('logs in local timezone when timezone is undefined', async () => { + const format = new KbnLoggerStringFormat({}); const result = await createPromiseFromStreams([ createListStream([makeEvent()]), From a64738d277a896d1f5419744f29c8e6042d25997 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 4 Sep 2018 17:09:16 -0500 Subject: [PATCH 08/68] [docs] fix missing float --- docs/migration/migrate_7_0.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/migration/migrate_7_0.asciidoc b/docs/migration/migrate_7_0.asciidoc index 7a9dd5ebae21e..c7e521c812326 100644 --- a/docs/migration/migrate_7_0.asciidoc +++ b/docs/migration/migrate_7_0.asciidoc @@ -57,6 +57,7 @@ considered unique based on its persistent UUID, which is written to the path.dat *Impact:* The '/shorten' API has been removed. Use the '/api/shorten_url' API instead. +[float] === Deprecated kibana.yml setting logging.useUTC has been replaced with logging.timezone *Details:* Any timezone can now be specified by canonical id. From 5baa6d51c9a5f6628a3448b034bb49f608a860a3 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 4 Sep 2018 15:48:46 -0700 Subject: [PATCH 09/68] [ftr/asyncInstance] fix error thrown for undefined provider instances (#22689) When a FTR service is created async the promise created by its provider is wrapped in a [`Proxy`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy) that checks before each property access if the instance has finished initializing. This breaks if the service provider returns undefined, which is the case for the `failureDebugging` service, because our truthy check will fail and we throw the error claiming the service is uninitialized, which is probably incorrect. This PR updates the proxy to use a `Symbol` to indicate when a service instance is not available yet and throws a different error when the proxy receives any request (get, set, etc.) and the service instance is not an object, as required by the Reflect APIs. --- .../lib/providers/async_instance.js | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/functional_test_runner/lib/providers/async_instance.js b/src/functional_test_runner/lib/providers/async_instance.js index f6118656ae0c4..aead3291efc91 100644 --- a/src/functional_test_runner/lib/providers/async_instance.js +++ b/src/functional_test_runner/lib/providers/async_instance.js @@ -18,95 +18,102 @@ */ const createdInstanceProxies = new WeakSet(); +const INITIALIZING = Symbol('async instance initializing'); export const isAsyncInstance = val =>( createdInstanceProxies.has(val) ); export const createAsyncInstance = (type, name, promiseForValue) => { - let finalValue; + let instance = INITIALIZING; - const initPromise = promiseForValue.then(v => finalValue = v); + const initPromise = promiseForValue.then(v => instance = v); const initFn = () => initPromise; const assertReady = desc => { - if (!finalValue) { + if (instance === INITIALIZING) { throw new Error(` ${type} \`${desc}\` is loaded asynchronously but isn't available yet. Either await the promise returned from ${name}.init(), or move this access into a test hook like \`before()\` or \`beforeEach()\`. `); } + + if (typeof instance !== 'object') { + throw new TypeError(` + ${type} \`${desc}\` is not supported because ${name} is ${typeof instance} + `); + } }; const proxy = new Proxy({}, { apply(target, context, args) { assertReady(`${name}()`); - return Reflect.apply(finalValue, context, args); + return Reflect.apply(instance, context, args); }, construct(target, args, newTarget) { assertReady(`new ${name}()`); - return Reflect.construct(finalValue, args, newTarget); + return Reflect.construct(instance, args, newTarget); }, defineProperty(target, prop, descriptor) { assertReady(`${name}.${prop}`); - return Reflect.defineProperty(finalValue, prop, descriptor); + return Reflect.defineProperty(instance, prop, descriptor); }, deleteProperty(target, prop) { assertReady(`${name}.${prop}`); - return Reflect.deleteProperty(finalValue, prop); + return Reflect.deleteProperty(instance, prop); }, get(target, prop, receiver) { if (prop === 'init') return initFn; assertReady(`${name}.${prop}`); - return Reflect.get(finalValue, prop, receiver); + return Reflect.get(instance, prop, receiver); }, getOwnPropertyDescriptor(target, prop) { assertReady(`${name}.${prop}`); - return Reflect.getOwnPropertyDescriptor(finalValue, prop); + return Reflect.getOwnPropertyDescriptor(instance, prop); }, getPrototypeOf() { assertReady(`${name}`); - return Reflect.getPrototypeOf(finalValue); + return Reflect.getPrototypeOf(instance); }, has(target, prop) { if (prop === 'init') return true; assertReady(`${name}.${prop}`); - return Reflect.has(finalValue, prop); + return Reflect.has(instance, prop); }, isExtensible() { assertReady(`${name}`); - return Reflect.isExtensible(finalValue); + return Reflect.isExtensible(instance); }, ownKeys() { assertReady(`${name}`); - return Reflect.ownKeys(finalValue); + return Reflect.ownKeys(instance); }, preventExtensions() { assertReady(`${name}`); - return Reflect.preventExtensions(finalValue); + return Reflect.preventExtensions(instance); }, set(target, prop, value, receiver) { assertReady(`${name}.${prop}`); - return Reflect.set(finalValue, prop, value, receiver); + return Reflect.set(instance, prop, value, receiver); }, setPrototypeOf(target, prototype) { assertReady(`${name}`); - return Reflect.setPrototypeOf(finalValue, prototype); + return Reflect.setPrototypeOf(instance, prototype); } }); From cd83db703783aa80b474fbc8caf56c7d9ab46fa0 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Tue, 4 Sep 2018 21:46:59 -0400 Subject: [PATCH 10/68] Fix #22510, dashboard-only mode doesn't display saved searches (#22685) --- x-pack/plugins/dashboard_mode/public/dashboard_viewer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js index 272174029ded9..ef2929f2ab2ff 100644 --- a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js @@ -33,6 +33,7 @@ import 'ui/vislib'; import 'ui/agg_response'; import 'ui/agg_types'; import 'ui/timepicker'; +import 'ui/pager'; import 'leaflet'; import { showAppRedirectNotification } from 'ui/notify'; From 5f4a1c58e88826789a2b558e2b2f8ea56af82a7b Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 5 Sep 2018 08:55:39 +0200 Subject: [PATCH 11/68] Move timezone settings into autoload file (#22623) * Move timezone settings into autoload file * Remove applying setting from timelion * Remove manual set from ML * Remove manual set from monitoring * Remove now obsolete code from embedding test plugin --- src/core_plugins/kibana/public/kibana.js | 3 -- .../kibana/public/kibana_root_controller.js | 34 ------------- src/core_plugins/timelion/public/app.js | 4 -- src/ui/public/autoload/all.js | 1 + src/ui/public/autoload/settings.js | 50 +++++++++++++++++++ .../plugins/visualize_embedding/public/app.js | 28 ----------- .../dashboard_mode/public/dashboard_viewer.js | 4 +- x-pack/plugins/ml/public/app.js | 8 --- .../plugins/monitoring/public/monitoring.js | 4 -- 9 files changed, 52 insertions(+), 84 deletions(-) delete mode 100644 src/core_plugins/kibana/public/kibana_root_controller.js create mode 100644 src/ui/public/autoload/settings.js diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index 522b6b7087025..984706556800d 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -59,7 +59,6 @@ import 'ui/agg_types'; import 'ui/timepicker'; import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; -import { KibanaRootController } from './kibana_root_controller'; routes.enable(); @@ -68,6 +67,4 @@ routes redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` }); -chrome.setRootController('kibana', KibanaRootController); - uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/src/core_plugins/kibana/public/kibana_root_controller.js b/src/core_plugins/kibana/public/kibana_root_controller.js deleted file mode 100644 index 830cd0bd16c7b..0000000000000 --- a/src/core_plugins/kibana/public/kibana_root_controller.js +++ /dev/null @@ -1,34 +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-timezone'; - -export function KibanaRootController($scope, courier, config) { - config.watch('dateFormat:tz', setDefaultTimezone, $scope); - config.watch('dateFormat:dow', setStartDayOfWeek, $scope); - - function setDefaultTimezone(tz) { - moment.tz.setDefault(tz); - } - - function setStartDayOfWeek(day) { - const dow = moment.weekdays().indexOf(day); - moment.updateLocale(moment.locale(), { week: { dow } }); - } -} diff --git a/src/core_plugins/timelion/public/app.js b/src/core_plugins/timelion/public/app.js index ffadf7c9cdb1b..316a9704b5c3d 100644 --- a/src/core_plugins/timelion/public/app.js +++ b/src/core_plugins/timelion/public/app.js @@ -18,7 +18,6 @@ */ import _ from 'lodash'; -import moment from 'moment-timezone'; import { DocTitleProvider } from 'ui/doc_title'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; @@ -101,9 +100,6 @@ app.controller('timelion', function ( $scope.page = config.get('timelion:showTutorial', true) ? 1 : 0; $scope.setPage = (page) => $scope.page = page; - // TODO: For some reason the Kibana core doesn't correctly do this for all apps. - moment.tz.setDefault(config.get('dateFormat:tz')); - timefilter.enableAutoRefreshSelector(); timefilter.enableTimeRangeSelector(); diff --git a/src/ui/public/autoload/all.js b/src/ui/public/autoload/all.js index fba21aacd1984..b1aa11cb72494 100644 --- a/src/ui/public/autoload/all.js +++ b/src/ui/public/autoload/all.js @@ -21,4 +21,5 @@ import './accessibility'; import './modules'; import './directives'; import './filters'; +import './settings'; import './styles'; diff --git a/src/ui/public/autoload/settings.js b/src/ui/public/autoload/settings.js new file mode 100644 index 0000000000000..48505037dffe4 --- /dev/null +++ b/src/ui/public/autoload/settings.js @@ -0,0 +1,50 @@ +/* + * 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. + */ + +/** + * Autoload this file if we want some of the top level settings applied to a plugin. + * Currently this file makes sure the following settings are applied globally: + * - dateFormat:tz (meaning the Kibana time zone will be used in your plugin) + * - dateFormat:dow (meaning the Kibana configured start of the week will be used in your plugin) + */ + +import moment from 'moment-timezone'; +import chrome from '../chrome'; + +function setDefaultTimezone(tz) { + moment.tz.setDefault(tz); +} + +function setStartDayOfWeek(day) { + const dow = moment.weekdays().indexOf(day); + moment.updateLocale(moment.locale(), { week: { dow } }); +} + +const uiSettings = chrome.getUiSettingsClient(); + +setDefaultTimezone(uiSettings.get('dateFormat:tz')); +setStartDayOfWeek(uiSettings.get('dateFormat:dow')); + +uiSettings.subscribe(({ key, newValue }) => { + if (key === 'dateFormat:tz') { + setDefaultTimezone(newValue); + } else if (key === 'dateFormat:dow') { + setStartDayOfWeek(newValue); + } +}); diff --git a/test/plugin_functional/plugins/visualize_embedding/public/app.js b/test/plugin_functional/plugins/visualize_embedding/public/app.js index 3f622c946a115..4463feac27513 100644 --- a/test/plugin_functional/plugins/visualize_embedding/public/app.js +++ b/test/plugin_functional/plugins/visualize_embedding/public/app.js @@ -37,34 +37,6 @@ import 'uiExports/savedObjectTypes'; import 'uiExports/fieldFormats'; import 'uiExports/search'; -// ----------- TODO Remove once https://github.com/elastic/kibana/pull/22623 is merged - -import moment from 'moment-timezone'; - -function setDefaultTimezone(tz) { - moment.tz.setDefault(tz); -} - -function setStartDayOfWeek(day) { - const dow = moment.weekdays().indexOf(day); - moment.updateLocale(moment.locale(), { week: { dow } }); -} - -const uiSettings = chrome.getUiSettingsClient(); - -setDefaultTimezone(uiSettings.get('dateFormat:tz')); -setStartDayOfWeek(uiSettings.get('dateFormat:dow')); - -uiSettings.subscribe(({ key, newValue }) => { - if (key === 'dateFormat:tz') { - setDefaultTimezone(newValue); - } else if (key === 'dateFormat:dow') { - setStartDayOfWeek(newValue); - } -}); - -// ----------------- END OF REMOVAL ---------- - import { Main } from './components/main'; const app = uiModules.get('apps/firewallDemoPlugin', ['kibana']); diff --git a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js index ef2929f2ab2ff..2b6997742280c 100644 --- a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js @@ -38,7 +38,6 @@ import 'leaflet'; import { showAppRedirectNotification } from 'ui/notify'; import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard/dashboard_constants'; -import { KibanaRootController } from 'plugins/kibana/kibana_root_controller'; uiModules.get('kibana') .config(dashboardConfigProvider => dashboardConfigProvider.turnHideWriteControlsOn()); @@ -47,9 +46,8 @@ routes.enable(); routes.otherwise({ redirectTo: defaultUrl() }); chrome - .setRootController('kibana', function ($controller, $scope, courier, config) { + .setRootController('kibana', function () { chrome.showOnlyById('kibana:dashboard'); - $controller(KibanaRootController, { $scope, courier, config }); }); uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/x-pack/plugins/ml/public/app.js b/x-pack/plugins/ml/public/app.js index ba1d17bdd9b8d..ad76c56cfdf19 100644 --- a/x-pack/plugins/ml/public/app.js +++ b/x-pack/plugins/ml/public/app.js @@ -34,14 +34,6 @@ import 'plugins/ml/components/loading_indicator'; import 'plugins/ml/settings'; import uiRoutes from 'ui/routes'; -import moment from 'moment-timezone'; -import { uiModules } from 'ui/modules'; - -const uiModule = uiModules.get('kibana'); -uiModule.run((config) => { - // Set the timezone for moment formatting to that configured in Kibana. - moment.tz.setDefault(config.get('dateFormat:tz')); -}); if (typeof uiRoutes.enable === 'function') { uiRoutes.enable(); diff --git a/x-pack/plugins/monitoring/public/monitoring.js b/x-pack/plugins/monitoring/public/monitoring.js index f564848d02c90..0e4528ba679c3 100644 --- a/x-pack/plugins/monitoring/public/monitoring.js +++ b/x-pack/plugins/monitoring/public/monitoring.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment-timezone'; import uiRoutes from 'ui/routes'; import chrome from 'ui/chrome'; import 'ui/autoload/all'; @@ -20,9 +19,6 @@ import 'plugins/monitoring/views/all'; const uiSettings = chrome.getUiSettingsClient(); -// Allow UTC times to be entered for Absolute Time range in timepicker -moment.tz.setDefault(uiSettings.get('dateFormat:tz')); - // default timepicker default to the last hour uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify({ from: 'now-1h', From 9c01863c0a454ec0eceafa157dc479d4ee1f179f Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 5 Sep 2018 09:35:18 +0200 Subject: [PATCH 12/68] Fix react vis type documentation (#22573) --- .../visualize/development-create-visualization.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development/visualize/development-create-visualization.asciidoc b/docs/development/visualize/development-create-visualization.asciidoc index 437b4ff9e5f3d..f843904fcdc41 100644 --- a/docs/development/visualize/development-create-visualization.asciidoc +++ b/docs/development/visualize/development-create-visualization.asciidoc @@ -179,7 +179,7 @@ VisTypesRegistryProvider.register(MyNewVisType); [[development-react-visualization-type]] ==== React Visualization Type React visualization type assumes you are using React as your rendering technology. -Just pass in a React component to `visConfig.template`. +Just pass in a React component to `visConfig.component`. The visualization will receive `vis`, `appState`, `updateStatus` and `visData` as props. It also has a `renderComplete` property, which needs to be called once the rendering has completed. @@ -197,7 +197,7 @@ const MyNewVisType = (Private) => { icon: 'my_icon', description: 'Cool new chart', visConfig: { - template: ReactComponent + component: ReactComponent } }); } From 4cf727aa980c7b5596c330c8e7082c097f42caea Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 5 Sep 2018 14:02:15 +0300 Subject: [PATCH 13/68] Add logging to messages validation (#22296) * Add logging and parallelization to messages validation * Refactor dev/i18n * Resolve comments * Remove parallelism and fix tests * Resolve comments --- .../extract_code_messages.test.js.snap | 31 --- .../extract_default_translations.test.js.snap | 179 +++++------------- .../extract_handlebars_messages.test.js.snap | 21 -- .../extract_html_messages.test.js.snap | 31 --- .../extract_i18n_call_messages.test.js.snap | 29 --- .../extract_pug_messages.test.js.snap | 15 -- .../extract_react_messages.test.js.snap | 27 --- src/dev/i18n/extract_default_translations.js | 101 ++-------- .../i18n/extract_default_translations.test.js | 35 +--- .../__snapshots__/code.test.js.snap | 31 +++ .../__snapshots__/handlebars.test.js.snap | 21 ++ .../__snapshots__/html.test.js.snap | 31 +++ .../__snapshots__/i18n_call.test.js.snap | 29 +++ .../extractors/__snapshots__/pug.test.js.snap | 15 ++ .../__snapshots__/react.test.js.snap | 27 +++ .../code.js} | 6 +- .../code.test.js} | 18 +- .../handlebars.js} | 30 +-- .../handlebars.test.js} | 4 +- .../html.js} | 36 +--- .../html.test.js} | 4 +- .../i18n_call.js} | 29 +-- .../i18n_call.test.js} | 6 +- src/dev/i18n/extractors/index.js | 25 +++ .../pug.js} | 4 +- .../pug.test.js} | 4 +- .../react.js} | 38 ++-- .../react.test.js} | 6 +- src/dev/i18n/index.js | 22 +++ .../__snapshots__/json.test.js.snap | 67 +++++++ .../__snapshots__/json5.test.js.snap | 65 +++++++ src/dev/i18n/serializers/index.js | 21 ++ src/dev/i18n/serializers/json.js | 34 ++++ src/dev/i18n/serializers/json.test.js | 37 ++++ src/dev/i18n/serializers/json5.js | 48 +++++ src/dev/i18n/serializers/json5.test.js | 42 ++++ src/dev/run/index.js | 2 +- src/dev/run_i18n_check.js | 47 ++++- 38 files changed, 701 insertions(+), 517 deletions(-) delete mode 100644 src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/code.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/html.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/pug.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/react.test.js.snap rename src/dev/i18n/{extract_code_messages.js => extractors/code.js} (94%) rename src/dev/i18n/{extract_code_messages.test.js => extractors/code.test.js} (89%) rename src/dev/i18n/{extract_handlebars_messages.js => extractors/handlebars.js} (68%) rename src/dev/i18n/{extract_handlebars_messages.test.js => extractors/handlebars.test.js} (95%) rename src/dev/i18n/{extract_html_messages.js => extractors/html.js} (78%) rename src/dev/i18n/{extract_html_messages.test.js => extractors/html.test.js} (94%) rename src/dev/i18n/{extract_i18n_call_messages.js => extractors/i18n_call.js} (64%) rename src/dev/i18n/{extract_i18n_call_messages.test.js => extractors/i18n_call.test.js} (95%) create mode 100644 src/dev/i18n/extractors/index.js rename src/dev/i18n/{extract_pug_messages.js => extractors/pug.js} (91%) rename src/dev/i18n/{extract_pug_messages.test.js => extractors/pug.test.js} (94%) rename src/dev/i18n/{extract_react_messages.js => extractors/react.js} (73%) rename src/dev/i18n/{extract_react_messages.test.js => extractors/react.test.js} (97%) create mode 100644 src/dev/i18n/index.js create mode 100644 src/dev/i18n/serializers/__snapshots__/json.test.js.snap create mode 100644 src/dev/i18n/serializers/__snapshots__/json5.test.js.snap create mode 100644 src/dev/i18n/serializers/index.js create mode 100644 src/dev/i18n/serializers/json.js create mode 100644 src/dev/i18n/serializers/json.test.js create mode 100644 src/dev/i18n/serializers/json5.js create mode 100644 src/dev/i18n/serializers/json5.test.js diff --git a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap deleted file mode 100644 index 507efdfd61595..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractCodeMessages extracts React, server-side and angular service default messages 1`] = ` -Array [ - Array [ - "kbn.mgmt.id-1", - Object { - "context": undefined, - "message": "Message text 1", - }, - ], - Array [ - "kbn.mgmt.id-2", - Object { - "context": "Message context", - "message": "Message text 2", - }, - ], - Array [ - "kbn.mgmt.id-3", - Object { - "context": undefined, - "message": "Message text 3", - }, - ], -] -`; - -exports[`extractCodeMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; - -exports[`extractCodeMessages throws on missing defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap index 750280b7f8d95..1a9997dc07dd9 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -1,142 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dev/i18n/extract_default_translations extracts messages to en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } +exports[`dev/i18n/extract_default_translations extracts messages from path to map 1`] = ` +Array [ + Array [ + "plugin_1.id_1", + Object { + "context": undefined, + "message": "Message 1", }, - \\"date\\": { - \\"short\\": { - \\"month\\": \\"numeric\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"2-digit\\" - }, - \\"medium\\": { - \\"month\\": \\"short\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"long\\": { - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"full\\": { - \\"weekday\\": \\"long\\", - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - } + ], + Array [ + "plugin_1.id_2", + Object { + "context": "Message context", + "message": "Message 2", }, - \\"time\\": { - \\"short\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\" - }, - \\"medium\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\" - }, - \\"long\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - }, - \\"full\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - } - } - }, - \\"plugin_1.id_1\\": \\"Message 1\\", - \\"plugin_1.id_2\\": { - \\"text\\": \\"Message 2\\", - \\"comment\\": \\"Message context\\" - }, - \\"plugin_1.id_3\\": \\"Message 3\\", - \\"plugin_1.id_4\\": \\"Message 4\\", - \\"plugin_1.id_5\\": \\"Message 5\\", - \\"plugin_1.id_6\\": \\"Message 6\\", - \\"plugin_1.id_7\\": \\"Message 7\\" -}" -`; - -exports[`dev/i18n/extract_default_translations injects default formats into en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } + ], + Array [ + "plugin_1.id_3", + Object { + "context": undefined, + "message": "Message 3", + }, + ], + Array [ + "plugin_1.id_4", + Object { + "context": undefined, + "message": "Message 4", + }, + ], + Array [ + "plugin_1.id_5", + Object { + "context": undefined, + "message": "Message 5", + }, + ], + Array [ + "plugin_1.id_6", + Object { + "context": "", + "message": "Message 6", }, - \\"date\\": { - \\"short\\": { - \\"month\\": \\"numeric\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"2-digit\\" - }, - \\"medium\\": { - \\"month\\": \\"short\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"long\\": { - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - }, - \\"full\\": { - \\"weekday\\": \\"long\\", - \\"month\\": \\"long\\", - \\"day\\": \\"numeric\\", - \\"year\\": \\"numeric\\" - } + ], + Array [ + "plugin_1.id_7", + Object { + "context": undefined, + "message": "Message 7", }, - \\"time\\": { - \\"short\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\" - }, - \\"medium\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\" - }, - \\"long\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - }, - \\"full\\": { - \\"hour\\": \\"numeric\\", - \\"minute\\": \\"numeric\\", - \\"second\\": \\"numeric\\", - \\"timeZoneName\\": \\"short\\" - } - } - }, - \\"plugin_2.message-id\\": \\"Message text\\" -}" + ], +] `; exports[`dev/i18n/extract_default_translations throws on id collision 1`] = ` " I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx -Error:  I18N ERROR  There is more than one default message for the same id \\"plugin_3.duplicate_id\\": +Error: There is more than one default message for the same id \\"plugin_3.duplicate_id\\": \\"Message 1\\" and \\"Message 2\\"" `; -exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `" I18N ERROR  Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`; +exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `"Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See .i18nrc.json for the list of supported namespaces."`; diff --git a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap deleted file mode 100644 index 7c9f72a6921ba..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_handlebars_messages extracts handlebars default messages 1`] = ` -Array [ - Array [ - "ui.id-1", - Object { - "context": "Message context", - "message": "Message text", - }, - ], -] -`; - -exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `" I18N ERROR  Empty id argument in Handlebars i18n is not allowed."`; - -exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `" I18N ERROR  Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; - -exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `" I18N ERROR  Wrong number of arguments for handlebars i18n call."`; - -exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `" I18N ERROR  Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap deleted file mode 100644 index aa6048b92b84c..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_html_messages extracts default messages from HTML 1`] = ` -Array [ - Array [ - "kbn.dashboard.id-1", - Object { - "context": "Message context 1", - "message": "Message text 1", - }, - ], - Array [ - "kbn.dashboard.id-2", - Object { - "context": undefined, - "message": "Message text 2", - }, - ], - Array [ - "kbn.dashboard.id-3", - Object { - "context": "Message context 3", - "message": "Message text 3", - }, - ], -] -`; - -exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `" I18N ERROR  Empty \\"i18n-id\\" value in angular directive is not allowed."`; - -exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `" I18N ERROR  Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap deleted file mode 100644 index 13a79e578861c..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 2`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `" I18N ERROR  defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; - -exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `" I18N ERROR  Message id in i18n() or i18n.translate() should be a string literal."`; - -exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; - -exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap deleted file mode 100644 index 16767f882063a..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractPugMessages extracts messages from pug template 1`] = ` -Array [ - "message-id", - Object { - "context": "Message context", - "message": "Default message", - }, -] -`; - -exports[`extractPugMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; - -exports[`extractPugMessages throws on missing default message 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap deleted file mode 100644 index 2bf17cab30c28..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_react_messages extractFormattedMessages extracts messages from "" element 1`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `" I18N ERROR  context value should be a string literal (\\"message-id\\")."`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `" I18N ERROR  defaultMessage value should be a string literal (\\"message-id\\")."`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `" I18N ERROR  Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 5bbfaa221b433..06d397961e233 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -18,28 +18,27 @@ */ import path from 'path'; -import { i18n } from '@kbn/i18n'; -import JSON5 from 'json5'; import normalize from 'normalize-path'; import chalk from 'chalk'; -import { extractHtmlMessages } from './extract_html_messages'; -import { extractCodeMessages } from './extract_code_messages'; -import { extractPugMessages } from './extract_pug_messages'; -import { extractHandlebarsMessages } from './extract_handlebars_messages'; -import { globAsync, readFileAsync, writeFileAsync } from './utils'; +import { + extractHtmlMessages, + extractCodeMessages, + extractPugMessages, + extractHandlebarsMessages, +} from './extractors'; +import { globAsync, readFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; -import { createFailError } from '../run'; - -const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; +import { createFailError, isFailError } from '../run'; function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); + if (targetMap.has(key) && existingValue.message !== value.message) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -There is more than one default message for the same id "${key}": + throw createFailError(`There is more than one default message for the same id "${key}": "${existingValue.message}" and "${value.message}"`); } + targetMap.set(key, value); } @@ -47,7 +46,7 @@ function normalizePath(inputPath) { return normalize(path.relative('.', inputPath)); } -function filterPaths(inputPaths) { +export function filterPaths(inputPaths) { const availablePaths = Object.values(paths); const pathsForExtraction = new Set(); @@ -79,9 +78,8 @@ export function validateMessageNamespace(id, filePath) { ); if (!id.startsWith(`${expectedNamespace}.`)) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -Expected "${id}" id to have "${expectedNamespace}" namespace. \ -See i18nrc.json for the list of supported namespaces.`); + throw createFailError(`Expected "${id}" id to have "${expectedNamespace}" namespace. \ +See .i18nrc.json for the list of supported namespaces.`); } } @@ -133,72 +131,15 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) { addMessageToMap(targetMap, id, value); } } catch (error) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` - ); + if (isFailError(error)) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` + ); + } + + throw error; } } }) ); } - -function serializeToJson5(defaultMessages) { - // .slice(0, -1): remove closing curly brace from json to append messages - let jsonBuffer = Buffer.from( - JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) - ); - - for (const [mapKey, mapValue] of defaultMessages) { - const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); - const formattedContext = mapValue.context - ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') - : ''; - - jsonBuffer = Buffer.concat([ - jsonBuffer, - Buffer.from(` '${mapKey}': '${formattedMessage}',`), - Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), - ]); - } - - // append previously removed closing curly brace - jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); - - return jsonBuffer; -} - -function serializeToJson(defaultMessages) { - const resultJsonObject = { formats: i18n.formats }; - - for (const [mapKey, mapValue] of defaultMessages) { - if (mapValue.context) { - resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; - } else { - resultJsonObject[mapKey] = mapValue.message; - } - } - - return JSON.stringify(resultJsonObject, undefined, 2); -} - -export async function extractDefaultTranslations({ paths, output, outputFormat }) { - const defaultMessagesMap = new Map(); - - for (const inputPath of filterPaths(paths)) { - await extractMessagesFromPathToMap(inputPath, defaultMessagesMap); - } - - // messages shouldn't be extracted to a file if output is not supplied - if (!output || !defaultMessagesMap.size) { - return; - } - - const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => - key1.localeCompare(key2) - ); - - await writeFileAsync( - path.resolve(output, 'en.json'), - outputFormat === 'json5' ? serializeToJson5(defaultMessages) : serializeToJson(defaultMessages) - ); -} diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index 57e89f731fc6a..b89361e87fcf7 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -20,7 +20,7 @@ import path from 'path'; import { - extractDefaultTranslations, + extractMessagesFromPathToMap, validateMessageNamespace, } from './extract_default_translations'; @@ -40,42 +40,21 @@ jest.mock('../../../.i18nrc.json', () => ({ exclude: [], })); -const utils = require('./utils'); -utils.writeFileAsync = jest.fn(); - describe('dev/i18n/extract_default_translations', () => { - test('extracts messages to en.json', async () => { + test('extracts messages from path to map', async () => { const [pluginPath] = pluginsPaths; + const resultMap = new Map(); - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); - - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); - }); - - test('injects default formats into en.json', async () => { - const [, pluginPath] = pluginsPaths; - - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); + await extractMessagesFromPathToMap(pluginPath, resultMap); - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); + expect([...resultMap].sort()).toMatchSnapshot(); }); test('throws on id collision', async () => { const [, , pluginPath] = pluginsPaths; + await expect( - extractDefaultTranslations({ paths: [pluginPath], output: pluginPath }) + extractMessagesFromPathToMap(pluginPath, new Map()) ).rejects.toThrowErrorMatchingSnapshot(); }); diff --git a/src/dev/i18n/extractors/__snapshots__/code.test.js.snap b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap new file mode 100644 index 0000000000000..26c621e32964d --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/code extracts React, server-side and angular service default messages 1`] = ` +Array [ + Array [ + "kbn.mgmt.id-1", + Object { + "context": undefined, + "message": "Message text 1", + }, + ], + Array [ + "kbn.mgmt.id-2", + Object { + "context": "Message context", + "message": "Message text 2", + }, + ], + Array [ + "kbn.mgmt.id-3", + Object { + "context": undefined, + "message": "Message text 3", + }, + ], +] +`; + +exports[`dev/i18n/extractors/code throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; + +exports[`dev/i18n/extractors/code throws on missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap new file mode 100644 index 0000000000000..7ca5178c7538f --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/handlebars extracts handlebars default messages 1`] = ` +Array [ + Array [ + "ui.id-1", + Object { + "context": "Message context", + "message": "Message text", + }, + ], +] +`; + +exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`; + +exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`; + +exports[`dev/i18n/extractors/handlebars throws on wrong properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap new file mode 100644 index 0000000000000..982341c880074 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/html extracts default messages from HTML 1`] = ` +Array [ + Array [ + "kbn.dashboard.id-1", + Object { + "context": "Message context 1", + "message": "Message text 1", + }, + ], + Array [ + "kbn.dashboard.id-2", + Object { + "context": undefined, + "message": "Message text 2", + }, + ], + Array [ + "kbn.dashboard.id-3", + Object { + "context": "Message context 3", + "message": "Message text 3", + }, + ], +] +`; + +exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`; + +exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap new file mode 100644 index 0000000000000..c9bf2f07716d4 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 2`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`; + +exports[`dev/i18n/extractors/i18n_call throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap new file mode 100644 index 0000000000000..c95fb0d149cd0 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/pug extracts messages from pug template 1`] = ` +Array [ + "message-id", + Object { + "context": "Message context", + "message": "Default message", + }, +] +`; + +exports[`dev/i18n/extractors/pug throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; + +exports[`dev/i18n/extractors/pug throws on missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/react.test.js.snap b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap new file mode 100644 index 0000000000000..6a51a5e216004 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/react extractFormattedMessages extracts messages from "" element 1`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if context value is not a string literal 1`] = `"context value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if message id is not a string literal 1`] = `"Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_code_messages.js b/src/dev/i18n/extractors/code.js similarity index 94% rename from src/dev/i18n/extract_code_messages.js rename to src/dev/i18n/extractors/code.js index e7b72e6efa162..e7477b17e2759 100644 --- a/src/dev/i18n/extract_code_messages.js +++ b/src/dev/i18n/extractors/code.js @@ -26,9 +26,9 @@ import { isMemberExpression, } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; /** * Detect Intl.formatMessage() function call (React). diff --git a/src/dev/i18n/extract_code_messages.test.js b/src/dev/i18n/extractors/code.test.js similarity index 89% rename from src/dev/i18n/extract_code_messages.test.js rename to src/dev/i18n/extractors/code.test.js index 5b3e64ebb4f07..3cc7d39f78d40 100644 --- a/src/dev/i18n/extract_code_messages.test.js +++ b/src/dev/i18n/extractors/code.test.js @@ -24,8 +24,8 @@ import { extractCodeMessages, isFormattedMessageElement, isIntlFormatMessageFunction, -} from './extract_code_messages'; -import { traverseNodes } from './utils'; +} from './code'; +import { traverseNodes } from '../utils'; const extractCodeMessagesSource = Buffer.from(` i18n('kbn.mgmt.id-1', { defaultMessage: 'Message text 1' }); @@ -65,7 +65,7 @@ function f() { } `; -describe('extractCodeMessages', () => { +describe('dev/i18n/extractors/code', () => { test('extracts React, server-side and angular service default messages', () => { const actual = Array.from(extractCodeMessages(extractCodeMessagesSource)); expect(actual.sort()).toMatchSnapshot(); @@ -84,12 +84,16 @@ describe('extractCodeMessages', () => { describe('isIntlFormatMessageFunction', () => { test('detects intl.formatMessage call expression', () => { - const callExpressionNodes = [...traverseNodes(parse(intlFormatMessageSource).program.body)].filter( - node => isCallExpression(node) - ); + const callExpressionNodes = [ + ...traverseNodes(parse(intlFormatMessageSource).program.body), + ].filter(node => isCallExpression(node)); expect(callExpressionNodes).toHaveLength(4); - expect(callExpressionNodes.every(callExpressionNode => isIntlFormatMessageFunction(callExpressionNode))).toBe(true); + expect( + callExpressionNodes.every(callExpressionNode => + isIntlFormatMessageFunction(callExpressionNode) + ) + ).toBe(true); }); }); diff --git a/src/dev/i18n/extract_handlebars_messages.js b/src/dev/i18n/extractors/handlebars.js similarity index 68% rename from src/dev/i18n/extract_handlebars_messages.js rename to src/dev/i18n/extractors/handlebars.js index 1aabdb61e2be1..7c57c8d0da731 100644 --- a/src/dev/i18n/extract_handlebars_messages.js +++ b/src/dev/i18n/extractors/handlebars.js @@ -17,10 +17,8 @@ * under the License. */ -import chalk from 'chalk'; - -import { formatJSString } from './utils'; -import { createFailError } from '../run'; +import { formatJSString } from '../utils'; +import { createFailError } from '../../run'; const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g; const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g; @@ -39,29 +37,22 @@ export function* extractHandlebarsMessages(buffer) { } if (tokens.length !== 3) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Wrong number of arguments for handlebars i18n call.` - ); + throw createFailError(`Wrong number of arguments for handlebars i18n call.`); } if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } const messageId = formatJSString(idString.slice(1, -1)); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty id argument in Handlebars i18n is not allowed.` - ); + throw createFailError(`Empty id argument in Handlebars i18n is not allowed.`); } if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Properties string in Handlebars i18n should be a string literal ("${messageId}").` + `Properties string in Handlebars i18n should be a string literal ("${messageId}").` ); } @@ -70,15 +61,13 @@ Properties string in Handlebars i18n should be a string literal ("${messageId}") if (typeof message !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in Handlebars i18n should be a string ("${messageId}").` + `defaultMessage value in Handlebars i18n should be a string ("${messageId}").` ); } if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` + `Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` ); } @@ -86,8 +75,7 @@ Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` if (context != null && typeof context !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Context value in Handlebars i18n should be a string ("${messageId}").` + `Context value in Handlebars i18n should be a string ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_handlebars_messages.test.js b/src/dev/i18n/extractors/handlebars.test.js similarity index 95% rename from src/dev/i18n/extract_handlebars_messages.test.js rename to src/dev/i18n/extractors/handlebars.test.js index e4f53852b6cc3..52365989bd7fd 100644 --- a/src/dev/i18n/extract_handlebars_messages.test.js +++ b/src/dev/i18n/extractors/handlebars.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractHandlebarsMessages } from './extract_handlebars_messages'; +import { extractHandlebarsMessages } from './handlebars'; -describe('dev/i18n/extract_handlebars_messages', () => { +describe('dev/i18n/extractors/handlebars', () => { test('extracts handlebars default messages', () => { const source = Buffer.from(`\ window.onload = function () { diff --git a/src/dev/i18n/extract_html_messages.js b/src/dev/i18n/extractors/html.js similarity index 78% rename from src/dev/i18n/extract_html_messages.js rename to src/dev/i18n/extractors/html.js index 4c8cad3ce1008..b576acb31c6d2 100644 --- a/src/dev/i18n/extract_html_messages.js +++ b/src/dev/i18n/extractors/html.js @@ -17,14 +17,13 @@ * under the License. */ -import chalk from 'chalk'; import { jsdom } from 'jsdom'; import { parse } from '@babel/parser'; import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types'; -import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; /** * Find all substrings of "{{ any text }}" pattern @@ -53,17 +52,13 @@ function parseFilterObjectExpression(expression) { for (const property of node.properties) { if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} defaultMessage value should be a string literal.` - ); + throw createFailError(`defaultMessage value should be a string literal.`); } message = formatJSString(property.value.value); } else if (isPropertyWithKey(property, CONTEXT_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal.` - ); + throw createFailError(`context value should be a string literal.`); } context = formatJSString(property.value.value); @@ -101,27 +96,20 @@ function* getFilterMessages(htmlContent) { const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); if (!filterObjectExpression || !idExpression) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Cannot parse i18n filter expression: {{ ${expression} }}` - ); + throw createFailError(`Cannot parse i18n filter expression: {{ ${expression} }}`); } const messageId = parseIdExpression(idExpression); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in angular filter expression is not allowed.` - ); + throw createFailError(`Empty "id" value in angular filter expression is not allowed.`); } const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {}; if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` + `Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` ); } @@ -137,17 +125,13 @@ function* getDirectiveMessages(htmlContent) { for (const element of document.querySelectorAll('[i18n-id]')) { const messageId = formatHTMLString(element.getAttribute('i18n-id')); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "i18n-id" value in angular directive is not allowed.` - ); + throw createFailError(`Empty "i18n-id" value in angular directive is not allowed.`); } const message = formatHTMLString(element.getAttribute('i18n-default-message')); if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular directive is not allowed ("${messageId}").` + `Empty defaultMessage in angular directive is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_html_messages.test.js b/src/dev/i18n/extractors/html.test.js similarity index 94% rename from src/dev/i18n/extract_html_messages.test.js rename to src/dev/i18n/extractors/html.test.js index d5cf7d6fd5ee2..40664edd81e4a 100644 --- a/src/dev/i18n/extract_html_messages.test.js +++ b/src/dev/i18n/extractors/html.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { extractHtmlMessages } from './extract_html_messages'; +import { extractHtmlMessages } from './html'; const htmlSourceBuffer = Buffer.from(`
@@ -37,7 +37,7 @@ const htmlSourceBuffer = Buffer.from(`
`); -describe('dev/i18n/extract_html_messages', () => { +describe('dev/i18n/extractors/html', () => { test('extracts default messages from HTML', () => { const actual = Array.from(extractHtmlMessages(htmlSourceBuffer)); expect(actual.sort()).toMatchSnapshot(); diff --git a/src/dev/i18n/extract_i18n_call_messages.js b/src/dev/i18n/extractors/i18n_call.js similarity index 64% rename from src/dev/i18n/extract_i18n_call_messages.js rename to src/dev/i18n/extractors/i18n_call.js index ba146c06621fe..1adcf42598e16 100644 --- a/src/dev/i18n/extract_i18n_call_messages.js +++ b/src/dev/i18n/extractors/i18n_call.js @@ -17,12 +17,11 @@ * under the License. */ -import chalk from 'chalk'; import { isObjectExpression, isStringLiteral } from '@babel/types'; -import { isPropertyWithKey, formatJSString } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatJSString } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; /** * Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST @@ -31,19 +30,13 @@ export function extractI18nCallMessages(node) { const [idSubTree, optionsSubTree] = node.arguments; if (!isStringLiteral(idSubTree)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Message id in i18n() or i18n.translate() should be a string literal.` - ); + throw createFailError(`Message id in i18n() or i18n.translate() should be a string literal.`); } const messageId = idSubTree.value; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in i18n() or i18n.translate() is not allowed.` - ); + throw createFailError(`Empty "id" value in i18n() or i18n.translate() is not allowed.`); } let message; @@ -51,8 +44,7 @@ Empty "id" value in i18n() or i18n.translate() is not allowed.` if (!isObjectExpression(optionsSubTree)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } @@ -60,8 +52,7 @@ Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId} if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -69,8 +60,7 @@ defaultMessage value in i18n() or i18n.translate() should be a string literal (" } else if (isPropertyWithKey(prop, CONTEXT_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -80,8 +70,7 @@ context value in i18n() or i18n.translate() should be a string literal ("${messa if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_i18n_call_messages.test.js b/src/dev/i18n/extractors/i18n_call.test.js similarity index 95% rename from src/dev/i18n/extract_i18n_call_messages.test.js rename to src/dev/i18n/extractors/i18n_call.test.js index 0985233e4b3dd..f3ab92f4f1d6e 100644 --- a/src/dev/i18n/extract_i18n_call_messages.test.js +++ b/src/dev/i18n/extractors/i18n_call.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { traverseNodes } from '../utils'; const i18nCallMessageSource = ` i18n('message-id-1', { defaultMessage: 'Default message 1', context: 'Message context 1' }); @@ -31,7 +31,7 @@ const translateCallMessageSource = ` i18n.translate('message-id-2', { defaultMessage: 'Default message 2', context: 'Message context 2' }); `; -describe('extractI18nCallMessages', () => { +describe('dev/i18n/extractors/i18n_call', () => { test('extracts "i18n" and "i18n.translate" functions call message', () => { let callExpressionNode = [...traverseNodes(parse(i18nCallMessageSource).program.body)].find( node => isCallExpression(node) diff --git a/src/dev/i18n/extractors/index.js b/src/dev/i18n/extractors/index.js new file mode 100644 index 0000000000000..7362eeb4e7003 --- /dev/null +++ b/src/dev/i18n/extractors/index.js @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { extractCodeMessages } from './code'; +export { extractHandlebarsMessages } from './handlebars'; +export { extractHtmlMessages } from './html'; +export { extractI18nCallMessages } from './i18n_call'; +export { extractPugMessages } from './pug'; +export { extractFormattedMessages, extractIntlMessages } from './react'; diff --git a/src/dev/i18n/extract_pug_messages.js b/src/dev/i18n/extractors/pug.js similarity index 91% rename from src/dev/i18n/extract_pug_messages.js rename to src/dev/i18n/extractors/pug.js index 8451c0b11db24..59851d19e88ab 100644 --- a/src/dev/i18n/extract_pug_messages.js +++ b/src/dev/i18n/extractors/pug.js @@ -19,8 +19,8 @@ import { parse } from '@babel/parser'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; /** * Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}` diff --git a/src/dev/i18n/extract_pug_messages.test.js b/src/dev/i18n/extractors/pug.test.js similarity index 94% rename from src/dev/i18n/extract_pug_messages.test.js rename to src/dev/i18n/extractors/pug.test.js index 0f72c13a6a339..7f901d1d992db 100644 --- a/src/dev/i18n/extract_pug_messages.test.js +++ b/src/dev/i18n/extractors/pug.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractPugMessages } from './extract_pug_messages'; +import { extractPugMessages } from './pug'; -describe('extractPugMessages', () => { +describe('dev/i18n/extractors/pug', () => { test('extracts messages from pug template', () => { const source = Buffer.from(`\ #{i18n('message-id', { defaultMessage: 'Default message', context: 'Message context' })} diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extractors/react.js similarity index 73% rename from src/dev/i18n/extract_react_messages.js rename to src/dev/i18n/extractors/react.js index 014f1214d0a18..074af4a76d5b4 100644 --- a/src/dev/i18n/extract_react_messages.js +++ b/src/dev/i18n/extractors/react.js @@ -18,17 +18,14 @@ */ import { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types'; -import chalk from 'chalk'; -import { isPropertyWithKey, formatJSString, formatHTMLString } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatJSString, formatHTMLString } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; function extractMessageId(value) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } return value.value; @@ -36,10 +33,7 @@ function extractMessageId(value) { function extractMessageValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value should be a string literal ("${id}").` - ); + throw createFailError(`defaultMessage value should be a string literal ("${id}").`); } return value.value; @@ -47,9 +41,7 @@ defaultMessage value should be a string literal ("${id}").` function extractContextValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal ("${id}").` - ); + throw createFailError(`context value should be a string literal ("${id}").`); } return value.value; @@ -65,8 +57,7 @@ export function extractIntlMessages(node) { if (!isObjectExpression(options)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Object with defaultMessage property is not passed to intl.formatMessage().` + `Object with defaultMessage property is not passed to intl.formatMessage().` ); } @@ -81,10 +72,7 @@ Object with defaultMessage property is not passed to intl.formatMessage().` : undefined; if (!messageId) { - createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in intl.formatMessage() is not allowed.` - ); + createFailError(`Empty "id" value in intl.formatMessage() is not allowed.`); } const message = messageProperty @@ -93,8 +81,7 @@ Empty "id" value in intl.formatMessage() is not allowed.` if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` + `Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` ); } @@ -122,9 +109,7 @@ export function extractFormattedMessages(node) { : undefined; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty "id" value in is not allowed.` - ); + throw createFailError(`Empty "id" value in is not allowed.`); } const message = messageProperty @@ -133,8 +118,7 @@ export function extractFormattedMessages(node) { if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty default message in is not allowed ("${messageId}").` + `Empty default message in is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_react_messages.test.js b/src/dev/i18n/extractors/react.test.js similarity index 97% rename from src/dev/i18n/extract_react_messages.test.js rename to src/dev/i18n/extractors/react.test.js index 00233ac1abed2..91e65a0ecc20f 100644 --- a/src/dev/i18n/extract_react_messages.test.js +++ b/src/dev/i18n/extractors/react.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression, isJSXOpeningElement, isJSXIdentifier } from '@babel/types'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; -import { traverseNodes } from './utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; +import { traverseNodes } from '../utils'; const intlFormatMessageCallSource = ` const MyComponentContent = ({ intl }) => ( @@ -79,7 +79,7 @@ intl.formatMessage({ `, ]; -describe('dev/i18n/extract_react_messages', () => { +describe('dev/i18n/extractors/react', () => { describe('extractIntlMessages', () => { test('extracts messages from "intl.formatMessage" function call', () => { const ast = parse(intlFormatMessageCallSource, { plugins: ['jsx'] }); diff --git a/src/dev/i18n/index.js b/src/dev/i18n/index.js new file mode 100644 index 0000000000000..703e6ac682855 --- /dev/null +++ b/src/dev/i18n/index.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. + */ + +export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations'; +export { writeFileAsync } from './utils'; +export { serializeToJson, serializeToJson5 } from './serializers'; diff --git a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap new file mode 100644 index 0000000000000..c35e91e25cbb6 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json should serialize default messages to JSON 1`] = ` +"{ + \\"formats\\": { + \\"number\\": { + \\"currency\\": { + \\"style\\": \\"currency\\" + }, + \\"percent\\": { + \\"style\\": \\"percent\\" + } + }, + \\"date\\": { + \\"short\\": { + \\"month\\": \\"numeric\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"2-digit\\" + }, + \\"medium\\": { + \\"month\\": \\"short\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"long\\": { + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"full\\": { + \\"weekday\\": \\"long\\", + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + } + }, + \\"time\\": { + \\"short\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\" + }, + \\"medium\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\" + }, + \\"long\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + }, + \\"full\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + } + } + }, + \\"plugin1.message.id-1\\": \\"Message text 1 \\", + \\"plugin2.message.id-2\\": { + \\"text\\": \\"Message text 2\\", + \\"comment\\": \\"Message context\\" + } +}" +`; diff --git a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap new file mode 100644 index 0000000000000..2166b32f28fd1 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json5 should serialize default messages to JSON5 1`] = ` +"{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'plugin1.message.id-1': 'Message text 1', + 'plugin2.message.id-2': 'Message text 2', // Message context +} +" +`; diff --git a/src/dev/i18n/serializers/index.js b/src/dev/i18n/serializers/index.js new file mode 100644 index 0000000000000..3c10d7754563d --- /dev/null +++ b/src/dev/i18n/serializers/index.js @@ -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 { serializeToJson } from './json'; +export { serializeToJson5 } from './json5'; diff --git a/src/dev/i18n/serializers/json.js b/src/dev/i18n/serializers/json.js new file mode 100644 index 0000000000000..8e615af1e81d3 --- /dev/null +++ b/src/dev/i18n/serializers/json.js @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +export function serializeToJson(defaultMessages) { + const resultJsonObject = { formats: i18n.formats }; + + for (const [mapKey, mapValue] of defaultMessages) { + if (mapValue.context) { + resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; + } else { + resultJsonObject[mapKey] = mapValue.message; + } + } + + return JSON.stringify(resultJsonObject, undefined, 2); +} diff --git a/src/dev/i18n/serializers/json.test.js b/src/dev/i18n/serializers/json.test.js new file mode 100644 index 0000000000000..9486a999fe7db --- /dev/null +++ b/src/dev/i18n/serializers/json.test.js @@ -0,0 +1,37 @@ +/* + * 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 { serializeToJson } from './json'; + +describe('dev/i18n/serializers/json', () => { + test('should serialize default messages to JSON', () => { + const messages = new Map([ + ['plugin1.message.id-1', { message: 'Message text 1 ' }], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson(messages)).toMatchSnapshot(); + }); +}); diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js new file mode 100644 index 0000000000000..0156053d5f43b --- /dev/null +++ b/src/dev/i18n/serializers/json5.js @@ -0,0 +1,48 @@ +/* + * 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 JSON5 from 'json5'; +import { i18n } from '@kbn/i18n'; + +const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; + +export function serializeToJson5(defaultMessages) { + // .slice(0, -1): remove closing curly brace from json to append messages + let jsonBuffer = Buffer.from( + JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) + ); + + for (const [mapKey, mapValue] of defaultMessages) { + const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); + const formattedContext = mapValue.context + ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') + : ''; + + jsonBuffer = Buffer.concat([ + jsonBuffer, + Buffer.from(` '${mapKey}': '${formattedMessage}',`), + Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), + ]); + } + + // append previously removed closing curly brace + jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); + + return jsonBuffer; +} diff --git a/src/dev/i18n/serializers/json5.test.js b/src/dev/i18n/serializers/json5.test.js new file mode 100644 index 0000000000000..90be880bd32a3 --- /dev/null +++ b/src/dev/i18n/serializers/json5.test.js @@ -0,0 +1,42 @@ +/* + * 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 { serializeToJson5 } from './json5'; + +describe('dev/i18n/serializers/json5', () => { + test('should serialize default messages to JSON5', () => { + const messages = new Map([ + [ + 'plugin1.message.id-1', + { + message: 'Message text 1', + }, + ], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson5(messages).toString()).toMatchSnapshot(); + }); +}); diff --git a/src/dev/run/index.js b/src/dev/run/index.js index 1eef88d60b0a5..b176ac365fcf4 100644 --- a/src/dev/run/index.js +++ b/src/dev/run/index.js @@ -18,4 +18,4 @@ */ export { run } from './run'; -export { createFailError, combineErrors } from './fail'; +export { createFailError, combineErrors, isFailError } from './fail'; diff --git a/src/dev/run_i18n_check.js b/src/dev/run_i18n_check.js index 9d5f0011d6f38..02d70622b54cd 100644 --- a/src/dev/run_i18n_check.js +++ b/src/dev/run_i18n_check.js @@ -17,13 +17,46 @@ * under the License. */ -import { run } from './run'; -import { extractDefaultTranslations } from './i18n/extract_default_translations'; +import chalk from 'chalk'; +import Listr from 'listr'; +import { resolve } from 'path'; + +import { run, createFailError } from './run'; +import { + filterPaths, + extractMessagesFromPathToMap, + writeFileAsync, + serializeToJson, + serializeToJson5, +} from './i18n/'; run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { - await extractDefaultTranslations({ - paths: Array.isArray(path) ? path : [path || './'], - output, - outputFormat, - }); + const paths = Array.isArray(path) ? path : [path || './']; + const filteredPaths = filterPaths(paths); + + if (filteredPaths.length === 0) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +None of input paths is available for extraction or validation. See .i18nrc.json.` + ); + } + + const list = new Listr( + filteredPaths.map(filteredPath => ({ + task: messages => extractMessagesFromPathToMap(filteredPath, messages), + title: filteredPath, + })) + ); + + // messages shouldn't be extracted to a file if output is not supplied + const messages = await list.run(new Map()); + if (!output || !messages.size) { + return; + } + + const sortedMessages = [...messages].sort(([key1], [key2]) => key1.localeCompare(key2)); + await writeFileAsync( + resolve(output, 'en.json'), + outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages) + ); }); From d8f907b18a161d487c9af8af1189935e4e7ba092 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 5 Sep 2018 15:27:24 +0200 Subject: [PATCH 14/68] Fix broken visualize CSS (#22707) --- .../kibana/public/visualize/editor/styles/_editor.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less index 6991ae34d6cb1..2cfc573345885 100644 --- a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less +++ b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less @@ -124,7 +124,7 @@ /* Without setting this to 0 you will run into a bug where the filter bar modal is hidden under a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ - > visualize { + > .visualize { height: 100%; flex: 1 1 auto; display: flex; @@ -419,7 +419,7 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ flex-basis: 100%; } - visualize { + .visualize { .flex-parent(); flex: 1 1 100%; } From ccf455e9fdf3fe092abcb988e03e42afd8a5be2d Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Wed, 5 Sep 2018 11:10:38 -0400 Subject: [PATCH 15/68] Fix #22581 by introducing an artificial delay (#22601) Introduce a delay into reports to allow visualizations time to appear in the DOM. This is intended as a temporary (and hacky) workaround until we come up with a more robust way to determine that all of the visualizations on the page are ready for capture. --- .../export_types/printable_pdf/server/lib/screenshots.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js index 0845ecad65664..57e17f924bb11 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js @@ -133,7 +133,13 @@ export function screenshotsObservableFactory(server) { } } - return Promise.all(renderedTasks); + // The renderComplete fires before the visualizations are in the DOM, so + // we wait for the event loop to flush before telling reporting to continue. This + // seems to correct a timing issue that was causing reporting to occasionally + // capture the first visualization before it was actually in the DOM. + const hackyWaitForVisualizations = () => new Promise(r => setTimeout(r, 100)); + + return Promise.all(renderedTasks).then(hackyWaitForVisualizations); }, args: [layout.selectors.renderComplete, captureConfig.loadDelay], awaitPromise: true, From b7918690f47a28c123e6f0cd3f331f9be89db944 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 5 Sep 2018 12:14:05 -0400 Subject: [PATCH 16/68] align staging urls with new endpoints (#22691) --- .../public/__tests__/region_map_visualization.js | 8 ++++---- src/core_plugins/tests_bundle/tests_entry_template.js | 2 +- .../public/__tests__/coordinate_maps_visualization.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/core_plugins/region_map/public/__tests__/region_map_visualization.js index 7f3064b99380f..32b9c22839ff7 100644 --- a/src/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -36,9 +36,9 @@ import afterdatachangeandresizePng from './afterdatachangeandresize.png'; import aftercolorchangePng from './aftercolorchange.png'; import changestartupPng from './changestartup.png'; -const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'; -const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`; -const vectorManifestUrl = `"https://staging-dot-elastic-layer.appspot.com/v1/manifest`; +const manifestUrl = 'https://catalogue-staging.maps.elastic.co/v2/manifest'; +const tmsManifestUrl = `https://tiles-maps-stage.elastic.co/v2/manifest`; +const vectorManifestUrl = `https://vector-staging.maps.elastic.co/v2/manifest`; const manifest = { 'services': [{ 'id': 'tiles_v2', @@ -189,7 +189,7 @@ describe('RegionMapsVisualizationTests', function () { 'attribution': '

Made with NaturalEarth | Elastic Maps Service

', 'name': 'World Countries', 'format': 'geojson', - 'url': 'https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', + 'url': 'https://vector-staging.maps.elastic.co/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', 'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, { 'name': 'iso3', 'description': 'Three letter abbreviation' diff --git a/src/core_plugins/tests_bundle/tests_entry_template.js b/src/core_plugins/tests_bundle/tests_entry_template.js index eb015454aaf27..799a25033bef2 100644 --- a/src/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/core_plugins/tests_bundle/tests_entry_template.js @@ -60,7 +60,7 @@ const legacyMetadata = { }, mapConfig: { includeElasticMapsService: true, - manifestServiceUrl: 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest' + manifestServiceUrl: 'https://catalogue-staging.maps.elastic.co/v2/manifest' }, vegaConfig: { enabled: true, diff --git a/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index b1cd3bfd37c36..426c2c37a08ed 100644 --- a/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -50,7 +50,7 @@ function mockRawData() { mockRawData(); -const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'; +const manifestUrl = 'https://catalogue-staging.maps.elastic.co/v2/manifest'; const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`; const manifest = { 'services': [{ From 23ed2135bf2917642651248610ced7b7c724f581 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 5 Sep 2018 17:16:13 +0100 Subject: [PATCH 17/68] [ML] Fixing issue with incorrect timezones in jobs list (#22714) * [ML] Fixing issue with incorrect timezones in jobs list * refactoring min and max calculation * changes based on review * changing TimeStamp to Timestamp --- .../components/job_actions/results.js | 20 +++++++--------- .../components/jobs_list/jobs_list.js | 8 +++++-- .../jobs_list_view/jobs_list_view.js | 2 +- .../start_datafeed_modal.js | 2 +- .../ml/server/models/job_service/jobs.js | 24 +++++++------------ 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js index 3565f510645da..05e1db7989077 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js @@ -14,6 +14,8 @@ import { } from '@elastic/eui'; import chrome from 'ui/chrome'; +import moment from 'moment'; +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; import { mlJobService } from 'plugins/ml/services/job_service'; @@ -21,22 +23,18 @@ function getLink(location, jobs) { let from = 0; let to = 0; if (jobs.length === 1) { - from = jobs[0].earliestTimeStamp.string; - to = jobs[0].latestTimeStamp.string; + from = jobs[0].earliestTimestampMs; + to = jobs[0].latestTimestampMs; } else { - const froms = jobs.map(j => j.earliestTimeStamp).sort((a, b) => a.unix > b.unix); - const tos = jobs.map(j => j.latestTimeStamp).sort((a, b) => a.unix < b.unix); - from = froms[0].string; - to = tos[0].string; + from = Math.min(...jobs.map(j => j.earliestTimestampMs)); + to = Math.max(...jobs.map(j => j.latestTimestampMs)); } - // if either of the dates are empty, set them to undefined - // moment will convert undefined to now. - from = (from === '') ? undefined : from; - to = (to === '') ? undefined : to; + const fromString = moment(from).format(TIME_FORMAT); + const toString = moment(to).format(TIME_FORMAT); const jobIds = jobs.map(j => j.id); - const url = mlJobService.createResultsUrl(jobIds, from, to, location); + const url = mlJobService.createResultsUrl(jobIds, fromString, toString, location); return `${chrome.getBasePath()}/app/${url}`; } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js index 9898cfb0c7518..7687ac372308c 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -11,6 +11,7 @@ import React, { } from 'react'; import { sortBy } from 'lodash'; +import moment from 'moment'; import { toLocaleString } from '../../../../util/string_utils'; import { ResultLinks, actionsMenuContent } from '../job_actions'; @@ -25,6 +26,7 @@ import { const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export class JobsList extends Component { constructor(props) { @@ -157,11 +159,13 @@ export class JobsList extends Component { }, { name: 'Latest timestamp', truncateText: false, - field: 'latestTimeStampUnix', + field: 'latestTimeStampSortValue', sortable: true, render: (time, item) => ( - { item.latestTimeStamp.string } + { + (item.latestTimestampMs === undefined) ? '' : moment(item.latestTimestampMs).format(TIME_FORMAT) + } ) }, { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 8205c75da9e33..d5594933cf86a 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -240,7 +240,7 @@ export class JobsListView extends Component { fullJobsList[job.id] = job.fullJob; delete job.fullJob; } - job.latestTimeStampUnix = job.latestTimeStamp.unix; + job.latestTimeStampSortValue = (job.latestTimeStampMs || 0); return job; }); const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses); diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index c22a7aeec1926..cd5a939cbba16 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -196,6 +196,6 @@ StartDatafeedModal.propTypes = { }; function getLowestLatestTime(jobs) { - const times = jobs.map(j => j.latestTimeStamp.unix.valueOf()); + const times = jobs.map(j => j.latestTimeStampSortValue); return moment(Math.min(...times)); } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.js b/x-pack/plugins/ml/server/models/job_service/jobs.js index 6a9da9ff5b75d..2cb43bdabc99a 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/plugins/ml/server/models/job_service/jobs.js @@ -13,8 +13,6 @@ import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import moment from 'moment'; import { uniq } from 'lodash'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - export function jobsProvider(callWithRequest) { const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(callWithRequest); @@ -99,8 +97,8 @@ export function jobsProvider(callWithRequest) { const jobs = fullJobsList.map((job) => { const hasDatafeed = (typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length); const { - earliest: earliestTimeStamp, - latest: latestTimeStamp } = earliestAndLatestTimeStamps(job.data_counts); + earliest: earliestTimestampMs, + latest: latestTimestampMs } = earliestAndLatestTimeStamps(job.data_counts); const tempJob = { id: job.job_id, @@ -112,8 +110,8 @@ export function jobsProvider(callWithRequest) { hasDatafeed, datafeedId: (hasDatafeed && job.datafeed_config.datafeed_id) ? job.datafeed_config.datafeed_id : '', datafeedState: (hasDatafeed && job.datafeed_config.state) ? job.datafeed_config.state : '', - latestTimeStamp, - earliestTimeStamp, + latestTimestampMs, + earliestTimestampMs, nodeName: (job.node) ? job.node.name : undefined, }; if (jobIds.find(j => (j === tempJob.id))) { @@ -243,22 +241,16 @@ export function jobsProvider(callWithRequest) { function earliestAndLatestTimeStamps(dataCounts) { const obj = { - earliest: { string: '', unix: 0 }, - latest: { string: '', unix: 0 }, + earliest: undefined, + latest: undefined, }; if (dataCounts.earliest_record_timestamp) { - const ts = moment(dataCounts.earliest_record_timestamp); - obj.earliest.string = ts.format(TIME_FORMAT); - obj.earliest.unix = ts.valueOf(); - obj.earliest.moment = ts; + obj.earliest = moment(dataCounts.earliest_record_timestamp).valueOf(); } if (dataCounts.latest_record_timestamp) { - const ts = moment(dataCounts.latest_record_timestamp); - obj.latest.string = ts.format(TIME_FORMAT); - obj.latest.unix = ts.valueOf(); - obj.latest.moment = ts; + obj.latest = moment(dataCounts.latest_record_timestamp).valueOf(); } return obj; From 865a51de0aa2f0a23fe25de9f48182f7010864f9 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 5 Sep 2018 13:02:28 -0600 Subject: [PATCH 18/68] Add instructions for running reporting functional tests to x-pack README (#22683) * add instructions * rest of instructions * pdf-image requirements * typo * add Ubutnu commands --- x-pack/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/x-pack/README.md b/x-pack/README.md index 63722ac16ab10..177a88f6e9f43 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -100,6 +100,36 @@ We also have SAML API integration tests which set up Elasticsearch and Kibana wi node scripts/functional_tests --config test/saml_api_integration/config ``` +#### Running Reporting functional tests + +prerequisites: +The reporting functional tests use [pdf-image](https://www.npmjs.com/package/pdf-image) to convert PDF's pages to png files for image comparisions between generated reports and baseline reports. +pdf-image requires the system commands `convert`, `gs`, and `pdfinfo` to function. Those can be set up by running the following. + +```sh +//OSX +brew install imagemagick ghostscript poppler + +//Ubutnu +sudo apt-get install imagemagick ghostscript poppler-utils +``` + +To run the reporting functional tests: + +Start the test server +```sh +// Run from the directory KIBANA_HOME/x-pack +node scripts/functional_tests_server +``` + +Start the test runner +```sh +// Run from the directory KIBANA_HOME/x-pack +node ../scripts/functional_test_runner --config test/reporting/configs/chromium_functional.js +``` + +**Note** Configurations from `kibana.dev.yml` are picked up when running the tests. Ensure that `kibana.dev.yml` does not contain any `xpack.reporting` configurations. + #### Developing functional tests If you are **developing functional tests** then you probably don't want to rebuild Elasticsearch and wait for all that setup on every test run, so instead use this command to build and start just the Elasticsearch and Kibana servers: From f647d6cd5c0c99a46ddc67b39a0f48b5ed23675c Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 5 Sep 2018 20:19:00 +0100 Subject: [PATCH 19/68] [ML] Makefield type icon component keyboard accessible (#22708) --- .../__snapshots__/field_type_icon.test.js.snap | 8 ++++---- .../field_type_icon/field_type_icon.js | 18 +++++++++--------- .../field_type_icon/field_type_icon.test.js | 7 ++++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap b/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap index f8ff8dd752323..df9367578f0ca 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap +++ b/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap @@ -2,7 +2,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` @@ -10,8 +10,8 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` exports[`FieldTypeIcon update component 1`] = ` `; diff --git a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js index 33c2fda04edad..cf23fa1468f7d 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js +++ b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js @@ -20,31 +20,31 @@ export function FieldTypeIcon({ tooltipEnabled = false, type }) { switch (type) { case ML_JOB_FIELD_TYPES.BOOLEAN: - ariaLabel = 'Boolean field'; + ariaLabel = 'boolean type'; iconClass = 'fa-adjust'; break; case ML_JOB_FIELD_TYPES.DATE: - ariaLabel = 'Date field'; + ariaLabel = 'date type'; iconClass = 'fa-clock-o'; break; case ML_JOB_FIELD_TYPES.NUMBER: - ariaLabel = 'Metric field'; + ariaLabel = 'number type'; iconChar = '#'; break; case ML_JOB_FIELD_TYPES.GEO_POINT: - ariaLabel = 'Geo-point field'; + ariaLabel = 'geo_point type'; iconClass = 'fa-globe'; break; case ML_JOB_FIELD_TYPES.KEYWORD: - ariaLabel = 'Aggregatable string field'; + ariaLabel = 'keyword type'; iconChar = 't'; break; case ML_JOB_FIELD_TYPES.TEXT: - ariaLabel = 'String field'; + ariaLabel = 'text type'; iconClass = 'fa-file-text-o'; break; case ML_JOB_FIELD_TYPES.IP: - ariaLabel = 'IP address field'; + ariaLabel = 'IP type'; iconClass = 'fa-laptop'; break; default: @@ -69,7 +69,7 @@ export function FieldTypeIcon({ tooltipEnabled = false, type }) { // to support having another component directly inside the tooltip anchor // see https://github.com/elastic/eui/issues/839 return ( - + ); @@ -86,7 +86,7 @@ FieldTypeIcon.propTypes = { // To pass on its properties we apply `rest` to the outer `span` element. function FieldTypeIconContainer({ ariaLabel, className, iconChar, ...rest }) { return ( - + {(iconChar === '') ? ( ) : ( diff --git a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js index 74432ee964330..975319f942739 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js @@ -8,6 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { FieldTypeIcon } from './field_type_icon'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; describe('FieldTypeIcon', () => { @@ -22,12 +23,12 @@ describe('FieldTypeIcon', () => { }); test(`render component when type matches a field type`, () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); test(`render with tooltip and test hovering`, () => { - const wrapper = mount(); + const wrapper = mount(); const container = wrapper.find({ className: 'field-type-icon-container' }); expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); @@ -42,7 +43,7 @@ describe('FieldTypeIcon', () => { test(`update component`, () => { const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBeTruthy(); - wrapper.setProps({ type: 'keyword' }); + wrapper.setProps({ type: ML_JOB_FIELD_TYPES.IP }); expect(wrapper).toMatchSnapshot(); }); From f7fbed34eb777a1f41e2d718b38b10eb4aace021 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 5 Sep 2018 21:31:03 +0200 Subject: [PATCH 20/68] [APM] Update Node.js onboarding instructions (#22562) --- .../kibana/server/tutorials/apm/apm_client_instructions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js index 49c4b999af18e..183cf26a6679e 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js @@ -30,10 +30,11 @@ export const createNodeClientInstructions = () => [ textPre: 'Agents are libraries that run inside of your application process.' + ' APM services are created programmatically based on the `serviceName`.' + - ' This agent supports Express, Koa, hapi, and custom Node.js.', + ' This agent supports a vararity of frameworks but can also be used with your custom stack.', commands: `// Add this to the VERY top of the first file loaded in your app var apm = require('elastic-apm-node').start({curlyOpen} - // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + // Override service name from package.json + // Allowed characters: a-z, A-Z, 0-9, -, _, and space serviceName: '', // Use if APM Server requires a token From 2938d94d972f7806dbb25d063effbc3d394fa6b5 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Wed, 5 Sep 2018 14:19:51 -0700 Subject: [PATCH 21/68] Mgmt saved object test (#22564) --- .../objects_table/components/flyout/flyout.js | 3 +- .../apps/management/_import_objects.js | 6 +- .../management/_mgmt_import_saved_objects.js | 59 ++++ .../exports/mgmt_import_objects.json | 37 +++ test/functional/apps/management/index.js | 1 + .../fixtures/es_archiver/mgmt/data.json.gz | Bin 0 -> 11964 bytes .../fixtures/es_archiver/mgmt/mappings.json | 283 ++++++++++++++++++ test/functional/page_objects/settings_page.js | 17 +- 8 files changed, 388 insertions(+), 18 deletions(-) create mode 100644 test/functional/apps/management/_mgmt_import_saved_objects.js create mode 100644 test/functional/apps/management/exports/mgmt_import_objects.json create mode 100644 test/functional/fixtures/es_archiver/mgmt/data.json.gz create mode 100644 test/functional/fixtures/es_archiver/mgmt/mappings.json diff --git a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js index f99789b1932fe..16e856fb610cc 100644 --- a/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js +++ b/src/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/flyout.js @@ -324,6 +324,7 @@ export class Flyout extends Component { const options = this.state.indexPatterns.map(indexPattern => ({ text: indexPattern.get('title'), value: indexPattern.id, + ['data-test-subj']: `indexPatternOption-${indexPattern.get('title')}`, })); options.unshift({ @@ -333,7 +334,7 @@ export class Flyout extends Component { return ( this.onIndexChanged(id, e)} options={options} /> diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index 16bd12edcb73c..cc2437e2a6378 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.js @@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaSavedObjects(); await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json')); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.setImportIndexFieldOption(2); + await PageObjects.settings.associateIndexPattern('d1e4c910-a2e6-11e7-bb30-233be9be6a15', 'logstash-*'); await PageObjects.settings.clickConfirmChanges(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.settings.clickImportDone(); @@ -71,7 +71,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.setImportIndexFieldOption(2); + await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); await PageObjects.settings.clickConfirmChanges(); // Override the visualization. @@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.setImportIndexFieldOption(2); + await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*'); await PageObjects.settings.clickConfirmChanges(); // *Don't* override the visualization. diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js new file mode 100644 index 0000000000000..e2bfd89f07544 --- /dev/null +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -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. + */ + +import expect from 'expect.js'; +import path from 'path'; + +export default function ({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'settings', 'header']); + + //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization + //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) + + describe('mgmt saved objects', function describeIndexTests() { + beforeEach(async function () { + await esArchiver.load('discover'); + await PageObjects.settings.navigateTo(); + }); + + afterEach(async function () { + await esArchiver.unload('discover'); + }); + + it('should import saved objects mgmt', async function () { + + await PageObjects.settings.clickKibanaSavedObjects(); + await PageObjects.settings.importFile(path.join(__dirname, 'exports', 'mgmt_import_objects.json')); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.settings.associateIndexPattern('4c3f3c30-ac94-11e8-a651-614b2788174a', 'logstash-*'); + await PageObjects.settings.clickConfirmChanges(); + await PageObjects.settings.clickImportDone(); + await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading(); + + //instead of asserting on count- am asserting on the titles- which is more accurate than count. + const objects = await PageObjects.settings.getSavedObjectsInTable(); + expect(objects.includes('mysavedsearch')).to.be(true); + expect(objects.includes('mysavedviz')).to.be(true); + + }); + + }); + +} diff --git a/test/functional/apps/management/exports/mgmt_import_objects.json b/test/functional/apps/management/exports/mgmt_import_objects.json new file mode 100644 index 0000000000000..88e03585bf1ee --- /dev/null +++ b/test/functional/apps/management/exports/mgmt_import_objects.json @@ -0,0 +1,37 @@ +[ + { + "_id": "6aea5700-ac94-11e8-a651-614b2788174a", + "_type": "search", + "_source": { + "title": "mysavedsearch", + "description": "", + "hits": 0, + "columns": [ + "_source" + ], + "sort": [ + "@timestamp", + "desc" + ], + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"4c3f3c30-ac94-11e8-a651-614b2788174a\",\"highlightAll\":true,\"version\":true,\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + } + }, + { + "_id": "8411daa0-ac94-11e8-a651-614b2788174a", + "_type": "visualization", + "_source": { + "title": "mysavedviz", + "visState": "{\"title\":\"mysavedviz\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}}]}", + "uiStateJSON": "{}", + "description": "", + "savedSearchId": "6aea5700-ac94-11e8-a651-614b2788174a", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + } + } +] diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.js index 727462a3fb1b6..29ff8ddb9ad78 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.js @@ -44,6 +44,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_index_pattern_filter')); loadTestFile(require.resolve('./_scripted_fields_filter')); loadTestFile(require.resolve('./_import_objects')); + loadTestFile(require.resolve('./_mgmt_import_saved_objects')); loadTestFile(require.resolve('./_test_huge_fields')); loadTestFile(require.resolve('./_handle_alias')); loadTestFile(require.resolve('./_handle_version_conflict')); diff --git a/test/functional/fixtures/es_archiver/mgmt/data.json.gz b/test/functional/fixtures/es_archiver/mgmt/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..d923b3a74e474f95d1906db534946609d43f9685 GIT binary patch literal 11964 zcmV;tE<@2DiwFP!000026YZT*j~h9X=imEN4E%VQPT210nQlJ~aEk@*WgixYGjP`n zg34q{w3sAKGNqCS{oRj2W=bh3C0FlkpZ_s8gF#zTl~g|kgTV+g81c97{_ux4>EzX$ z4}W;mx%SN;>4k$dBfavs{3Jiswo@G|MsWv-~Z)*zWeFJ`*$Dy^mFs)pWpxQ z%kNL`zkG6jZ8K`q{o6R?SKs~R=l4JV@$#ci^>Q@nf6QO%-P<2!r#5fDm-79ezkl~m z{gfZ&zxmV8oAzzr{;8MU`#00ufBkO$@2}te`rUVb`(310-tfJbDXCyTybABDGQRrE zSBv_`x6NOE{%IlV>yNpTc+$y~KNWX!ui}4uvk-Z&mFXh=@c;bnmp4`p>X$bket9Ew z_sg3<{!$>z3o!+4x8=3gjW*>M-{!}$4cceS{Ir)QDtbxD(C*~M&>K@2={33C2DO#x z`qiLh_piYX|Ki&JMFSJ(PxjjB@@N<^01$IeGPqCHOH|_gZj6pQ4yaMJ2`Y|$&dEeolib2fG&6!r(``TGpsD;>WEH)} z#FwcapiVj;05Xx|hn>>fU4j?q#K}dfN8P0zzEBG$zD%P|>i15V`nx51hQya=Nkc)F zWdUR$$KUt3^zRpA#=@847!0xyGak4M(@|5d*@ssCu84o1B$tp(#Oy#4v--*^XWPW1u$=U_0Q1q_g3CvDk3lmszm=AcGE`m$+R&+k&i}}R)P_E@ z@hu15J}54z$t7tTs5_reXve5JeAwM_5S#q=cBgJuvW;^_Ns9tnbHQ1t>x5bqAP2t^ zUkjGRB~s;u92qh*F=j}0x>osImyOu(d%N$y1`=J8Zu3dCDK>+QNkMzEGijMZ$e=Mf zFNy;jan@E(YkfwOX^hqW2(N-zJ#_ts;(UM^Rf~z(I&14Cm)OX7vTY7vT%6}O1Khn* zUPz-4iVHE#EEWRBGE~=?er(#EHeFCw#8Wo?0|U_PK?Ph)<79_Rhd#)-V@qx(loxkJ zM_Sjs!<1ZQ`w3mRykKA_ctL91cFSS-ohQ%IxCBocYVj)UP610vP`;w%=_@83+|w-wD+ewTyBk zVI|&*TrTNA3VB+iJGBk6%ad$6m1e;jWR4VAkU~XbYovpY-Yl{bSg^v6|Ctni{9WvT zSAqp3Z5zX6p*(`L`S0)m9S5PU$PXj>z!NGL&Y%YAz3PPDvfv#(ylDNlhExJK3b>F` zx-<*Wx>v}7lp;V5pyiQjCKd*y<-aR+IYDt(H-4~KQ;>bH+j0g3LwESDYD{e}l8(1oux(E1P5zS<3;kuiy>iVQ||mNiPG6Pz`^ zm>;V$(c=?|V(I1;v)clm1v~*BbZmJM$C!8lqn={jAXiN6^{Sk;fg!rsiLQq}RGkp( zwRzEI!-{o8HSy^VQEFIiA(mu0@XR7Yl-b4PA<~^s>|x>1ym4YnR2O2t9|kxfDqk4m zP8?TS4xvb}Q$Fob(SX;7La1JJOcCqo>I~P*wEJC~SN zEPJ6RObG^sZ0!K^q0VgIuKMU}$OPDw_1VnUghyB~xXrB#THOgijuCHJ5tFTM89`HT zS22NhE!^1dHZo={9NWE0wSpR;Ebll;q`gY*4jqz#Fm+Ocz)A(UMO+qh!xkpg%5w|n zJX~>uJI2qW%Qh&;0eqkrVKjmAYVi^1E{O}Oj_qwDJ(0drGD6;<1zrHRdi{$XzefcQNcC$mYca%UZH0FE9#bZ2g;b2Td~s#brzu&O1i-t+HMYRr7^ChWOit)v;YpOltBaMU34vC zzzH3|EJ2Y^T?(i3sATadJ%fQRnDE1VF38w|0vrx?) zxO`CQs571)oBXd;1}XZ#hfBKMM?k#VZUk# z5pc(H0f#tE&j}~A#trfX84q@FD<0zTT9v5FVzzsb`!Wl>E+DS73qTcu3%~;OlI|Vi z0&YsfGi)uajOeOZDZ%xzLZT~WQlXbETH?vf=f(s*VTccVvJfHkL?J-zNsgkIS$1BoQ5z@UvPB;i7*0`|aPVf&z6 z0IC!E3gqa<&~j1@x(h&EAwQ35RLuZx7iwEDbn8%y6zVMHRz<9hzt;fwTA8ktS(vs$ zpqzx)e1o`?p<^idiM!TG);wfRS9TbQy5f>{^PwFiy~KeRE63oPwA94{_S`C8P^~+t zY;J7pA5qs-TlN3|W4O)RB;X2%aZAASYuQrX}Y%6dj$ zZ!GefstaaD)3j3ao1!<|+@|M*&uD6Lnv%SxBDX2Xbn0=Ma)W6;xlJv~SwLt%_8^U} zH_imGj;x^j;94pQ(+P?jLdO6fHC&wV%9#@;VJVmO=`hT8VW*^TsI?;Ud|QPcnuxI&)i(ZeNZ84XMtxQ?(R{+o0fxY%Urvc#(?hA*Yb(w2&s=}#~rl0<7?3Is;9n& z8nDFXL+ts?96d^UgbW=ltw3{#nVw=5zl*zXL2!nC=MVJi7Kvfm9&sEilgA@LU-7C3g4KFxLAFsyvE8*i94*K8% zTiri@0k#w1>Kh35TuT3n&Di%UutIL# z;ITH(zs_8aRu~9-my&-^XYyUPB9wRD8+YV^8%4Vj)`?PycR*(jFJe$J$}O4aQaXYG zF*UGE=$y$pTK`UB(&9ekcSLyVKr2VU=Y-b+qqE($3n%FO0=U2Y7`zt9F3!am6yt9Y z$CmO@IDP~CsO2#`eB5$}Uj3!{b&iD&-*}fwc$No=zg(;ZC{RScbed=v1yUba< z*PuIvuRJriak4}A1+Q4)&G}55nZ#b57?93dD>@y7ddZWI8?TQAV zcE9VIbdwkVqwdlUsW0%ya9%Efd!NF6=S8t6(9DrhM&ykfKws53c~Lm{diHDw3gdGC(BVY*^@KA>|^`1HBKOG)BW^#uF-ou_sI@ z=i?D5E+p;qHNM5v$f-I$TrtcmNRQJK{f5h{y4ehjhiYUh-3SyHUgOmUI>O?lXs_{s z&e$nA5m)=pfVk^M7_G>Vpk7zjyvV@azQROR#w$o%M?1Q9uYl(?`KlY48Ex0>)k$EF z`5G+n*i&RB+np*`ia}SMGh`=>bNkWbV)nt=#70r?X|Xl+%sW|D5fmNdpax-tjJ}S} zh{$RRu9->3J*Gt(i{c5)yWKaL z(WZu4O#nk8)d*R^>)g>IIz@3sN?VxhkA5-4X=5L=Qu%M)eW5fhB zn#`fxxR6lw(GA{ND(-e)5C{m7QW#ulHYYD5Tnnv7wA%9EbYtQHn*Yhl8Wl9{<@{Y` zD+D|3FDJ020`p-D&xyF>Um(e0Ln_^P1qW?}$h8pp)9n^hc-WS4KI_ySDjr3#$Z$xN zfv)h%0V3wU;(`x)>9`cyIWHll@@0gk?UY6J8D>NVQY`A(q2?5>p#ttdUSXmhBHTmb zZjqibRi~pb_wTW1IEnQl%ZZ|Kj6P?c%c2q}wh^vHx7cZ7AT1nlO;oEM86e$9-y zTP!mnYS4wBNq&#XLYBAzEbvi#+69*q=8e4y7$|GJLtRvkN^f^Kl5;UN^pQGcm+-EI z_BYfy0>kNNQE_gZsOkB)KpA8UB^oFfiGUncdmXenp;37wQ}4on#-(q@@(6nw zl194Zu?Cr(*DaPNp5+F;Al^};Eru7sI#n90jw&W_NM|}=$jc+tMP`i^S2GukHlA!N zg7QYUH%rgPIisY-0$yu)iYVAsiQ<5k^@qW1s+>q$VSBxiR+f!t>WyxvgFeydBqM4RqVH0nSKXq@w+Q%0Ve0aFEM-l+g&EjF&dOl6(1 zE~|VMyd?TCW1h-dFj9{Z?!xR9^7-b2j+6bz=7h>lyCJOm#bQqLL)Fr2P%2^z3L35S zapw|_Q-4JVm5yb+PwjnT%8OEyY)i%=WDP#M5=eL~s@jylD zruLN7}oYhL}c<^4<22QkmQj|AHle+b-4FI$~XTzEB2rU)A-tkWX(1N$m0Qva>Tuz)8|Ypl~@87H$a zAtN68KEWO2Cl`EPBYHWS!~&TY?fGUB7mWzinVJi1brhN5@`xL*kXg9m6Y=jEVJ$c$X{iI|HgIpH$#C1a8yiTi5c-F)x}0!asZeus3c^a&_^ z!U-n_=Q(Kv%G-EKT2vKrHg(DycN*x{Cg_T_AoTLyf?_D=0&==Zu~=ELhyBc(S127I z7pOJb(Q=dT=xJd>ly6DMhuuP~blM)AIjBy!%_r3+I?5Gjse4~z&e2Q91{QQVScjc+ z4%n&0sBtx7P%&a2GR=HRW|JzhRrQK8=E#)+Dx2waCXUMV!f3n4N2P!@#{+|J+Wb*> z9sxk~dIOAXqhN!;ig(}W5dsd`6qOxm$`z5uxVC;AYtiH(6r;uBqTn?t z@;2HHDQ-qGfl^&cfzbx&h71ylMWA#CiaxKor0^3fXhc`g@B_LVOxW;a$(eYFGl7c} z*sYxKkQ3V%h^UV50&((sRR@ZRnF2DW)+*R+#RVaYyzn(%+`RBL+ALXR*wDI7sYk^H zcg2!Iu$Eer{b&ewHK`7ca;vNgvh<9=onH@tft7Yi47wyN5gFLnRa|5ZIK`xf%N;yK z4VVxaB;(zoBJU$kuxwH@u6Y@uBBy6ulVa~XUb8Yt(U^dNcP0+bj02OT+zTuG$?OFc zJTdnU6f`yWjuSLF_reK%4nJW+x%`Kr>D-WwJGszE-8HYlfMv#l??AtfGPYC-GRe5B zhk{5w?XSr`WkGtJDRB=6cJcKZur@9#Hw^Ovr%h5odk0TYwPgV5#)L@;rjpqRwvvvm z=H9YH*K8Gx?AGuiFy5uBS2K0`;(!X3wtEYz4jQXZ+AUz7F3nCSXud(ms9*_o%B~pt zgsG6wQw7KuyG5gSVa1)dglR|bgTsJK&Td_|$w8wrbNFaX4Ig0yx579K(sp3S zTu(TQhc`xC(r$x0+BwV}C<6z4uxCv8vCht(3JG1D6D7PfjcfNc2nHAE1(b(ODe6=# zgjz}Ar3&Sz-O+s>RnQ84NuM#{M?0v&9pLBBLOks5HAx zrId#)m8us}e*sIRyd}NFm9U6ZOnUC&Jd>%Hq-+wDF)NIY zsmVe^mEs*M{=k=+gpa9hr(U>l;^Ns@WGIGBGi+cX$3fJY1S`kdCkGZUCKu!ud0tNy zCS;`TuGkE53r?6nZ!|KXQ)YM5kzWMPIYxyp%A*W+c7gCHH?rMtLq_OCZ^pRGq&X^5 zH0$$ZVgy|tC{DyiX2)*eMw~+?tFxp*`Z~RWy6)@13VNAv3|Bno2`gG!>SNNP00OLX z$_;P!Q-c)R_O96}=>_1`?#IM*xnE%2?!STE8n?+hP|llVR*QoYtwv%vO2#rwOAh_Lw&nUi3n64OJ~0d6|p)0R%Svl`>ZACHKyCmtjIE-`}5w!&ut zZ^dTpdleXw2Q}n4#Rq-JVU;*&bz}rz6+1a`J_U{(>8GEtKwlL(su`yyQwFF8*gf8U znG=#@tk0vKlf0XM$Cc_c?2kOL15fND7wj&7N{tYCDfbQcN2c6?DK{JaK1!Xq>Gp#8 zBSYrEkeQc0=O6q#H{g%lfCD#RY#pY+WFwTwJaD--})2zJcN7Nr#QU{jQ zO!S(K_q;~@ksWbhN6bl&;l1WN+>g9)120@`-K|*=ti0t_nxRKE!i!sT;fO6A6ZfdmegGI znw5L}5c?xf?7$P7i*B=Vl~<8J@rBiV>}4(L^_Q-%xg|t%7VoIgyUO)IWtSLpF8z0+rrT2i1wBw<GIg9gKlwa!#|>?4`YU0+(S0ZW6TQ}8+~?mE}@-*v{6po z4@%E!^u`je{vvKn+`qzHFudKX#96Uw-|l||&8pvow`xsY)S2lP9nNBRZn~w-ZjJ!& zb@_liw$id;WhdN|ZMm>kQ^85PbypD;wiLFe&SSAQFS&8bH_jO)?KdGk#m}bMN~3}l zb6-O5&8czD;jGxtC=EX4QQOWPkvUrxQqw6PMW^#iwpH-7mqyJDP?HJ=39YGZcCu1+ zAA6xY4yf>BKXaC+Fnfz@q@|+^yv?658JN@BT=I6I#~O5XnD#Z%A?cw)hU|?yiZ+?X zOWF}c*327J0Tl2(qYbJ(umFun9dB1aONZuaM-#TDmJU13rWbPKLW=mPfpOe{jaSH;DH@C1EjYh+ZVKAb$5VNk`!5h6rKR_`JETN9~$qRS?-if3kl9teh6xG zOv=_9Lc%Bu4@@D}5*i+O?eY}}CXy|`0-Gw}h#cw5tqRDD3e78f0I55Jj!09Rg)9lQ z-D%ThZ&&0`LW*D?AeSrnkc^SC*wGxLYr-T$5>W$PP+Z}oT36S2=QE}M*Kj#Z$nU23 zzf5PHPUTK2)n3a>qJUaW&C&HHF%m(37PwYVpwXyM}%RPS&Owbw8y zEo~zsR{6cBpbqDH?;5Aa##b;69-VykH#R=E=GSocfmFZOW;UbN~7TeOuG-U)SCmUxBWWkT6hkdnjFI@@{0=3 z**b@Ko7&uPrx8QvTS}xd)lV=>1^ZH<7g9l2ae*h)nU^Y#8Qmsm$hjvoG$d%~a^Wk| z60R4M0}Cxp%x$tWUU0{`Hy}P|uM*VB+#}9RM5T+AC?r2pBWadjJa> z?>(TU5GpCl0XXhtpbn6Hw09zAqP`O?7yX@Z87VM%4s66*U^xi4Bm^|smYK+a6WmXz zkQCZ5z)b2T0d5FgFl$#(oT7+NATdt_Zz(kc$e6lL9q>7^61CSb(Q<0?Z>Y3WW3AaA z8|#wpr(9#WJD*>nJ>SFHrX4KuF3|Fg;<)$M(P8Ic+LkbjCUNXm;xahTTlkWTnk`CJFh=h~#G|3wCYD~tFw zUZfg{YpkBmbZ6MS8X;Z(|0*vBWw-){*9MhcQ*?7$AdxN@twMtHRV>^=rXUHK?*KVV z)Jwhl(#SEp>TBh1zu}#Od&+JM_dVvJ2ul-Cd%K|kg*fC;GKXk6tQnvenGSM|I@4jUP!wyHI@3)b6j;0ZN03HS16yF! zzQfw~zN15GuzC%*Z7IPS)P&E3InFgw4`{8Pt~s?UnS-JhVXRx*GSPW^YUE)Hbf4k% z+`wv)vCFj@NOwTd#Yl|UVyu_wz~xvWvCU&; z1Rm@`4frlPL6R2?&1hBBvIais1$xLUX^;atXxx?@3MJ6Jj0-9JgooS}DrA(|gB*$% zKHOk@N{j+(M|I6tvdbE6JN43haw8oK;LnyNG`bO5vcd~CjJAO)p?-}Q9VH$hTce2( z*B(J0k{7bk-Gt)T_v$OQFA#eo_cA`81Gsm-~dkd6EU|3X2dy>pxyzX%Phz$ zdFG{epE#jHXJ_0H7YCDva4RY&;f_|ed+Uxybz9Z(kn#;P193xcC{1XHcQYy)R5kb; zHmu|)m7Tao+ulv>F=DgP+dNg0(AJMhY{njDW~{JM2}Pg~{hko6!SD*Bakh-O8xsw= zLApb^q#YyG%T<;>sKarV7l?kWCu#8%5hb|5J2yE)=Dg2h#p6R3tI?X+{Z7=#|`qBZe5k2~kz?yaUs!M!LS)!W=3Wwqjq}+ zoaMLNz<$Op>MY=P40$1{{UV#FS_axNqiZXCaNGNKhu9J?D$P*b51No*3Y(ktFiv(z z9km`{hHjIMbCd^mhGRj_$ZjmC`RMX3G$ReELN4lEt{q0KKF_Zrhf1Zm00(t$`CNL( z8Mke)+Pi-Rw$~wxm`*f>>-)Px{jl7-tR@C>uVIZg>M!UDao(R5(c}pi!Epykd!RgY zHaMdISGTCbL4}4sUu<-WxQLrkn@-%8<3u8)R$F>2=#nZf&fgyDgRA>K$ zjTm?G9WLSS3!q7y*i0BjOUISChy&7r{yp7jlJ-WuWFjHG#wB&YYsz$wMd%J!55s7o zSH0AF0aZlrju0#d|BerMIWD!OsdJaKT@-UYY{=O%q0WW>B3{wa=dhnp6Jsi@w)tP1 z|F7Ig&nP!$zwwa?u=9~Bq6^H;x>e$ue*`V#y*kB49W=D&R>&8x1309Yt)gaK5ub9< z)D{7j)5t_Ae7y^MF(Oa-L2oJDTC|dmAVs}si7QvDQZ@Zx*I(8+tNjEA_2VXv8@vt^ z4>TMHJKv(!84M`N%Fy$nf9)2I-5fI6^^n)5;=4_lFv0i~;6M+Buq+f_J&(E}8j zQH-uztgA=y6qBM_s6hesC%dLH3h7Gu9b3}vK0?yDqtppjc9jlSdf_vy=rS6t+$@8Sjk1#Ct$6c@o?prRQ4}XrB~o^Wmg}g3a>tfm0oG@3M#w81}Ql|7D7a>;@DXi zAZNtP2aJZQZG%A4LH>ZKWqikHOp1h(%WImMa4=_dW;)X7H~|>paNYT z2g{`WIyVpC%F05Cr?dzGZ(D1*6h70f@-m!n*Wl2^fVn&q8D1CG6+Woq)+>CtHYr<8 zEsa>&T0fJ#V(38shgAho$~1A>Iusp4P&+Uf;ld59~+J}8AvC%`2* z5PbY8JIR^2}t2Sq_Mt(gfumG zlt`T_CrB!T+Ps5=`Vo-|5(+W%v4EAct9Q6$Kf^d?hZucyK?l~VbaY|gXbUO41zs1N_i*0B0#JqE0i3HDR!aesN4mdqk=EMc_}$N z!_Codb2Qsrt#iaO z*h{x7cGNqq0{v0lN+aq|1UG}Y{!QYM=@-$K^AtYQvz)|({N#e5bWu?)8&v> zO0;z{DHKH9s=7-|=<2%npd1)^4EcfH15629e)kM{L_HqvyJF98uJbK#?c(ep_@E+4G;0B3G=bZa^bE~q@J&SK*Aa?s|4 z$(E-q-RJe!bklPHGVcOfcs}uApG(|Ay6hg<4jWRvfJ+fC^qc^L8eCG1N~RN3K2A7P zg`7xtE+%+DQL2VOV%F-Y>XngM2OKgn%G5mpLP z^?Kjzlr(8aRY$$Ev35!XD@5=J6{@|!4dkSRK%LHznr)27xYcDBdW@xZI*^gOXQTHC zth!ulkfpy~Y+T<{5p&fsU=40eEI{c+-7)}$+LO8Gq$+YM2#-A7OPe<2`Cb!{)Jv;8 zIXK9Wr~_T;&<*=cS?4w|BK(x8cBheTnKL+Np{tPD1IGzS*9bdhk`&N-Igx@J$%%3& zgWT(&1yn?{Mha@qCpwRf$6>-ya4nA(UlT)&kkRanS5t!oQn%kV&ZxIm8j%%qWWWhM zb?P%@rA#l#@;k&YD0qkHcPfX1U4DB^6co17%T}k_k_#(g{0S^z$?hk#c)dx!1`Cvt ze~;Qnq%GfjKBS|gnT`%q%k`-;&4(SFMLGl4sf|onW(X?aqQN4j$yrhYoAD(kuKg~Q zu=Zx^t|I8C5smPm12sw=WV_Q=K~L-(RHV$E2Z&26w57cP%;~dEU9A#3?4rFqsf)GX z0F7vBZxIoV?or#$9b-csfOV@V&)yq(5=ipAWmGRZiPm+nfH-C7Z^@8{6=*O07Hz6t z0~Fk@5GGi-DK!lzE%J+Vd!@Vz&0te#r1!zS=plI@>J zp)#?pv$kFA74eX9NKH3d?K& literal 0 HcmV?d00001 diff --git a/test/functional/fixtures/es_archiver/mgmt/mappings.json b/test/functional/fixtures/es_archiver/mgmt/mappings.json new file mode 100644 index 0000000000000..107a45fab187b --- /dev/null +++ b/test/functional/fixtures/es_archiver/mgmt/mappings.json @@ -0,0 +1,283 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + }, + "aliases": {} + } +} \ No newline at end of file diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 1f2fc65a1182c..55d64b3ae18a4 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -570,9 +570,10 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await testSubjects.click('importSavedObjectsConfirmBtn'); } - async setImportIndexFieldOption(child) { + async associateIndexPattern(oldIndexPatternId, newIndexPatternTitle) { await find.clickByCssSelector( - `select[data-test-subj="managementChangeIndexSelection"] > option:nth-child(${child})` + `select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] > + [data-test-subj="indexPatternOption-${newIndexPatternTitle}"]` ); } @@ -580,18 +581,6 @@ export function SettingsPageProvider({ getService, getPageObjects }) { await testSubjects.click('changeIndexConfirmButton'); } - async clickVisualizationsTab() { - await testSubjects.click('objectsTab-visualizations'); - } - - async clickSearchesTab() { - await testSubjects.click('objectsTab-searches'); - } - - async getVisualizationRows() { - return await testSubjects.findAll(`objectsTableRow`); - } - async waitUntilSavedObjectsTableIsNotLoading() { return retry.try(async () => { const exists = await find.existsByDisplayedByCssSelector( From b26e2b46f05e5af099385eda3eef8f77a40dbcc0 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 5 Sep 2018 15:33:57 -0600 Subject: [PATCH 22/68] Pass scoped context to tutorial providers when building tutorials (#22260) * Pass scoped context to tutorial providers when building tutorials * only generated scoped context one time * spelling --- .../routes/api/home/register_tutorials.js | 2 +- src/ui/tutorials_mixin.js | 28 ++++-- src/ui/tutorials_mixin.test.js | 86 +++++++++++++++++++ 3 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 src/ui/tutorials_mixin.test.js diff --git a/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js b/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js index c7e0909631eae..89f1ff6213a14 100644 --- a/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js +++ b/src/core_plugins/kibana/server/routes/api/home/register_tutorials.js @@ -23,7 +23,7 @@ export function registerTutorials(server) { path: '/api/kibana/home/tutorials', method: ['GET'], handler: async function (req, reply) { - reply(server.getTutorials()); + reply(server.getTutorials(req)); } }); } diff --git a/src/ui/tutorials_mixin.js b/src/ui/tutorials_mixin.js index 7ab14bfc45080..af3663a83b812 100644 --- a/src/ui/tutorials_mixin.js +++ b/src/ui/tutorials_mixin.js @@ -17,24 +17,40 @@ * under the License. */ -import _ from 'lodash'; import Joi from 'joi'; import { tutorialSchema } from '../core_plugins/kibana/common/tutorials/tutorial_schema'; export function tutorialsMixin(kbnServer, server) { - const tutorials = []; + const tutorialProviders = []; + const scopedTutorialContextFactories = []; - server.decorate('server', 'getTutorials', () => { - return _.cloneDeep(tutorials); + server.decorate('server', 'getTutorials', (request) => { + const initialContext = {}; + const scopedContext = scopedTutorialContextFactories.reduce((accumulatedContext, contextFactory) => { + return { ...accumulatedContext, ...contextFactory(request) }; + }, initialContext); + + return tutorialProviders.map((tutorialProvider) => { + return tutorialProvider(server, scopedContext); + }); }); server.decorate('server', 'registerTutorial', (specProvider) => { - const { error, value } = Joi.validate(specProvider(server), tutorialSchema); + const emptyContext = {}; + const { error } = Joi.validate(specProvider(server, emptyContext), tutorialSchema); if (error) { throw new Error(`Unable to register tutorial spec because its invalid. ${error}`); } - tutorials.push(value); + tutorialProviders.push(specProvider); + }); + + server.decorate('server', 'addScopedTutorialContextFactory', (scopedTutorialContextFactory) => { + if (typeof scopedTutorialContextFactory !== 'function') { + throw new Error(`Unable to add scoped(request) context factory because you did not provide a function`); + } + + scopedTutorialContextFactories.push(scopedTutorialContextFactory); }); } diff --git a/src/ui/tutorials_mixin.test.js b/src/ui/tutorials_mixin.test.js new file mode 100644 index 0000000000000..b29ff10a4d798 --- /dev/null +++ b/src/ui/tutorials_mixin.test.js @@ -0,0 +1,86 @@ +/* + * 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 { createServer } from '../test_utils/kbn_server'; + +const validTutorial = { + id: 'spec1', + category: 'other', + name: 'spec1', + shortDescription: 'short description', + longDescription: 'long description', + onPrem: { + instructionSets: [ + { + instructionVariants: [ + { + id: 'instructionVariant1', + instructions: [ + {} + ] + } + ] + } + ] + } +}; + +describe('tutorial mixins', () => { + + let kbnServer; + beforeEach(async () => { + kbnServer = createServer(); + await kbnServer.ready(); + }); + + afterEach(async () => { + await kbnServer.close(); + }); + + describe('scoped context', () => { + + const mockRequest = {}; + const spacesContextFactory = (request) => { + if (request !== mockRequest) { + throw new Error('context factory not called with request object'); + } + return { + spaceId: 'my-space' + }; + }; + const specProvider = (server, context) => { + const tutorial = { ...validTutorial }; + tutorial.shortDescription = `I have been provided with scoped context, spaceId: ${context.spaceId}`; + return tutorial; + }; + beforeEach(async () => { + kbnServer.server.addScopedTutorialContextFactory(spacesContextFactory); + kbnServer.server.registerTutorial(specProvider); + }); + + test('passes scoped context to specProviders', () => { + const tutorials = kbnServer.server.getTutorials(mockRequest); + expect(tutorials.length).toBe(1); + expect(tutorials[0].shortDescription).toBe('I have been provided with scoped context, spaceId: my-space'); + }); + }); + +}); + + From 97fccac8d13de3e6f55be1aa20e52b35da59537c Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Wed, 5 Sep 2018 19:49:18 -0400 Subject: [PATCH 23/68] Fix IE scrollbar issue on TSVB gauges in dashboard (#22740) --- .../metrics/public/visualizations/components/gauge_vis.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js b/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js index 7f129d3e061a1..bb0e205e87393 100644 --- a/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js +++ b/src/core_plugins/metrics/public/visualizations/components/gauge_vis.js @@ -85,7 +85,8 @@ class GaugeVis extends Component { position: 'relative', display: 'flex', rowDirection: 'column', - flex: '1 0 auto' + flex: '1 0 auto', + overflow: 'hidden', // Fixes IE scrollbars issue }, svg: { position: 'absolute', From eeee0d800ae1601254af94cbbb3106b207b91c65 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Wed, 5 Sep 2018 19:54:38 -0500 Subject: [PATCH 24/68] x-pack tests should use servers from other config files already loaded (#22739) * x-pack tests should use servers from other config files already loaded * Fix es_test_config that was using TEST_KIBANA_USERNAME/PASSWORD --- packages/kbn-test/src/es/es_test_config.js | 5 +++-- x-pack/test/functional/config.js | 24 +--------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/kbn-test/src/es/es_test_config.js b/packages/kbn-test/src/es/es_test_config.js index 2e9e91e50b4b2..e273172ef614b 100644 --- a/packages/kbn-test/src/es/es_test_config.js +++ b/packages/kbn-test/src/es/es_test_config.js @@ -53,8 +53,9 @@ export const esTestConfig = new class EsTestConfig { }; } - const username = process.env.TEST_KIBANA_USERNAME || adminTestUser.username; - const password = process.env.TEST_KIBANA_PASSWORD || adminTestUser.password; + const username = process.env.TEST_ES_USERNAME || adminTestUser.username; + const password = process.env.TEST_ES_PASSWORD || adminTestUser.password; + return { // Allow setting any individual component(s) of the URL, // or use default values (username and password from ../kbn/users.js) diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 5f4cb60fa81e8..77c0aee49b83e 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -7,7 +7,6 @@ /* eslint-disable kibana-custom/no-default-export */ import { resolve } from 'path'; -import { format as formatUrl } from 'url'; import { SecurityPageProvider, @@ -57,25 +56,6 @@ export default async function ({ readConfigFile }) { const kibanaFunctionalConfig = await readConfigFile(require.resolve('../../../test/functional/config.js')); const kibanaAPITestsConfig = await readConfigFile(require.resolve('../../../test/api_integration/config.js')); - const servers = { - elasticsearch: { - protocol: process.env.TEST_ES_PROTOCOL || 'http', - hostname: process.env.TEST_ES_HOSTNAME || 'localhost', - port: parseInt(process.env.TEST_ES_PORT, 10) || 9240, - auth: 'elastic:changeme', - username: 'elastic', - password: 'changeme', - }, - kibana: { - protocol: process.env.TEST_KIBANA_PROTOCOL || 'http', - hostname: process.env.TEST_KIBANA_HOSTNAME || 'localhost', - port: parseInt(process.env.TEST_KIBANA_PORT, 10) || 5640, - auth: 'elastic:changeme', - username: 'elastic', - password: 'changeme', - }, - }; - return { // list paths to the files that contain your plugins tests testFiles: [ @@ -135,7 +115,7 @@ export default async function ({ readConfigFile }) { reporting: ReportingPageProvider, }, - servers, + servers: kibanaFunctionalConfig.get('servers'), esTestCluster: { license: 'trial', @@ -151,8 +131,6 @@ export default async function ({ readConfigFile }) { serverArgs: [ ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', - `--server.port=${servers.kibana.port}`, - `--elasticsearch.url=${formatUrl(servers.elasticsearch)}`, '--xpack.xpack_main.telemetry.enabled=false', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions ], From 8d8513ca6cfeb7551e62ca2b84e7bcc06bb64095 Mon Sep 17 00:00:00 2001 From: Aliaksandr Yankouski Date: Thu, 6 Sep 2018 09:55:29 +0300 Subject: [PATCH 25/68] Translations for tutorial common (#22071) * filebeat_instructions translations * tutorials common * fix translations * use unicode code * folder path to variable * Fix message id * Remove disabling no-multi-str rule, remove spaces --- .../common/tutorials/filebeat_instructions.js | 467 +++++++++++++----- .../common/tutorials/logstash_instructions.js | 53 +- .../tutorials/metricbeat_instructions.js | 428 +++++++++++----- .../tutorials/onprem_cloud_instructions.js | 47 +- 4 files changed, 718 insertions(+), 277 deletions(-) diff --git a/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js index 692074d2ce897..b96f1f6b1b7d0 100644 --- a/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/filebeat_instructions.js @@ -17,16 +17,22 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from './instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; export const createFilebeatInstructions = () => ({ INSTALL: { OSX: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', 'tar xzvf filebeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -34,76 +40,123 @@ export const createFilebeatInstructions = () => ({ ], }, DEB: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-amd64.deb', 'sudo dpkg -i filebeat-{config.kibana.version}-amd64.deb', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/filebeat).', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), }, RPM: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({linkUrl}).', + values: { + linkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi filebeat-{config.kibana.version}-x86_64.rpm', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/filebeat).', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.rpmTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({linkUrl}).', + values: { + linkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + }, + }), }, WINDOWS: { - title: 'Download and install Filebeat', - textPre: - 'First time using Filebeat? See the [Getting Started Guide]' + - '({config.docs.beats.filebeat}/filebeat-getting-started.html).\n' + - '1. Download the Filebeat Windows zip file from the [Download](https://www.elastic.co/downloads/beats/filebeat) page.\n' + - '2. Extract the contents of the zip file into `C:\\Program Files`.\n' + - '3. Rename the `filebeat-{config.kibana.version}-windows` directory to `Filebeat`.\n' + - '4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select' + - ' **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n' + - '5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.windowsTextPre', { + defaultMessage: 'First time using Filebeat? See the [Getting Started Guide]({guideLinkUrl}).\n\ + 1. Download the Filebeat Windows zip file from the [Download]({filebeatLinkUrl}) page.\n\ + 2. Extract the contents of the zip file into {folderPath}.\n\ + 3. Rename the `{directoryName}` directory to `Filebeat`.\n\ + 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ + 5. From the PowerShell prompt, run the following commands to install Filebeat as a Windows service.', + values: { + folderPath: '`C:\\Program Files`', + guideLinkUrl: '{config.docs.beats.filebeat}/filebeat-getting-started.html', + filebeatLinkUrl: 'https://www.elastic.co/downloads/beats/filebeat', + directoryName: 'filebeat-{config.kibana.version}-windows', + } + }), commands: [ 'PS > cd C:\\Program Files\\Filebeat', 'PS C:\\Program Files\\Filebeat> .\\install-service-filebeat.ps1', ], - textPost: - 'Modify the settings under `output.elasticsearch` in the ' + - '`C:\\Program Files\\Filebeat\\filebeat.yml` file to point to your Elasticsearch installation.', - }, + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.install.windowsTextPost', { + defaultMessage: 'Modify the settings under {propertyName} in the {filebeatPath} file to point to your Elasticsearch installation.', + values: { + propertyName: '`output.elasticsearch`', + filebeatPath: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + } + }), + } }, START: { OSX: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards.' + - ' If the dashboards are already set up, omit this command.', - commands: ['./filebeat setup', './filebeat -e'], + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.osxTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + './filebeat setup', + './filebeat -e', + ] }, DEB: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo filebeat setup', 'sudo service filebeat start'], + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.debTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo filebeat setup', + 'sudo service filebeat start', + ] }, RPM: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo filebeat setup', 'sudo service filebeat start'], + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo filebeat setup', + 'sudo service filebeat start', + ], }, WINDOWS: { - title: 'Start Filebeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Filebeat', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.start.windowsTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), commands: [ 'PS C:\\Program Files\\Filebeat> filebeat.exe setup', 'PS C:\\Program Files\\Filebeat> Start-Service filebeat', @@ -112,8 +165,15 @@ export const createFilebeatInstructions = () => ({ }, CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -122,13 +182,26 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/filebeat/filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -137,13 +210,26 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/filebeat/filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -152,14 +238,26 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Filebeat\\filebeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -168,98 +266,209 @@ export const createFilebeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', - }, + textPost: i18n.translate('kbn.common.tutorials.filebeatInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + } }, PLUGINS: { GEOIP_AND_UA: { - title: 'Install Elasticsearch GeoIP and user agent plugins', - textPre: - 'This module requires two Elasticsearch plugins that are not ' + - 'installed by default.\n\nFrom the Elasticsearch installation folder, run:', + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipUaTitle', { + defaultMessage: 'Install Elasticsearch GeoIP and user agent plugins', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipUaTextPre', { + defaultMessage: 'This module requires two Elasticsearch plugins that are not installed by default.\n\n\ +From the Elasticsearch installation folder, run:', + }), commands: [ 'bin/elasticsearch-plugin install ingest-geoip', 'bin/elasticsearch-plugin install ingest-user-agent', ], }, GEOIP: { - title: 'Install Elasticsearch GeoIP plugin', - textPre: - 'This module requires an Elasticsearch plugin that is not ' + - 'installed by default.\n\nFrom the Elasticsearch installation folder, run:', - commands: ['bin/elasticsearch-plugin install ingest-geoip'], - }, - }, + title: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipTitle', { + defaultMessage: 'Install Elasticsearch GeoIP plugin', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatInstructions.plugins.geoipTextPre', { + defaultMessage: 'This module requires two Elasticsearch plugins that are not installed by default.\n\n\ +From the Elasticsearch installation folder, run:', + }), + commands: [ + 'bin/elasticsearch-plugin install ingest-geoip' + ] + } + } }); export const createFilebeatCloudInstructions = () => ({ CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/filebeat/filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/filebeat/filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/filebeat/filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Filebeat\\filebeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', - }, - }, + title: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`C:\\Program Files\\Filebeat\\filebeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatCloudInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + } + } }); export function filebeatEnableInstructions(moduleName) { return { OSX: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the installation directory, run:', - commands: ['./filebeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.osxTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.osxTextPre', { + defaultMessage: 'From the installation directory, run:', + }), + commands: [ + './filebeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.osxTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, DEB: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo filebeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `/etc/filebeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.debTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo filebeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.debTextPost', { + defaultMessage: 'Modify the settings in the `/etc/filebeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, RPM: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo filebeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `/etc/filebeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.rpmTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo filebeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.rpmTextPost', { + defaultMessage: 'Modify the settings in the `/etc/filebeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, WINDOWS: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the `C:\\Program Files\\Filebeat` folder, run:', - commands: ['PS C:\\Program Files\\Filebeat> filebeat.exe modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', - }, + title: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.windowsTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.windowsTextPre', { + defaultMessage: 'From the {path} folder, run:', + values: { path: `C:\\Program Files\\Filebeat` }, + }), + commands: [ + 'PS C:\\Program Files\\Filebeat> filebeat.exe modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.filebeatEnableInstructions.windowsTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), + } }; } export function filebeatStatusCheck(moduleName) { return { - title: 'Module status', - text: 'Check that data is received from the Filebeat `' + moduleName + '` module', - btnLabel: 'Check data', - success: 'Data successfully received from this module', - error: 'No data has been received from this module yet', + title: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.title', { + defaultMessage: 'Module status', + }), + text: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.text', { + defaultMessage: 'Check that data is received from the Filebeat `{moduleName}` module', + values: { moduleName }, + }), + btnLabel: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.buttonLabel', { + defaultMessage: 'Check data', + }), + success: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.successText', { + defaultMessage: 'Data successfully received from this module', + }), + error: i18n.translate('kbn.common.tutorials.filebeatStatusCheck.errorText', { + defaultMessage: 'No data has been received from this module yet', + }), esHitsCheck: { index: 'filebeat-*', query: { @@ -299,7 +508,9 @@ export function onPremInstructions(moduleName, platforms, geoipRequired, uaRequi return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.filebeat.premInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: variants, statusCheck: filebeatStatusCheck(moduleName), }, @@ -331,7 +542,9 @@ export function onPremCloudInstructions(moduleName, platforms) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.filebeat.premCloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: variants, statusCheck: filebeatStatusCheck(moduleName), }, @@ -360,7 +573,9 @@ export function cloudInstructions(moduleName, platforms) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.filebeat.cloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: variants, statusCheck: filebeatStatusCheck(moduleName), }, diff --git a/src/core_plugins/kibana/common/tutorials/logstash_instructions.js b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js index 4c52314236d10..5203f44ef9c8d 100644 --- a/src/core_plugins/kibana/common/tutorials/logstash_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/logstash_instructions.js @@ -17,19 +17,28 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export const createLogstashInstructions = () => ({ INSTALL: { OSX: [ { - title: 'Download and install the Java Runtime Environment', - textPre: - 'Follow the installation instructions [here](https://docs.oracle.com/javase/8/docs/technotes/guides/install/mac_jre.html).', + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.osxTitle', { + defaultMessage: 'Download and install the Java Runtime Environment', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.osxTextPre', { + defaultMessage: 'Follow the installation instructions [here]({link}).', + values: { link: 'https://docs.oracle.com/javase/8/docs/technotes/guides/install/mac_jre.html' }, + }), }, { - title: 'Download and install Logstash', - textPre: - 'First time using Logstash? See the ' + - '[Getting Started Guide]({config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html).', + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.osxTitle', { + defaultMessage: 'Download and install Logstash', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.osxTextPre', { + defaultMessage: 'First time using Logstash? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.tar.gz', 'tar xzvf logstash-{config.kibana.version}.tar.gz', @@ -38,18 +47,28 @@ export const createLogstashInstructions = () => ({ ], WINDOWS: [ { - title: 'Download and install the Java Runtime Environment', - textPre: - 'Follow the installation instructions [here](https://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jre_install.html).', + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.windowsTitle', { + defaultMessage: 'Download and install the Java Runtime Environment', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.java.windowsTextPre', { + defaultMessage: 'Follow the installation instructions [here]({link}).', + values: { link: 'https://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jre_install.html' }, + }), }, { - title: 'Download and install Logstash', - textPre: - 'First time using Logstash? See the ' + - '[Getting Started Guide]({config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html).\n' + - ' 1. [Download](https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.zip) the Logstash Windows zip file.\n' + - ' 2. Extract the contents of the zip file.', - }, + title: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.windowsTitle', { + defaultMessage: 'Download and install Logstash', + }), + textPre: i18n.translate('kbn.common.tutorials.logstashInstructions.install.logstash.windowsTextPre', { + defaultMessage: 'First time using Logstash? See the [Getting Started Guide]({logstashLink}).\n\ + 1. [Download]({elasticLink}) the Logstash Windows zip file.\n\ + 2. Extract the contents of the zip file.', + values: { + logstashLink: '{config.docs.base_url}guide/en/logstash/current/getting-started-with-logstash.html', + elasticLink: 'https://artifacts.elastic.co/downloads/logstash/logstash-{config.kibana.version}.zip' + }, + }), + } ], }, }); diff --git a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js index 8e1b9b95bcb47..b15d3aaab1e7e 100644 --- a/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/metricbeat_instructions.js @@ -17,16 +17,20 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from './instruction_variant'; import { createTrycloudOption1, createTrycloudOption2 } from './onprem_cloud_instructions'; export const createMetricbeatInstructions = () => ({ INSTALL: { OSX: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.osxTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.osxTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', 'tar xzvf metricbeat-{config.kibana.version}-darwin-x86_64.tar.gz', @@ -34,76 +38,112 @@ export const createMetricbeatInstructions = () => ({ ], }, DEB: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-amd64.deb', 'sudo dpkg -i metricbeat-{config.kibana.version}-amd64.deb', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/metricbeat).', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + }), }, RPM: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.rpmTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.rpmTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({link}).', + values: { link: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html' }, + }), commands: [ 'curl -L -O https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi metricbeat-{config.kibana.version}-x86_64.rpm', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page](https://www.elastic.co/downloads/beats/metricbeat).', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.debTextPost', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({link}).', + values: { link: 'https://www.elastic.co/downloads/beats/metricbeat' }, + }), }, WINDOWS: { - title: 'Download and install Metricbeat', - textPre: - 'First time using Metricbeat? See the [Getting Started Guide]' + - '({config.docs.beats.metricbeat}/metricbeat-getting-started.html).\n' + - '1. Download the Metricbeat Windows zip file from the [Download](https://www.elastic.co/downloads/beats/metricbeat) page.\n' + - '2. Extract the contents of the zip file into `C:\\Program Files`.\n' + - '3. Rename the `metricbeat-{config.kibana.version}-windows` directory to `Metricbeat`.\n' + - '4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select' + - ' **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n' + - '5. From the PowerShell prompt, run the following commands to install Metricbeat as a Windows service.', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.windowsTitle', { + defaultMessage: 'Download and install Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.windowsTextPre', { + defaultMessage: 'First time using Metricbeat? See the [Getting Started Guide]({metricbeatLink}).\n\ + 1. Download the Metricbeat Windows zip file from the [Download]({elasticLink}) page.\n\ + 2. Extract the contents of the zip file into {folderPath}.\n\ + 3. Rename the {directoryName} directory to `Metricbeat`.\n\ + 4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n\ + 5. From the PowerShell prompt, run the following commands to install Metricbeat as a Windows service.', + values: { + directoryName: '`metricbeat-{config.kibana.version}-windows`', + folderPath: '`C:\\Program Files`', + metricbeatLink: '{config.docs.beats.metricbeat}/metricbeat-getting-started.html', + elasticLink: 'https://www.elastic.co/downloads/beats/metricbeat', + }, + }), commands: [ 'PS > cd C:\\Program Files\\Metricbeat', 'PS C:\\Program Files\\Metricbeat> .\\install-service-metricbeat.ps1', ], - textPost: - 'Modify the settings under `output.elasticsearch` in the ' + - '`C:\\Program Files\\Metricbeat\\metricbeat.yml` file to point to your Elasticsearch installation.', - }, + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.install.windowsTextPost', { + defaultMessage: 'Modify the settings under `output.elasticsearch` in the {path} file to point to your Elasticsearch installation.', + values: { path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`' }, + }), + } }, START: { OSX: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards.' + - ' If the dashboards are already set up, omit this command.', - commands: ['./metricbeat setup', './metricbeat -e'], + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.osxTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.osxTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + './metricbeat setup', + './metricbeat -e', + ] }, DEB: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.debTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.debTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo metricbeat setup', + 'sudo service metricbeat start', + ] }, RPM: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', - commands: ['sudo metricbeat setup', 'sudo service metricbeat start'], + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.rpmTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.rpmTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), + commands: [ + 'sudo metricbeat setup', + 'sudo service metricbeat start', + ], }, WINDOWS: { - title: 'Start Metricbeat', - textPre: - 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, ' + - 'omit this command.', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.windowsTitle', { + defaultMessage: 'Start Metricbeat', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.start.windowsTextPre', { + defaultMessage: 'The `setup` command loads the Kibana dashboards. If the dashboards are already set up, omit this command.', + }), commands: [ 'PS C:\\Program Files\\Metricbeat> metricbeat.exe setup', 'PS C:\\Program Files\\Metricbeat> Start-Service metricbeat', @@ -112,8 +152,15 @@ export const createMetricbeatInstructions = () => ({ }, CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -122,13 +169,26 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -137,13 +197,26 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -152,14 +225,26 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Metricbeat\\metricbeat.yml` to set the connection information:', + title: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information:', + values: { + path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + }, + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -168,81 +253,182 @@ export const createMetricbeatInstructions = () => ({ 'setup.kibana:', ' host: ""', ], - textPost: - 'Where `` is the password of the `elastic` user, ' + - '`` is the URL of Elasticsearch, and `` is the URL of Kibana.', - }, - }, + textPost: i18n.translate('kbn.common.tutorials.metricbeatInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user, {esUrlTemplate} is the URL of Elasticsearch, \ +and {kibanaUrlTemplate} is the URL of Kibana.', + values: { + passwordTemplate: '``', + esUrlTemplate: '``', + kibanaUrlTemplate: '``', + }, + }), + } + } }); export const createMetricbeatCloudInstructions = () => ({ CONFIG: { OSX: { - title: 'Edit the configuration', - textPre: 'Modify `metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.osxTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.osxTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.osxTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, DEB: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.debTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.debTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.debTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, RPM: { - title: 'Edit the configuration', - textPre: - 'Modify `/etc/metricbeat/metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.rpmTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.rpmTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`/etc/metricbeat/metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.rpmTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), }, WINDOWS: { - title: 'Edit the configuration', - textPre: - 'Modify `C:\\Program Files\\Filebeat\\metricbeat.yml` to set the connection information for Elastic Cloud:', - commands: ['cloud.id: "{config.cloud.id}"', 'cloud.auth: "elastic:"'], - textPost: 'Where `` is the password of the `elastic` user.', - }, - }, + title: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.windowsTitle', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.windowsTextPre', { + defaultMessage: 'Modify {path} to set the connection information for Elastic Cloud:', + values: { + path: '`C:\\Program Files\\Metricbeat\\metricbeat.yml`', + }, + }), + commands: [ + 'cloud.id: "{config.cloud.id}"', + 'cloud.auth: "elastic:"' + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatCloudInstructions.config.windowsTextPost', { + defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.', + values: { passwordTemplate: '``' }, + }), + } + } }); export function metricbeatEnableInstructions(moduleName) { return { OSX: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the installation directory, run:', - commands: ['./metricbeat modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.osxTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.osxTextPre', { + defaultMessage: 'From the installation directory, run:', + }), + commands: [ + './metricbeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.osxTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, DEB: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo metricbeat modules enable ' + moduleName], - textPost: - 'Modify the settings in the `/etc/metricbeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.debTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo metricbeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.debTextPost', { + defaultMessage: 'Modify the settings in the `/etc/metricbeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, RPM: { - title: 'Enable and configure the ' + moduleName + ' module', - commands: ['sudo metricbeat modules enable ' + moduleName], - textPost: - 'Modify the settings in the `/etc/metricbeat/modules.d/' + moduleName + '.yml` file.', + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.rpmTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + commands: [ + 'sudo metricbeat modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.rpmTextPost', { + defaultMessage: 'Modify the settings in the `/etc/metricbeat/modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), }, WINDOWS: { - title: 'Enable and configure the ' + moduleName + ' module', - textPre: 'From the `C:\\Program Files\\Metricbeat` folder, run:', - commands: ['PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable ' + moduleName], - textPost: 'Modify the settings in the `modules.d/' + moduleName + '.yml` file.', - }, + title: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.windowsTitle', { + defaultMessage: 'Enable and configure the {moduleName} module', + values: { moduleName }, + }), + textPre: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.windowsTextPre', { + defaultMessage: 'From the {path} folder, run:', + values: { path: `C:\\Program Files\\Metricbeat` }, + }), + commands: [ + 'PS C:\\Program Files\\Metricbeat> metricbeat.exe modules enable ' + moduleName, + ], + textPost: i18n.translate('kbn.common.tutorials.metricbeatEnableInstructions.windowsTextPost', { + defaultMessage: 'Modify the settings in the `modules.d/{moduleName}.yml` file.', + values: { moduleName }, + }), + } }; } export function metricbeatStatusCheck(moduleName) { return { - title: 'Module status', - text: 'Check that data is received from the Metricbeat `' + moduleName + '` module', - btnLabel: 'Check data', - success: 'Data successfully received from this module', - error: 'No data has been received from this module yet', + title: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.title', { + defaultMessage: 'Module status', + }), + text: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.text', { + defaultMessage: 'Check that data is received from the Metricbeat `{moduleName}` module', + values: { moduleName }, + }), + btnLabel: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.buttonLabel', { + defaultMessage: 'Check data', + }), + success: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.successText', { + defaultMessage: 'Data successfully received from this module', + }), + error: i18n.translate('kbn.common.tutorials.metricbeatStatusCheck.errorText', { + defaultMessage: 'No data has been received from this module yet', + }), esHitsCheck: { index: 'metricbeat-*', query: { @@ -264,7 +450,9 @@ export function onPremInstructions(moduleName) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.metricbeat.premInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, @@ -317,7 +505,9 @@ export function onPremCloudInstructions(moduleName) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.metricbeat.premCloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, @@ -377,7 +567,9 @@ export function cloudInstructions(moduleName) { return { instructionSets: [ { - title: 'Getting Started', + title: i18n.translate('kbn.common.tutorials.metricbeat.cloudInstructions.gettingStarted.title', { + defaultMessage: 'Getting Started', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, diff --git a/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js index eec1848c06a93..088038ab5beb1 100644 --- a/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js +++ b/src/core_plugins/kibana/common/tutorials/onprem_cloud_instructions.js @@ -17,24 +17,39 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export const createTrycloudOption1 = () => ({ - title: 'Option 1: Try module in Elastic Cloud', - textPre: - 'Go to [Elastic Cloud](https://www.elastic.co/cloud/as-a-service/signup?blade=kib). Register if you ' + - 'do not already have an account. Free 14-day trial available.\n\n' + - 'Log into the Elastic Cloud console\n\n' + - 'To create a cluster, in Elastic Cloud console:\n' + - ' 1. Select **Create Deployment** and specify the **Deployment Name**\n' + - ' 2. Modify the other deployment options as needed (or not, the defaults are great to get started)\n' + - ' 3. Click **Create Deployment**\n' + - ' 4. Wait until deployment creation completes\n' + - ' 5. Go to the new Cloud Kibana instance and follow the Kibana Home instructions', + title: i18n.translate('kbn.common.tutorials.premCloudInstructions.option1.title', { + defaultMessage: 'Option 1: Try module in Elastic Cloud', + }), + textPre: i18n.translate('kbn.common.tutorials.premCloudInstructions.option1.textPre', { + defaultMessage: 'Go to [Elastic Cloud]({link}). Register if you \ +do not already have an account. Free 14-day trial available.\n\n\ +Log into the Elastic Cloud console\n\n\ +To create a cluster, in Elastic Cloud console:\n\ + 1. Select **Create Deployment** and specify the **Deployment Name**\n\ + 2. Modify the other deployment options as needed (or not, the defaults are great to get started)\n\ + 3. Click **Create Deployment**\n\ + 4. Wait until deployment creation completes\n\ + 5. Go to the new Cloud Kibana instance and follow the Kibana Home instructions', + values: { + link: 'https://www.elastic.co/cloud/as-a-service/signup?blade=kib', + } + }), }); export const createTrycloudOption2 = () => ({ - title: 'Option 2: Connect local Kibana to a Cloud instance', - textPre: - 'If you are running this Kibana instance against a hosted Elasticsearch instance,' + - ' proceed with manual setup.\n\n' + - 'Save the **Elasticsearch** endpoint as `` and the cluster **Password** as `` for your records', + title: i18n.translate('kbn.common.tutorials.premCloudInstructions.option2.title', { + defaultMessage: 'Option 2: Connect local Kibana to a Cloud instance', + }), + textPre: i18n.translate('kbn.common.tutorials.premCloudInstructions.option2.textPre', { + defaultMessage: 'If you are running this Kibana instance against a hosted Elasticsearch instance, \ +proceed with manual setup.\n\n\ +Save the **Elasticsearch** endpoint as {urlTemplate} and the cluster **Password** as {passwordTemplate} for your records', + values: { + urlTemplate: '``', + passwordTemplate: '``', + } + }), }); From e6ebcf2506ba6adcc8f7c9349417689a4dc813f2 Mon Sep 17 00:00:00 2001 From: pavel06081991 Date: Thu, 6 Sep 2018 13:59:43 +0300 Subject: [PATCH 26/68] translate tutorials(apm) (#22217) * translate tutorials(apm) * remove tabs from the line beginning * remove id duplicate * Remove disabling no-multi-str rule * Move command to the values --- .../tutorials/apm/apm_client_instructions.js | 394 ++++++++++++------ .../tutorials/apm/apm_server_instructions.js | 69 ++- .../server/tutorials/apm/elastic_cloud.js | 15 +- .../kibana/server/tutorials/apm/index.js | 36 +- .../kibana/server/tutorials/apm/on_prem.js | 49 ++- 5 files changed, 393 insertions(+), 170 deletions(-) diff --git a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js index 183cf26a6679e..138bebdc57998 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js @@ -17,123 +17,198 @@ * under the License. */ -/* eslint-disable max-len */ +import { i18n } from '@kbn/i18n'; export const createNodeClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent for Node.js as a dependency to your application.', + title: i18n.translate('kbn.server.tutorials.apm.nodeClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.nodeClient.install.textPre', { + defaultMessage: 'Install the APM agent for Node.js as a dependency to your application.', + }), commands: ['npm install elastic-apm-node --save'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the `serviceName`.' + - ' This agent supports a vararity of frameworks but can also be used with your custom stack.', - commands: `// Add this to the VERY top of the first file loaded in your app + title: i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `serviceName`. \ +This agent supports a vararity of frameworks but can also be used with your custom stack.', + }), + commands: `// ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.addThisToTheFileTopComment', { + defaultMessage: 'Add this to the VERY top of the first file loaded in your app', + })} var apm = require('elastic-apm-node').start({curlyOpen} - // Override service name from package.json - // Allowed characters: a-z, A-Z, 0-9, -, _, and space + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Override service name from package.json', + })} + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.allowedCharactersComment', { + defaultMessage: 'Allowed characters: a-z, A-Z, 0-9, -, _, and space', + })} serviceName: '', - // Use if APM Server requires a token + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.useIfApmRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} secretToken: '', - // Set custom APM Server URL (default: http://localhost:8200) + // ${i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} serverUrl: '' {curlyClose})`.split('\n'), - textPost: `See [the documentation]({config.docs.base_url}guide/en/apm/agent/nodejs/1.x/index.html) for advanced usage, including how to use with [Babel/ES Modules]({config.docs.base_url}guide/en/apm/agent/nodejs/1.x/advanced-setup.html#es-modules).`, + textPost: i18n.translate('kbn.server.tutorials.apm.nodeClient.configure.textPost', { + defaultMessage: 'See [the documentation]({documentationLink}) for advanced usage, including how to use with \ +[Babel/ES Modules]({babelEsModulesLink}).', + values: { + documentationLink: '{config.docs.base_url}guide/en/apm/agent/nodejs/1.x/index.html', + babelEsModulesLink: '{config.docs.base_url}guide/en/apm/agent/nodejs/1.x/advanced-setup.html#es-modules', + }, + }), }, ]; export const createDjangoClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent for Python as a dependency.', + title: i18n.translate('kbn.server.tutorials.apm.djangoClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.djangoClient.install.textPre', { + defaultMessage: 'Install the APM agent for Python as a dependency.', + }), commands: ['$ pip install elastic-apm'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the `SERVICE_NAME`.', - commands: `# Add the agent to the installed apps + title: i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `SERVICE_NAME`.', + }), + commands: `# ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.addAgentComment', { + defaultMessage: 'Add the agent to the installed apps', + })} INSTALLED_APPS = ( 'elasticapm.contrib.django', # ... ) ELASTIC_APM = {curlyOpen} - # Set required service name. Allowed characters: - # a-z, A-Z, 0-9, -, _, and space + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Set required service name. Allowed characters:', + })} + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.allowedCharactersComment', { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + })} 'SERVICE_NAME': '', - # Use if APM Server requires a token + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} 'SECRET_TOKEN': '', - # Set custom APM Server URL (default: http://localhost:8200) + # ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} 'SERVER_URL': '', {curlyClose} -# To send performance metrics, add our tracing middleware: +# ${i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.commands.addTracingMiddlewareComment', { + defaultMessage: 'To send performance metrics, add our tracing middleware:', + })} MIDDLEWARE = ( 'elasticapm.contrib.django.middleware.TracingMiddleware', #... )`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/python/2.x/django-support.html) for advanced usage.', + textPost: i18n.translate('kbn.server.tutorials.apm.djangoClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/python/2.x/django-support.html' }, + }), }, ]; export const createFlaskClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent for Python as a dependency.', + title: i18n.translate('kbn.server.tutorials.apm.flaskClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.flaskClient.install.textPre', { + defaultMessage: 'Install the APM agent for Python as a dependency.', + }), commands: ['$ pip install elastic-apm[flask]'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the `SERVICE_NAME`.', - commands: `# initialize using environment variables + title: i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the `SERVICE_NAME`.', + }), + commands: `# ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment', { + defaultMessage: 'initialize using environment variables', + })} from elasticapm.contrib.flask import ElasticAPM app = Flask(__name__) apm = ElasticAPM(app) -# or configure to use ELASTIC_APM in your application's settings +# ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.configureElasticApmComment', { + defaultMessage: 'or configure to use ELASTIC_APM in your application\'s settings', + })} from elasticapm.contrib.flask import ElasticAPM app.config['ELASTIC_APM'] = {curlyOpen} - # Set required service name. Allowed characters: - # a-z, A-Z, 0-9, -, _, and space + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Set required service name. Allowed characters:', + })} + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.allowedCharactersComment', { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + })} 'SERVICE_NAME': '', - # Use if APM Server requires a token + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} 'SECRET_TOKEN': '', - # Set custom APM Server URL (default: http://localhost:8200) + # ${i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} 'SERVER_URL': '', {curlyClose} apm = ElasticAPM(app)`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/python/2.x/flask-support.html) for advanced usage.', + textPost: i18n.translate('kbn.server.tutorials.apm.flaskClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/python/2.x/flask-support.html' }, + }), }, ]; export const createRailsClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Add the agent to your Gemfile.', + title: i18n.translate('kbn.server.tutorials.apm.railsClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.railsClient.install.textPre', { + defaultMessage: 'Add the agent to your Gemfile.', + }), commands: [`gem 'elastic-apm'`], }, { - title: 'Configure the agent', - textPre: - 'APM is automatically started when your app boots. Configure the agent, by creating the config file `config/elastic_apm.yml`', + title: i18n.translate('kbn.server.tutorials.apm.railsClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.railsClient.configure.textPre', { + defaultMessage: 'APM is automatically started when your app boots. Configure the agent, by creating the config file {configFile}', + values: { configFile: '`config/elastic_apm.yml`' }, + }), commands: `# config/elastic_apm.yml: # Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space @@ -145,22 +220,30 @@ export const createRailsClientInstructions = () => [ # Set custom APM Server URL (default: http://localhost:8200) # server_url: 'http://localhost:8200'`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html) for configuration options and advanced usage.\n\n', + textPost: i18n.translate('kbn.server.tutorials.apm.railsClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html' }, + }), }, ]; export const createRackClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Add the agent to your Gemfile.', + title: i18n.translate('kbn.server.tutorials.apm.rackClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.rackClient.install.textPre', { + defaultMessage: 'Add the agent to your Gemfile.', + }), commands: [`gem 'elastic-apm'`], }, { - title: 'Configure the agent', - textPre: - 'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.', + title: i18n.translate('kbn.server.tutorials.apm.rackClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.rackClient.configure.textPre', { + defaultMessage: 'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.', + }), commands: `# config.ru require 'sinatra/base' @@ -171,8 +254,12 @@ export const createRackClientInstructions = () => [ end ElasticAPM.start( - app: MySinatraApp, # required - config_file: '' # optional, defaults to config/elastic_apm.yml + app: MySinatraApp, # ${i18n.translate('kbn.server.tutorials.apm.rackClient.configure.commands.requiredComment', { + defaultMessage: 'required', + })} + config_file: '' # ${i18n.translate('kbn.server.tutorials.apm.rackClient.configure.commands.optionalComment', { + defaultMessage: 'optional, defaults to config/elastic_apm.yml', + })} ) run MySinatraApp @@ -180,90 +267,146 @@ export const createRackClientInstructions = () => [ at_exit {curlyOpen} ElasticAPM.stop {curlyClose}`.split('\n'), }, { - title: 'Create config file', - textPre: 'Create a config file `config/elastic_apm.yml`:', + title: i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.title', { + defaultMessage: 'Create config file', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.textPre', { + defaultMessage: 'Create a config file {configFile}:', + values: { configFile: '`config/elastic_apm.yml`' }, + }), commands: `# config/elastic_apm.yml: -# Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space -# Defaults to the name of your Rack app's class. +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.setServiceNameComment', { + defaultMessage: 'Set service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', + })} +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', { + defaultMessage: 'Defaults to the name of your Rack app\'s class.', + })} # service_name: 'my-service' -# Use if APM Server requires a token +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', { + defaultMessage: 'Use if APM Server requires a token', + })} # secret_token: '' -# Set custom APM Server URL (default: http://localhost:8200) +# ${i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.commands.setCustomApmServerComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultServerUrl})', + values: { defaultServerUrl: 'http://localhost:8200' }, + })} # server_url: 'http://localhost:8200'`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html) for configuration options and advanced usage.\n\n', + textPost: i18n.translate('kbn.server.tutorials.apm.rackClient.createConfig.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/ruby/1.x/index.html' }, + }), }, ]; export const createJsClientInstructions = () => [ { - title: 'Enable Real User Monitoring support in the APM server', - textPre: - 'Please refer to [the documentation]({config.docs.base_url}guide/en/apm/server/{config.docs.version}/rum.html).', + title: i18n.translate('kbn.server.tutorials.apm.jsClient.enableRealUserMonitoring.title', { + defaultMessage: 'Enable Real User Monitoring support in the APM server', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.jsClient.enableRealUserMonitoring.textPre', { + defaultMessage: 'Please refer to [the documentation]({documentationLink}).', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/server/{config.docs.version}/rum.html' }, + }), }, { - title: 'Install the APM agent', - textPre: 'Install the APM agent for JavaScript as a dependency to your application:', + title: i18n.translate('kbn.server.tutorials.apm.jsClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.jsClient.install.textPre', { + defaultMessage: 'Install the APM agent for JavaScript as a dependency to your application:', + }), commands: [`npm install elastic-apm-js-base --save`], }, { - title: 'Configure the agent', - textPre: 'Agents are libraries that run inside of your application.', + title: i18n.translate('kbn.server.tutorials.apm.jsClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.jsClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application.', + }), commands: `import {curlyOpen} init as initApm {curlyClose} from 'elastic-apm-js-base' var apm = initApm({curlyOpen} - // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + // ${i18n.translate('kbn.server.tutorials.apm.jsClient.configure.commands.setRequiredServiceNameComment', { + defaultMessage: 'Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)', + })} serviceName: '', - // Set custom APM Server URL (default: http://localhost:8200) + // ${i18n.translate('kbn.server.tutorials.apm.jsClient.configure.commands.setCustomApmServerUrlComment', { + defaultMessage: 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + })} serverUrl: '', - // Set service version (required for sourcemap feature) + // ${i18n.translate('kbn.server.tutorials.apm.jsClient.configure.commands.setServiceVersionComment', { + defaultMessage: 'Set service version (required for sourcemap feature)', + })} serviceVersion: '' {curlyClose})`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/js-base/current/index.html) for advanced usage.', + textPost: i18n.translate('kbn.server.tutorials.apm.jsClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for advanced usage.', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/js-base/current/index.html' }, + }), }, ]; export const createGoClientInstructions = () => [ { - title: 'Install the APM agent', - textPre: 'Install the APM agent packages for Go.', + title: i18n.translate('kbn.server.tutorials.apm.goClient.install.title', { + defaultMessage: 'Install the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.goClient.install.textPre', { + defaultMessage: 'Install the APM agent packages for Go.', + }), commands: ['go get github.com/elastic/apm-agent-go'], }, { - title: 'Configure the agent', - textPre: - 'Agents are libraries that run inside of your application process.' + - ' APM services are created programmatically based on the executable ' + - ' file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', - commands: `# Initialize using environment variables: - -# Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space. -# If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used. + title: i18n.translate('kbn.server.tutorials.apm.goClient.configure.title', { + defaultMessage: 'Configure the agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.goClient.configure.textPre', { + defaultMessage: 'Agents are libraries that run inside of your application process. \ +APM services are created programmatically based on the executable \ +file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', + }), + commands: `# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', { + defaultMessage: 'Initialize using environment variables:', + })} + +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.setServiceNameComment', { + defaultMessage: 'Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space.', + })} +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.usedExecutableNameComment', { + defaultMessage: 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', + })} export ELASTIC_APM_SERVICE_NAME= -# Set the APM Server URL. If unspecified, the agent will effectively be disabled. +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.setAmpServerUrlComment', { + defaultMessage: 'Set the APM Server URL. If unspecified, the agent will effectively be disabled.', + })} export ELASTIC_APM_SERVER_URL= -# Set if APM Server requires a token. +# ${i18n.translate('kbn.server.tutorials.apm.goClient.configure.commands.setIfAmpServerRequiresTokenComment', { + defaultMessage: 'Set if APM Server requires a token.', + })} export ELASTIC_APM_SECRET_TOKEN= `.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/go/current/configuration.html) for advanced configuration.', + textPost: i18n.translate('kbn.server.tutorials.apm.goClient.configure.textPost', { + defaultMessage: 'See the [documentation]({documenationLink}) for advanced configuration.', + values: { documenationLink: '{config.docs.base_url}guide/en/apm/agent/go/current/configuration.html' }, + }), }, { - title: 'Instrument your application', - textPre: - 'Instrument your Go application by using one of the provided instrumentation modules or ' + - 'by using the tracer API directly.', + title: i18n.translate('kbn.server.tutorials.apm.goClient.instrument.title', { + defaultMessage: 'Instrument your application', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.goClient.instrument.textPre', { + defaultMessage: 'Instrument your Go application by using one of the provided instrumentation modules or \ +by using the tracer API directly.', + }), commands: ` import ( "net/http" @@ -277,37 +420,46 @@ func main() {curlyOpen} http.ListenAndServe(":8080", apmhttp.Wrap(mux)) {curlyClose} `.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/go/current/instrumenting-source.html) for a detailed ' + - 'guide to instrumenting Go source code.\n\n' + - '**Warning: The Go agent is currently in Beta and not meant for production use.**', + textPost: i18n.translate('kbn.server.tutorials.apm.goClient.instrument.textPost', { + defaultMessage: 'See the [documentation]({documentationLink}) for a detailed \ +guide to instrumenting Go source code.\n\n\ +**Warning: The Go agent is currently in Beta and not meant for production use.**', + values: { documentationLink: '{config.docs.base_url}guide/en/apm/agent/go/current/instrumenting-source.html' }, + }), }, ]; export const createJavaClientInstructions = () => [ { - title: 'Download the APM agent', - textPre: - 'Download the agent jar from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Ca%3Aelastic-apm-agent). ' + - 'Do **not** add the agent as a dependency to your application.', + title: i18n.translate('kbn.server.tutorials.apm.javaClient.download.title', { + defaultMessage: 'Download the APM agent', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.javaClient.download.textPre', { + defaultMessage: 'Download the agent jar from [Maven Central]({mavenCentralLink}). \ +Do **not** add the agent as a dependency to your application.', + values: { mavenCentralLink: 'http://search.maven.org/#search%7Cga%7C1%7Ca%3Aelastic-apm-agent' }, + }), }, { - title: 'Start your application with the javaagent flag', - textPre: - 'Add the `-javaagent` flag and configure the agent with system properties.\n' + - '\n' + - ' * Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n' + - ' * Set custom APM Server URL (default: http://localhost:8200)\n' + - ' * Set the base package of your application', + title: i18n.translate('kbn.server.tutorials.apm.javaClient.startApplication.title', { + defaultMessage: 'Start your application with the javaagent flag', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.javaClient.startApplication.textPre', { + defaultMessage: 'Add the `-javaagent` flag and configure the agent with system properties.\n\n \ +* Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \ +* Set custom APM Server URL (default: {customApmServerUrl})\n \ +* Set the base package of your application', + values: { customApmServerUrl: 'http://localhost:8200' }, + }), commands: `java -javaagent:/path/to/elastic-apm-agent-.jar \\ -Delastic.apm.service_name=my-application \\ -Delastic.apm.server_url=http://localhost:8200 \\ -Delastic.apm.application_packages=org.example \\ -jar my-application.jar`.split('\n'), - textPost: - 'See the [documentation]' + - '({config.docs.base_url}guide/en/apm/agent/java/current/index.html) for configuration options and advanced usage.\n\n' + - '**Warning: The Java agent is currently in Beta and not meant for production use.**', + textPost: i18n.translate('kbn.server.tutorials.apm.javaClient.startApplication.textPost', { + defaultMessage: 'See the [documentation]({documenationLink}) for configuration options and advanced \ +usage.\n\n**Warning: The Java agent is currently in Beta and not meant for production use.**', + values: { documenationLink: '{config.docs.base_url}guide/en/apm/agent/java/current/index.html' }, + }), }, ]; diff --git a/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js b/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js index 59d4d5fe75c1b..2826aca5194db 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/apm/apm_server_instructions.js @@ -17,11 +17,16 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; + export const createEditConfig = () => ({ - title: 'Edit the configuration', - textPre: - `If you're using an X-Pack secured version of Elastic Stack, you must specify` + - ' credentials in the `apm-server.yml` config file.', + title: i18n.translate('kbn.server.tutorials.apm.editConfig.title', { + defaultMessage: 'Edit the configuration', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.editConfig.textPre', { + defaultMessage: 'If you\'re using an X-Pack secured version of Elastic Stack, you must specify \ +credentials in the `apm-server.yml` config file.', + }), commands: [ 'output.elasticsearch:', ' hosts: [""]', @@ -31,8 +36,12 @@ export const createEditConfig = () => ({ }); const createStartServer = () => ({ - title: 'Start APM Server', - textPre: 'The server processes and stores application performance metrics in Elasticsearch.', + title: i18n.translate('kbn.server.tutorials.apm.startServer.title', { + defaultMessage: 'Start APM Server', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.startServer.textPre', { + defaultMessage: 'The server processes and stores application performance metrics in Elasticsearch.', + }), }); export function createStartServerUnix() { @@ -45,7 +54,9 @@ export function createStartServerUnix() { }; } -const createDownloadServerTitle = () => 'Download and unpack APM Server'; +const createDownloadServerTitle = () => i18n.translate('kbn.server.tutorials.apm.downloadServer.title', { + defaultMessage: 'Download and unpack APM Server', +}); export const createDownloadServerOsx = () => ({ title: createDownloadServerTitle(), @@ -62,8 +73,10 @@ export const createDownloadServerDeb = () => ({ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-amd64.deb', 'sudo dpkg -i apm-server-{config.kibana.version}-amd64.deb', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page]({config.docs.base_url}downloads/apm/apm-server).', + textPost: i18n.translate('kbn.server.tutorials.apm.downloadServerTitle', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', + values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server' }, + }), }); export const createDownloadServerRpm = () => ({ @@ -72,8 +85,10 @@ export const createDownloadServerRpm = () => ({ 'curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-{config.kibana.version}-x86_64.rpm', 'sudo rpm -vi apm-server-{config.kibana.version}-x86_64.rpm', ], - textPost: - 'Looking for the 32-bit packages? See the [Download page]({config.docs.base_url}downloads/apm/apm-server).', + textPost: i18n.translate('kbn.server.tutorials.apm.downloadServerRpm', { + defaultMessage: 'Looking for the 32-bit packages? See the [Download page]({downloadPageLink}).', + values: { downloadPageLink: '{config.docs.base_url}downloads/apm/apm-server' }, + }), }); export function createWindowsServerInstructions() { @@ -82,20 +97,32 @@ export function createWindowsServerInstructions() { return [ { title: createDownloadServerTitle(), - textPre: - '1. Download the APM Server Windows zip file from the [Download page](https://www.elastic.co/downloads/apm/apm-server).\n' + - '2. Extract the contents of the zip file into `C:\\Program Files`.\n' + - '3. Rename the `apm-server-{config.kibana.version}-windows` directory to `APM-Server`.\n' + - '4. Open a PowerShell prompt as an Administrator (right-click the PowerShell icon and select' + - ' **Run As Administrator**). If you are running Windows XP, you might need to download and install PowerShell.\n' + - '5. From the PowerShell prompt, run the following commands to install APM Server as a Windows service:', + textPre: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPre', { + defaultMessage: '1. Download the APM Server Windows zip file from the \ +[Download page]({downloadPageLink}).\n2. Extract the contents of \ +the zip file into {zipFileExtractFolder}.\n3. Rename the {apmServerDirectory} \ +directory to `APM-Server`.\n4. Open a PowerShell prompt as an Administrator \ +(right-click the PowerShell icon and select \ +**Run As Administrator**). If you are running Windows XP, you might need to download and install \ +PowerShell.\n5. From the PowerShell prompt, run the following commands to install APM Server as a Windows service:', + values: { + downloadPageLink: 'https://www.elastic.co/downloads/apm/apm-server', + zipFileExtractFolder: '`C:\\Program Files`', + apmServerDirectory: '`apm-server-{config.kibana.version}-windows`', + } + }), commands: [ `PS > cd 'C:\\Program Files\\APM-Server'`, `PS C:\\Program Files\\APM-Server> .\\install-service-apm-server.ps1`, ], - textPost: - 'Note: If script execution is disabled on your system, you need to set the execution policy for the current session' + - ' to allow the script to run. For example: `PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`.', + textPost: i18n.translate('kbn.server.tutorials.apm.windowsServerInstructions.textPost', { + defaultMessage: 'Note: If script execution is disabled on your system, \ +you need to set the execution policy for the current session \ +to allow the script to run. For example: {command}.', + values: { + command: '`PowerShell.exe -ExecutionPolicy UnRestricted -File .\\install-service-apm-server.ps1`' + } + }), }, createEditConfig(), { diff --git a/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js b/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js index 4bce847b082e7..0a18d06d3fc81 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js +++ b/src/core_plugins/kibana/server/tutorials/apm/elastic_cloud.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; import { @@ -31,9 +32,13 @@ import { } from './apm_client_instructions'; const createServerUrlInstruction = () => ({ - title: 'APM Server endpoint', - textPre: `Retrieve the APM Server URL from the Deployments section on the Elastic Cloud dashboard. - You will also need the APM Server secret token, which was generated on deployment.`, + title: i18n.translate('kbn.server.tutorials.apm.serverUrlInstruction.title', { + defaultMessage: 'APM Server endpoint', + }), + textPre: i18n.translate('kbn.server.tutorials.apm.serverUrlInstruction.textPre', { + defaultMessage: 'Retrieve the APM Server URL from the Deployments section on the Elastic Cloud dashboard. \ +You will also need the APM Server secret token, which was generated on deployment.', + }), }); export function createElasticCloudInstructions() { @@ -42,7 +47,9 @@ export function createElasticCloudInstructions() { return { instructionSets: [ { - title: 'APM Agents', + title: i18n.translate('kbn.server.tutorials.apm.elasticCloudInstructions.title', { + defaultMessage: 'APM Agents', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.NODE, diff --git a/src/core_plugins/kibana/server/tutorials/apm/index.js b/src/core_plugins/kibana/server/tutorials/apm/index.js index fb92e9acb9609..512805a4231dd 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/index.js +++ b/src/core_plugins/kibana/server/tutorials/apm/index.js @@ -17,12 +17,15 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { TUTORIAL_CATEGORY } from '../../../common/tutorials/tutorial_category'; import { onPremInstructions } from './on_prem'; import { createElasticCloudInstructions } from './elastic_cloud'; import { getSavedObjects } from './saved_objects/get_saved_objects'; -const apmIntro = 'Collect in-depth performance metrics and errors from inside your applications.'; +const apmIntro = i18n.translate('kbn.server.tutorials.apm.introduction', { + defaultMessage: 'Collect in-depth performance metrics and errors from inside your applications.', +}); function isEnabled(config) { const ENABLED_KEY = 'xpack.apm.ui.enabled'; @@ -41,7 +44,9 @@ export function apmSpecProvider(server) { dashboards: [ { id: '8d3ed660-7828-11e7-8c47-65b845b5cfb3', - linkLabel: 'APM dashboard', + linkLabel: i18n.translate('kbn.server.tutorials.apm.specProvider.artifacts.dashboards.linkLabel', { + defaultMessage: 'APM dashboard', + }), isOverview: true, }, ], @@ -50,28 +55,35 @@ export function apmSpecProvider(server) { if (isEnabled(config)) { artifacts.application = { path: '/app/apm', - label: 'Launch APM', + label: i18n.translate('kbn.server.tutorials.apm.specProvider.artifacts.application.label', { + defaultMessage: 'Launch APM', + }), }; } return { id: 'apm', - name: 'APM', + name: i18n.translate('kbn.server.tutorials.apm.specProvider.name', { + defaultMessage: 'APM', + }), category: TUTORIAL_CATEGORY.OTHER, shortDescription: apmIntro, - longDescription: - 'Application Performance Monitoring (APM) collects in-depth' + - ' performance metrics and errors from inside your application.' + - ' It allows you to monitor the performance of thousands of applications in real time.' + - ' [Learn more]({config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html).', + longDescription: i18n.translate('kbn.server.tutorials.apm.specProvider.longDescription', { + defaultMessage: 'Application Performance Monitoring (APM) collects in-depth \ +performance metrics and errors from inside your application. \ +It allows you to monitor the performance of thousands of applications in real time. \ +[Learn more]({learnMoreLink}).', + values: { learnMoreLink: '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html' }, + }), euiIconType: 'apmApp', artifacts: artifacts, onPrem: onPremInstructions(apmIndexPattern), elasticCloud: createElasticCloudInstructions(), previewImagePath: '/plugins/kibana/home/tutorial_resources/apm/apm.png', savedObjects: getSavedObjects(apmIndexPattern), - savedObjectsInstallMsg: - 'Load index pattern, visualizations, and pre-defined dashboards.' + - ' An index pattern is required for some features in the APM UI.', + savedObjectsInstallMsg: i18n.translate('kbn.server.tutorials.apm.specProvider.savedObjectsInstallMsg', { + defaultMessage: 'Load index pattern, visualizations, and pre-defined dashboards. \ +An index pattern is required for some features in the APM UI.', + }), }; } diff --git a/src/core_plugins/kibana/server/tutorials/apm/on_prem.js b/src/core_plugins/kibana/server/tutorials/apm/on_prem.js index b1a3938592e73..a75ce27384b21 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/on_prem.js +++ b/src/core_plugins/kibana/server/tutorials/apm/on_prem.js @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { INSTRUCTION_VARIANT } from '../../../common/tutorials/instruction_variant'; import { createWindowsServerInstructions, @@ -44,7 +45,9 @@ export function onPremInstructions(apmIndexPattern) { return { instructionSets: [ { - title: 'APM Server', + title: i18n.translate('kbn.server.tutorials.apm.apmServer.title', { + defaultMessage: 'APM Server', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.OSX, @@ -64,11 +67,21 @@ export function onPremInstructions(apmIndexPattern) { }, ], statusCheck: { - title: 'APM Server status', - text: 'Make sure APM Server is running before you start implementing the APM agents.', - btnLabel: 'Check APM Server status', - success: 'You have correctly setup APM-Server', - error: 'APM-Server has still not connected to Elasticsearch', + title: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.title', { + defaultMessage: 'APM Server status', + }), + text: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.text', { + defaultMessage: 'Make sure APM Server is running before you start implementing the APM agents.', + }), + btnLabel: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.btnLabel', { + defaultMessage: 'Check APM Server status', + }), + success: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.successMessage', { + defaultMessage: 'You have correctly setup APM-Server', + }), + error: i18n.translate('kbn.server.tutorials.apm.apmServer.statusCheck.errorMessage', { + defaultMessage: 'APM-Server has still not connected to Elasticsearch', + }), esHitsCheck: { index: apmIndexPattern, query: { @@ -84,7 +97,9 @@ export function onPremInstructions(apmIndexPattern) { }, }, { - title: 'APM Agents', + title: i18n.translate('kbn.server.tutorials.apm.apmAgents.title', { + defaultMessage: 'APM Agents', + }), instructionVariants: [ { id: INSTRUCTION_VARIANT.NODE, @@ -120,11 +135,21 @@ export function onPremInstructions(apmIndexPattern) { }, ], statusCheck: { - title: 'Agent status', - text: 'Make sure your application is running and the agents are sending data.', - btnLabel: 'Check agent status', - success: 'Data successfully received from one or more agents', - error: `No data has been received from agents yet`, + title: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.title', { + defaultMessage: 'Agent status', + }), + text: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.text', { + defaultMessage: 'Make sure your application is running and the agents are sending data.', + }), + btnLabel: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.btnLabel', { + defaultMessage: 'Check agent status', + }), + success: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.successMessage', { + defaultMessage: 'Data successfully received from one or more agents', + }), + error: i18n.translate('kbn.server.tutorials.apm.apmAgents.statusCheck.errorMessage', { + defaultMessage: 'No data has been received from agents yet', + }), esHitsCheck: { index: apmIndexPattern, query: { From a2265211184235de329dbde065a9f0728b6fa70a Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 08:30:35 -0400 Subject: [PATCH 27/68] [Spaces] - Experimental Public spaces api (#22501) [skip ci] --- docs/api/spaces-management/delete.asciidoc | 25 ++ docs/api/spaces-management/get.asciidoc | 77 +++++ docs/api/spaces-management/post.asciidoc | 50 ++++ docs/api/spaces-management/put.asciidoc | 50 ++++ docs/api/spaces.asciidoc | 17 ++ .../spaces/common/is_reserved_space.ts | 2 +- x-pack/plugins/spaces/index.js | 6 +- .../spaces/public/lib/spaces_manager.ts | 6 +- .../server/lib/{errors.js => errors.ts} | 4 +- ..._license.js => route_pre_check_license.ts} | 6 +- .../lib/{space_schema.js => space_schema.ts} | 2 +- ...ces_url_parser.js => spaces_url_parser.ts} | 13 +- .../routes/api/__fixtures__/create_spaces.ts | 29 ++ .../api/__fixtures__/create_test_handler.ts | 142 +++++++++ .../server/routes/api/__fixtures__/index.ts | 15 + .../server/routes/api/public/delete.test.ts | 85 ++++++ .../spaces/server/routes/api/public/delete.ts | 42 +++ .../server/routes/api/public/get.test.ts | 86 ++++++ .../spaces/server/routes/api/public/get.ts | 61 ++++ .../spaces/server/routes/api/public/index.ts | 20 ++ .../server/routes/api/public/post.test.ts | 106 +++++++ .../spaces/server/routes/api/public/post.ts | 46 +++ .../server/routes/api/public/put.test.ts | 97 ++++++ .../spaces/server/routes/api/public/put.ts | 49 +++ .../spaces/server/routes/api/v1/index.ts | 13 + .../spaces/server/routes/api/v1/spaces.js | 202 ------------- .../server/routes/api/v1/spaces.test.js | 281 ------------------ .../server/routes/api/v1/spaces.test.ts | 93 ++++++ .../spaces/server/routes/api/v1/spaces.ts | 45 +++ .../lib/convert_saved_object_to_space.test.ts | 27 ++ .../lib/convert_saved_object_to_space.ts | 20 ++ .../server/routes/lib/get_space_by_id.ts | 19 ++ .../plugins/spaces/server/routes/lib/index.ts | 8 + 33 files changed, 1246 insertions(+), 498 deletions(-) create mode 100644 docs/api/spaces-management/delete.asciidoc create mode 100644 docs/api/spaces-management/get.asciidoc create mode 100644 docs/api/spaces-management/post.asciidoc create mode 100644 docs/api/spaces-management/put.asciidoc create mode 100644 docs/api/spaces.asciidoc rename x-pack/plugins/spaces/server/lib/{errors.js => errors.ts} (85%) rename x-pack/plugins/spaces/server/lib/{route_pre_check_license.js => route_pre_check_license.ts} (80%) rename x-pack/plugins/spaces/server/lib/{space_schema.js => space_schema.ts} (96%) rename x-pack/plugins/spaces/server/lib/{spaces_url_parser.js => spaces_url_parser.ts} (80%) create mode 100644 x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/delete.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/delete.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/get.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/get.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/index.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/post.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/post.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/put.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/public/put.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/v1/index.ts delete mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.js delete mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js create mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/api/v1/spaces.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts create mode 100644 x-pack/plugins/spaces/server/routes/lib/index.ts diff --git a/docs/api/spaces-management/delete.asciidoc b/docs/api/spaces-management/delete.asciidoc new file mode 100644 index 0000000000000..c5ae025dd9e2e --- /dev/null +++ b/docs/api/spaces-management/delete.asciidoc @@ -0,0 +1,25 @@ +[[spaces-api-delete]] +=== Delete space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +[WARNING] +================================================== +Deleting a space will automatically delete all saved objects that belong to that space. This operation cannot be undone! +================================================== + +==== Request + +To delete a space, submit a DELETE request to the `/api/spaces/space/` +endpoint: + +[source,js] +-------------------------------------------------- +DELETE /api/spaces/space/marketing +-------------------------------------------------- +// KIBANA + +==== Response + +If the space is successfully deleted, the response code is `204`; otherwise, the response +code is 404. diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc new file mode 100644 index 0000000000000..c79a883a80e4b --- /dev/null +++ b/docs/api/spaces-management/get.asciidoc @@ -0,0 +1,77 @@ +[[spaces-api-get]] +=== Get Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Retrieves all {kib} spaces, or a specific space. + +==== Get all {kib} spaces + +===== Request + +To retrieve all spaces, issue a GET request to the +/api/spaces/space endpoint. + +[source,js] +-------------------------------------------------- +GET /api/spaces/space +-------------------------------------------------- +// KIBANA + +===== Response + +A successful call returns a response code of `200` and a response body containing a JSON +representation of the spaces. + +[source,js] +-------------------------------------------------- +[ + { + "id": "default", + "name": "Default", + "description" : "This is the Default Space", + "_reserved": true + }, + { + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" + }, + { + "id": "sales", + "name": "Sales", + "initials": "MK" + }, +] +-------------------------------------------------- + +==== Get a specific space + +===== Request + +To retrieve a specific space, issue a GET request to +the `/api/spaces/space/` endpoint: + +[source,js] +-------------------------------------------------- +GET /api/spaces/space/marketing +-------------------------------------------------- +// KIBANA + +===== Response + +A successful call returns a response code of `200` and a response body containing a JSON +representation of the space. + +[source,js] +-------------------------------------------------- +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc new file mode 100644 index 0000000000000..569835c78b2f8 --- /dev/null +++ b/docs/api/spaces-management/post.asciidoc @@ -0,0 +1,50 @@ +[[spaces-api-post]] +=== Create Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Creates a new {kib} space. To update an existing space, use the PUT command. + +==== Request + +To create a space, issue a POST request to the +`/api/spaces/space` endpoint. + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space +-------------------------------------------------- + +==== Request Body + +The following parameters can be specified in the body of a POST request to create a space: + +`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation. + +`name`:: (string) Required display name for the space. + +`description`:: (string) Optional description for the space. + +`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name. +If specified, initials should be either 1 or 2 characters. + +`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name. + +===== Example + +[source,js] +-------------------------------------------------- +POST /api/spaces/space +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- +// KIBANA + +==== Response + +A successful call returns a response code of `200` with the created Space. diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc new file mode 100644 index 0000000000000..529742bf2ce66 --- /dev/null +++ b/docs/api/spaces-management/put.asciidoc @@ -0,0 +1,50 @@ +[[spaces-api-put]] +=== Update Space + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +Updates an existing {kib} space. To create a new space, use the POST command. + +==== Request + +To update a space, issue a PUT request to the +`/api/spaces/space/` endpoint. + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space/ +-------------------------------------------------- + +==== Request Body + +The following parameters can be specified in the body of a PUT request to update a space: + +`id`:: (string) Required identifier for the space. This identifier becomes part of Kibana's URL when inside the space. This cannot be changed by the update operation. + +`name`:: (string) Required display name for the space. + +`description`:: (string) Optional description for the space. + +`initials`:: (string) Optionally specify the initials shown in the Space Avatar for this space. By default, the initials will be automatically generated from the space name. +If specified, initials should be either 1 or 2 characters. + +`color`:: (string) Optioanlly specify the hex color code used in the Space Avatar for this space. By default, the color will be automatically generated from the space name. + +===== Example + +[source,js] +-------------------------------------------------- +PUT /api/spaces/space/marketing +{ + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "initials": "MK" +} +-------------------------------------------------- +// KIBANA + +==== Response + +A successful call returns a response code of `200` with the updated Space. diff --git a/docs/api/spaces.asciidoc b/docs/api/spaces.asciidoc new file mode 100644 index 0000000000000..ea66d50d396b9 --- /dev/null +++ b/docs/api/spaces.asciidoc @@ -0,0 +1,17 @@ +[role="xpack"] +[[spaces-api]] +== Kibana Spaces API + +experimental[This API is *experimental* and may be changed or removed completely in a future release. The underlying Spaces concepts are stable, but the APIs for managing Spaces are currently experimental.] + +The spaces API allows people to manage their spaces within {kib}. + +* <> +* <> +* <> +* <> + +include::spaces-management/put.asciidoc[] +include::spaces-management/post.asciidoc[] +include::spaces-management/get.asciidoc[] +include::spaces-management/delete.asciidoc[] diff --git a/x-pack/plugins/spaces/common/is_reserved_space.ts b/x-pack/plugins/spaces/common/is_reserved_space.ts index 0889686aa77f5..40acd7630b66c 100644 --- a/x-pack/plugins/spaces/common/is_reserved_space.ts +++ b/x-pack/plugins/spaces/common/is_reserved_space.ts @@ -13,6 +13,6 @@ import { Space } from './model/space'; * @param space the space * @returns boolean */ -export function isReservedSpace(space: Space): boolean { +export function isReservedSpace(space: Space | null): boolean { return get(space, '_reserved', false); } diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index f03e8bde19afa..3eafab6461f9e 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -7,7 +7,8 @@ import { resolve } from 'path'; import { validateConfig } from './server/lib/validate_config'; import { checkLicense } from './server/lib/check_license'; -import { initSpacesApi } from './server/routes/api/v1/spaces'; +import { initPublicSpacesApi } from './server/routes/api/public'; +import { initPrivateApis } from './server/routes/api/v1'; import { initSpacesRequestInterceptors } from './server/lib/space_request_interceptors'; import { createDefaultSpace } from './server/lib/create_default_space'; import { createSpacesService } from './server/lib/create_spaces_service'; @@ -93,7 +94,8 @@ export const spaces = (kibana) => new kibana.Plugin({ spacesSavedObjectsClientWrapperFactory(spacesService) ); - initSpacesApi(server); + initPrivateApis(server); + initPublicSpacesApi(server); initSpacesRequestInterceptors(server); diff --git a/x-pack/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/plugins/spaces/public/lib/spaces_manager.ts index a6b21f2fa5229..53835ab122f87 100644 --- a/x-pack/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/lib/spaces_manager.ts @@ -16,12 +16,12 @@ export class SpacesManager extends EventEmitter { constructor(httpAgent: any, chrome: any) { super(); this.httpAgent = httpAgent; - this.baseUrl = chrome.addBasePath(`/api/spaces/v1`); + this.baseUrl = chrome.addBasePath(`/api/spaces`); } public async getSpaces(): Promise { return await this.httpAgent - .get(`${this.baseUrl}/spaces`) + .get(`${this.baseUrl}/space`) .then((response: IHttpResponse) => response.data); } @@ -43,7 +43,7 @@ export class SpacesManager extends EventEmitter { public async changeSelectedSpace(space: Space) { return await this.httpAgent - .post(`${this.baseUrl}/space/${space.id}/select`) + .post(`${this.baseUrl}/v1/space/${space.id}/select`) .then((response: IHttpResponse) => { if (response.data && response.data.location) { window.location = response.data.location; diff --git a/x-pack/plugins/spaces/server/lib/errors.js b/x-pack/plugins/spaces/server/lib/errors.ts similarity index 85% rename from x-pack/plugins/spaces/server/lib/errors.js rename to x-pack/plugins/spaces/server/lib/errors.ts index 6996faeaac8de..4f95c175b0f15 100644 --- a/x-pack/plugins/spaces/server/lib/errors.js +++ b/x-pack/plugins/spaces/server/lib/errors.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +// @ts-ignore import { wrap as wrapBoom } from 'boom'; -export function wrapError(error) { +export function wrapError(error: any) { return wrapBoom(error, error.status); } diff --git a/x-pack/plugins/spaces/server/lib/route_pre_check_license.js b/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts similarity index 80% rename from x-pack/plugins/spaces/server/lib/route_pre_check_license.js rename to x-pack/plugins/spaces/server/lib/route_pre_check_license.ts index 891e9fc4125a9..449836633993c 100644 --- a/x-pack/plugins/spaces/server/lib/route_pre_check_license.js +++ b/x-pack/plugins/spaces/server/lib/route_pre_check_license.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -const Boom = require('boom'); +import Boom from 'boom'; -export function routePreCheckLicense(server) { +export function routePreCheckLicense(server: any) { const xpackMainPlugin = server.plugins.xpack_main; const pluginId = 'spaces'; - return function forbidApiAccess(request, reply) { + return function forbidApiAccess(request: any, reply: any) { const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); if (!licenseCheckResults.showSpaces) { reply(Boom.forbidden(licenseCheckResults.linksMessage)); diff --git a/x-pack/plugins/spaces/server/lib/space_schema.js b/x-pack/plugins/spaces/server/lib/space_schema.ts similarity index 96% rename from x-pack/plugins/spaces/server/lib/space_schema.js rename to x-pack/plugins/spaces/server/lib/space_schema.ts index dc60c10a36bc2..043856235acba 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.js +++ b/x-pack/plugins/spaces/server/lib/space_schema.ts @@ -13,5 +13,5 @@ export const spaceSchema = Joi.object({ description: Joi.string(), initials: Joi.string().max(MAX_SPACE_INITIALS), color: Joi.string().regex(/^#[a-z0-9]{6}$/, `6 digit hex color, starting with a #`), - _reserved: Joi.boolean() + _reserved: Joi.boolean(), }).default(); diff --git a/x-pack/plugins/spaces/server/lib/spaces_url_parser.js b/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts similarity index 80% rename from x-pack/plugins/spaces/server/lib/spaces_url_parser.js rename to x-pack/plugins/spaces/server/lib/spaces_url_parser.ts index 397863785e86c..14113cbf9d807 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_url_parser.js +++ b/x-pack/plugins/spaces/server/lib/spaces_url_parser.ts @@ -5,8 +5,11 @@ */ import { DEFAULT_SPACE_ID } from '../../common/constants'; -export function getSpaceIdFromPath(requestBasePath = '/', serverBasePath = '/') { - let pathToCheck = requestBasePath; +export function getSpaceIdFromPath( + requestBasePath: string = '/', + serverBasePath: string = '/' +): string { + let pathToCheck: string = requestBasePath; if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { pathToCheck = requestBasePath.substr(serverBasePath.length); @@ -28,7 +31,11 @@ export function getSpaceIdFromPath(requestBasePath = '/', serverBasePath = '/') return spaceId; } -export function addSpaceIdToPath(basePath = '/', spaceId = '', requestedPath = '') { +export function addSpaceIdToPath( + basePath: string = '/', + spaceId: string = '', + requestedPath: string = '' +): string { if (requestedPath && !requestedPath.startsWith('/')) { throw new Error(`path must start with a /`); } diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts new file mode 100644 index 0000000000000..85284e3fc3a1c --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_spaces.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function createSpaces() { + return [ + { + id: 'a-space', + attributes: { + name: 'a space', + }, + }, + { + id: 'b-space', + attributes: { + name: 'b space', + }, + }, + { + id: 'default', + attributes: { + name: 'Default Space', + _reserved: true, + }, + }, + ]; +} diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts new file mode 100644 index 0000000000000..a184f21076d4f --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -0,0 +1,142 @@ +/* + * 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. + */ + +// @ts-ignore +import { Server } from 'hapi'; +import { createSpaces } from './create_spaces'; + +export interface TestConfig { + [configKey: string]: any; +} + +export interface TestOptions { + setupFn?: (server: any) => void; + testConfig?: TestConfig; + payload?: any; + preCheckLicenseImpl?: (req: any, reply: any) => any; +} + +export type TeardownFn = () => void; + +export interface RequestRunnerResult { + server: any; + mockSavedObjectsClient: any; + response: any; +} + +export type RequestRunner = ( + method: string, + path: string, + options?: TestOptions +) => Promise; + +export const defaultPreCheckLicenseImpl = (request: any, reply: any) => reply(); + +const baseConfig: TestConfig = { + 'server.basePath': '', +}; + +export function createTestHandler(initApiFn: (server: any, preCheckLicenseImpl: any) => void) { + const teardowns: TeardownFn[] = []; + + const spaces = createSpaces(); + + const request: RequestRunner = async ( + method: string, + path: string, + options: TestOptions = {} + ) => { + const { + setupFn = () => { + return; + }, + testConfig = {}, + payload, + preCheckLicenseImpl = defaultPreCheckLicenseImpl, + } = options; + + let pre = jest.fn(); + if (preCheckLicenseImpl) { + pre = pre.mockImplementation(preCheckLicenseImpl); + } + + const server = new Server(); + + const config = { + ...baseConfig, + ...testConfig, + }; + + server.connection({ port: 0 }); + + await setupFn(server); + + server.decorate( + 'server', + 'config', + jest.fn(() => { + return { + get: (key: string) => config[key], + }; + }) + ); + + initApiFn(server, pre); + + server.decorate('request', 'getBasePath', jest.fn()); + server.decorate('request', 'setBasePath', jest.fn()); + + // Mock server.getSavedObjectsClient() + const mockSavedObjectsClient = { + get: jest.fn((type, id) => { + return spaces.filter(s => s.id === id)[0]; + }), + find: jest.fn(() => { + return { + total: spaces.length, + saved_objects: spaces, + }; + }), + create: jest.fn(() => ({})), + update: jest.fn(() => ({})), + delete: jest.fn(), + errors: { + isNotFoundError: jest.fn(() => true), + }, + }; + + server.decorate('request', 'getSavedObjectsClient', () => mockSavedObjectsClient); + + teardowns.push(() => server.stop()); + + const testRun = async () => { + const response = await server.inject({ + method, + url: path, + payload, + }); + + if (preCheckLicenseImpl) { + expect(pre).toHaveBeenCalled(); + } else { + expect(pre).not.toHaveBeenCalled(); + } + + return response; + }; + + return { + server, + mockSavedObjectsClient, + response: await testRun(), + }; + }; + + return { + request, + teardowns, + }; +} diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts new file mode 100644 index 0000000000000..37fe32c80032e --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createSpaces } from './create_spaces'; +export { + createTestHandler, + TestConfig, + TestOptions, + TeardownFn, + RequestRunner, + RequestRunnerResult, +} from './create_test_handler'; diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts new file mode 100644 index 0000000000000..523b5a2cb2e7b --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); +import Boom from 'boom'; +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initDeleteSpacesApi } from './delete'; + +describe('Spaces Public API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initDeleteSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test(`'DELETE spaces/{id}' deletes the space`, async () => { + const { response } = await request('DELETE', '/api/spaces/space/a-space'); + + const { statusCode } = response; + + expect(statusCode).toEqual(204); + }); + + test(`returns result of routePreCheckLicense`, async () => { + const { response } = await request('DELETE', '/api/spaces/space/a-space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(payload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + + test('DELETE spaces/{id} pretends to delete a non-existent space', async () => { + const { response } = await request('DELETE', '/api/spaces/space/not-a-space'); + + const { statusCode } = response; + + expect(statusCode).toEqual(204); + }); + + test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { + const { response } = await request('DELETE', '/api/spaces/space/default'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(400); + expect(JSON.parse(payload)).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'This Space cannot be deleted because it is reserved.', + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/delete.ts b/x-pack/plugins/spaces/server/routes/api/public/delete.ts new file mode 100644 index 0000000000000..9937b786ccfa7 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/delete.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { isReservedSpace } from '../../../../common/is_reserved_space'; +import { wrapError } from '../../../lib/errors'; +import { getSpaceById } from '../../lib'; + +export function initDeleteSpacesApi(server: any, routePreCheckLicenseFn: any) { + server.route({ + method: 'DELETE', + path: '/api/spaces/space/{id}', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + let result; + + try { + const existingSpace = await getSpaceById(client, id); + if (isReservedSpace(existingSpace)) { + return reply( + wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.')) + ); + } + + result = await client.delete('space', id); + } catch (error) { + return reply(wrapError(error)); + } + + return reply(result).code(204); + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.test.ts b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts new file mode 100644 index 0000000000000..4d04759b283a8 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/get.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); +import Boom from 'boom'; +import { Space } from '../../../../common/model/space'; +import { createSpaces, createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initGetSpacesApi } from './get'; + +describe('GET spaces', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + const spaces = createSpaces(); + + beforeEach(() => { + const setup = createTestHandler(initGetSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test(`'GET spaces' returns all available spaces`, async () => { + const { response } = await request('GET', '/api/spaces/space'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + const resultSpaces: Space[] = JSON.parse(payload); + expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id)); + }); + + test(`returns result of routePreCheckLicense`, async () => { + const { response } = await request('GET', '/api/spaces/space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(payload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + + test(`'GET spaces/{id}' returns the space with that id`, async () => { + const { response } = await request('GET', '/api/spaces/space/default'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + const resultSpace = JSON.parse(payload); + expect(resultSpace.id).toEqual('default'); + }); + + test(`'GET spaces/{id}' returns 404 when retrieving a non-existent space`, async () => { + const { response } = await request('GET', '/api/spaces/space/not-a-space'); + + const { statusCode } = response; + + expect(statusCode).toEqual(404); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/get.ts b/x-pack/plugins/spaces/server/routes/api/public/get.ts new file mode 100644 index 0000000000000..95f1d273a8f6b --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/get.ts @@ -0,0 +1,61 @@ +/* + * 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 Boom from 'boom'; +import { wrapError } from '../../../lib/errors'; +import { convertSavedObjectToSpace } from '../../lib'; + +export function initGetSpacesApi(server: any, routePreCheckLicenseFn: any) { + server.route({ + method: 'GET', + path: '/api/spaces/space', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + let spaces; + + try { + const result = await client.find({ + type: 'space', + sortField: 'name.keyword', + }); + + spaces = result.saved_objects.map(convertSavedObjectToSpace); + } catch (error) { + return reply(wrapError(error)); + } + + return reply(spaces); + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); + + server.route({ + method: 'GET', + path: '/api/spaces/space/{id}', + async handler(request: any, reply: any) { + const spaceId = request.params.id; + + const client = request.getSavedObjectsClient(); + + try { + const response = await client.get('space', spaceId); + + return reply(convertSavedObjectToSpace(response)); + } catch (error) { + if (client.errors.isNotFoundError(error)) { + return reply(Boom.notFound()); + } + return reply(wrapError(error)); + } + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/index.ts b/x-pack/plugins/spaces/server/routes/api/public/index.ts new file mode 100644 index 0000000000000..602b62ab26d06 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { initDeleteSpacesApi } from './delete'; +import { initGetSpacesApi } from './get'; +import { initPostSpacesApi } from './post'; +import { initPutSpacesApi } from './put'; + +export function initPublicSpacesApi(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + + initDeleteSpacesApi(server, routePreCheckLicenseFn); + initGetSpacesApi(server, routePreCheckLicenseFn); + initPostSpacesApi(server, routePreCheckLicenseFn); + initPutSpacesApi(server, routePreCheckLicenseFn); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.test.ts b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts new file mode 100644 index 0000000000000..f97931d36ed66 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/post.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +import Boom from 'boom'; +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initPostSpacesApi } from './post'; + +describe('Spaces Public API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initPostSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test('POST /space should create a new space with the provided ID', async () => { + const payload = { + id: 'my-space-id', + name: 'my new space', + description: 'with a description', + }; + + const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces/space', { + payload, + }); + + const { statusCode } = response; + + expect(statusCode).toEqual(200); + expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith( + 'space', + { name: 'my new space', description: 'with a description' }, + { id: 'my-space-id', overwrite: false } + ); + }); + + test(`returns result of routePreCheckLicense`, async () => { + const payload = { + id: 'my-space-id', + name: 'my new space', + description: 'with a description', + }; + + const { response } = await request('POST', '/api/spaces/space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + payload, + }); + + const { statusCode, payload: responsePayload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(responsePayload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + + test('POST /space should not allow a space to be updated', async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { response } = await request('POST', '/api/spaces/space', { payload }); + + const { statusCode, payload: responsePayload } = response; + + expect(statusCode).toEqual(409); + expect(JSON.parse(responsePayload)).toEqual({ + error: 'Conflict', + message: + 'A space with the identifier a-space already exists. Please choose a different identifier', + statusCode: 409, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/post.ts b/x-pack/plugins/spaces/server/routes/api/public/post.ts new file mode 100644 index 0000000000000..fd51390af5023 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/post.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { omit } from 'lodash'; +import { wrapError } from '../../../lib/errors'; +import { spaceSchema } from '../../../lib/space_schema'; +import { getSpaceById } from '../../lib'; + +export function initPostSpacesApi(server: any, routePreCheckLicenseFn: any) { + server.route({ + method: 'POST', + path: '/api/spaces/space', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const space = omit(request.payload, ['id', '_reserved']); + + const id = request.payload.id; + + const existingSpace = await getSpaceById(client, id); + if (existingSpace) { + return reply( + Boom.conflict( + `A space with the identifier ${id} already exists. Please choose a different identifier` + ) + ); + } + + try { + return reply(await client.create('space', { ...space }, { id, overwrite: false })); + } catch (error) { + return reply(wrapError(error)); + } + }, + config: { + validate: { + payload: spaceSchema, + }, + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.test.ts b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts new file mode 100644 index 0000000000000..2af4fc9bbeaf3 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/put.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); +import Boom from 'boom'; +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initPutSpacesApi } from './put'; + +describe('Spaces Public API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initPutSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test('PUT /space should update an existing space with the provided ID', async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/space/a-space', { + payload, + }); + + const { statusCode } = response; + + expect(statusCode).toEqual(200); + expect(mockSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsClient.update).toHaveBeenCalledWith('space', 'a-space', { + name: 'my updated space', + description: 'with a description', + }); + }); + + test(`returns result of routePreCheckLicense`, async () => { + const payload = { + id: 'a-space', + name: 'my updated space', + description: 'with a description', + }; + + const { response } = await request('PUT', '/api/spaces/space/a-space', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + payload, + }); + + const { statusCode, payload: responsePayload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(responsePayload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + + test('PUT /space should not allow a new space to be created', async () => { + const payload = { + id: 'a-new-space', + name: 'my new space', + description: 'with a description', + }; + + const { response } = await request('PUT', '/api/spaces/space/a-new-space', { payload }); + + const { statusCode } = response; + + expect(statusCode).toEqual(404); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/public/put.ts b/x-pack/plugins/spaces/server/routes/api/public/put.ts new file mode 100644 index 0000000000000..093d7c777e786 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/public/put.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { omit } from 'lodash'; +import { Space } from '../../../../common/model/space'; +import { wrapError } from '../../../lib/errors'; +import { spaceSchema } from '../../../lib/space_schema'; +import { convertSavedObjectToSpace, getSpaceById } from '../../lib'; + +export function initPutSpacesApi(server: any, routePreCheckLicenseFn: any) { + server.route({ + method: 'PUT', + path: '/api/spaces/space/{id}', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const space: Space = omit(request.payload, ['id']); + const id = request.params.id; + + const existingSpace = await getSpaceById(client, id); + + if (existingSpace) { + space._reserved = existingSpace._reserved; + } else { + return reply(Boom.notFound(`Unable to find space with ID ${id}`)); + } + + let result; + try { + result = await client.update('space', id, { ...space }); + } catch (error) { + return reply(wrapError(error)); + } + + const updatedSpace = convertSavedObjectToSpace(result); + return reply(updatedSpace); + }, + config: { + validate: { + payload: spaceSchema, + }, + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/api/v1/index.ts b/x-pack/plugins/spaces/server/routes/api/v1/index.ts new file mode 100644 index 0000000000000..75659c14c03ae --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/v1/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; +import { initPrivateSpacesApi } from './spaces'; + +export function initPrivateApis(server: any) { + const routePreCheckLicenseFn = routePreCheckLicense(server); + initPrivateSpacesApi(server, routePreCheckLicenseFn); +} diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.js deleted file mode 100644 index 5d250c1c92c07..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.js +++ /dev/null @@ -1,202 +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 Boom from 'boom'; -import { omit } from 'lodash'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { spaceSchema } from '../../../lib/space_schema'; -import { wrapError } from '../../../lib/errors'; -import { isReservedSpace } from '../../../../common/is_reserved_space'; -import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; - -export function initSpacesApi(server) { - const routePreCheckLicenseFn = routePreCheckLicense(server); - - function convertSavedObjectToSpace(savedObject) { - return { - id: savedObject.id, - ...savedObject.attributes - }; - } - - server.route({ - method: 'GET', - path: '/api/spaces/v1/spaces', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - let spaces; - - try { - const result = await client.find({ - type: 'space', - sortField: 'name.keyword', - }); - - spaces = result.saved_objects.map(convertSavedObjectToSpace); - } catch (error) { - return reply(wrapError(error)); - } - - return reply(spaces); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'GET', - path: '/api/spaces/v1/space/{id}', - async handler(request, reply) { - const spaceId = request.params.id; - - const client = request.getSavedObjectsClient(); - - try { - const response = await client.get('space', spaceId); - - return reply(convertSavedObjectToSpace(response)); - } catch (error) { - return reply(wrapError(error)); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/spaces/v1/space', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const space = omit(request.payload, ['id', '_reserved']); - - const id = request.payload.id; - - const existingSpace = await getSpaceById(client, id); - if (existingSpace) { - return reply(Boom.conflict(`A space with the identifier ${id} already exists. Please choose a different identifier`)); - } - - try { - return reply(await client.create('space', { ...space }, { id, overwrite: false })); - } catch (error) { - return reply(wrapError(error)); - } - - }, - config: { - validate: { - payload: spaceSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'PUT', - path: '/api/spaces/v1/space/{id}', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const space = omit(request.payload, ['id']); - const id = request.params.id; - - const existingSpace = await getSpaceById(client, id); - - if (existingSpace) { - space._reserved = existingSpace._reserved; - } else { - return reply(Boom.notFound(`Unable to find space with ID ${id}`)); - } - - let result; - try { - result = await client.update('space', id, { ...space }); - } catch (error) { - return reply(wrapError(error)); - } - - const updatedSpace = convertSavedObjectToSpace(result); - return reply(updatedSpace); - }, - config: { - validate: { - payload: spaceSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'DELETE', - path: '/api/spaces/v1/space/{id}', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const id = request.params.id; - - let result; - - try { - const existingSpace = await getSpaceById(client, id); - if (isReservedSpace(existingSpace)) { - return reply(wrapError(Boom.badRequest('This Space cannot be deleted because it is reserved.'))); - } - - result = await client.delete('space', id); - } catch (error) { - return reply(wrapError(error)); - } - - return reply(result).code(204); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/spaces/v1/space/{id}/select', - async handler(request, reply) { - const client = request.getSavedObjectsClient(); - - const id = request.params.id; - - try { - const existingSpace = await getSpaceById(client, id); - - const config = server.config(); - - return reply({ - location: addSpaceIdToPath(config.get('server.basePath'), existingSpace.id, config.get('server.defaultRoute')) - }); - - } catch (error) { - return reply(wrapError(error)); - } - } - }); - - async function getSpaceById(client, spaceId) { - try { - const existingSpace = await client.get('space', spaceId); - return { - id: existingSpace.id, - ...existingSpace.attributes - }; - } catch (error) { - if (client.errors.isNotFoundError(error)) { - return null; - } - throw error; - } - } -} diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js deleted file mode 100644 index fb957e4586343..0000000000000 --- a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.js +++ /dev/null @@ -1,281 +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 { Server } from 'hapi'; -import { initSpacesApi } from './spaces'; - -jest.mock('../../../lib/route_pre_check_license', () => { - return { - routePreCheckLicense: () => (request, reply) => reply.continue() - }; -}); - -jest.mock('../../../../../../server/lib/get_client_shield', () => { - return { - getClient: () => { - return { - callWithInternalUser: jest.fn(() => { }) - }; - } - }; -}); - -const spaces = [{ - id: 'a-space', - attributes: { - name: 'a space', - } -}, { - id: 'b-space', - attributes: { - name: 'b space', - } -}, { - id: 'default', - attributes: { - name: 'Default Space', - _reserved: true - } -}]; - -describe('Spaces API', () => { - const teardowns = []; - let request; - - const baseConfig = { - 'server.basePath': '' - }; - - beforeEach(() => { - request = async (method, path, options = {}) => { - - const { - setupFn = () => { }, - testConfig = {}, - payload, - } = options; - - const server = new Server(); - - const config = { - ...baseConfig, - ...testConfig - }; - - server.connection({ port: 0 }); - - await setupFn(server); - - server.decorate('server', 'config', jest.fn(() => { - return { - get: (key) => config[key] - }; - })); - - initSpacesApi(server); - - server.decorate('request', 'getBasePath', jest.fn()); - server.decorate('request', 'setBasePath', jest.fn()); - - // Mock server.getSavedObjectsClient() - const mockSavedObjectsClient = { - get: jest.fn((type, id) => { - return spaces.filter(s => s.id === id)[0]; - }), - find: jest.fn(() => { - return { - total: spaces.length, - saved_objects: spaces - }; - }), - create: jest.fn(() => ({})), - update: jest.fn(() => ({})), - delete: jest.fn(), - errors: { - isNotFoundError: jest.fn(() => true) - } - }; - - server.decorate('request', 'getSavedObjectsClient', () => mockSavedObjectsClient); - - teardowns.push(() => server.stop()); - - return { - server, - mockSavedObjectsClient, - response: await server.inject({ - method, - url: path, - payload, - }) - }; - }; - }); - - afterEach(async () => { - await Promise.all(teardowns.splice(0).map(fn => fn())); - }); - - test(`'GET spaces' returns all available spaces`, async () => { - const { response } = await request('GET', '/api/spaces/v1/spaces'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - const resultSpaces = JSON.parse(payload); - expect(resultSpaces.map(s => s.id)).toEqual(spaces.map(s => s.id)); - }); - - test(`'GET spaces/{id}' returns the space with that id`, async () => { - const { response } = await request('GET', '/api/spaces/v1/space/default'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - const resultSpace = JSON.parse(payload); - expect(resultSpace.id).toEqual('default'); - }); - - test(`'DELETE spaces/{id}' deletes the space`, async () => { - const { response } = await request('DELETE', '/api/spaces/v1/space/a-space'); - - const { - statusCode - } = response; - - expect(statusCode).toEqual(204); - }); - - test(`'DELETE spaces/{id}' cannot delete reserved spaces`, async () => { - const { response } = await request('DELETE', '/api/spaces/v1/space/default'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(400); - expect(JSON.parse(payload)).toEqual({ - statusCode: 400, - error: "Bad Request", - message: "This Space cannot be deleted because it is reserved." - }); - }); - - test('POST /space should create a new space with the provided ID', async () => { - const payload = { - id: 'my-space-id', - name: 'my new space', - description: 'with a description', - }; - - const { mockSavedObjectsClient, response } = await request('POST', '/api/spaces/v1/space', { payload }); - - const { - statusCode, - } = response; - - expect(statusCode).toEqual(200); - expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(mockSavedObjectsClient.create) - .toHaveBeenCalledWith('space', { name: 'my new space', description: 'with a description' }, { id: 'my-space-id', overwrite: false }); - }); - - test('POST /space should not allow a space to be updated', async () => { - const payload = { - id: 'a-space', - name: 'my updated space', - description: 'with a description', - }; - - const { response } = await request('POST', '/api/spaces/v1/space', { payload }); - - const { - statusCode, - payload: responsePayload, - } = response; - - expect(statusCode).toEqual(409); - expect(JSON.parse(responsePayload)).toEqual({ - error: 'Conflict', - message: "A space with the identifier a-space already exists. Please choose a different identifier", - statusCode: 409 - }); - }); - - test('PUT /space should update an existing space with the provided ID', async () => { - const payload = { - id: 'a-space', - name: 'my updated space', - description: 'with a description', - }; - - const { mockSavedObjectsClient, response } = await request('PUT', '/api/spaces/v1/space/a-space', { payload }); - - const { - statusCode, - } = response; - - expect(statusCode).toEqual(200); - expect(mockSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(mockSavedObjectsClient.update) - .toHaveBeenCalledWith('space', 'a-space', { name: 'my updated space', description: 'with a description' }); - }); - - test('PUT /space should not allow a new space to be created', async () => { - const payload = { - id: 'a-new-space', - name: 'my new space', - description: 'with a description', - }; - - const { response } = await request('PUT', '/api/spaces/v1/space/a-new-space', { payload }); - - const { - statusCode, - } = response; - - expect(statusCode).toEqual(404); - }); - - test('POST space/{id}/select should respond with the new space location', async () => { - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/s/a-space'); - }); - - test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { - const testConfig = { - 'server.basePath': '/my/base/path' - }; - - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { testConfig }); - - const { - statusCode, - payload - } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/my/base/path/s/a-space'); - }); -}); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts new file mode 100644 index 0000000000000..bbdab1be34910 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../lib/route_pre_check_license', () => { + return { + routePreCheckLicense: () => (request: any, reply: any) => reply.continue(), + }; +}); + +jest.mock('../../../../../../server/lib/get_client_shield', () => { + return { + getClient: () => { + return { + callWithInternalUser: jest.fn(() => { + return; + }), + }; + }, + }; +}); + +import Boom from 'boom'; +import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; +import { initPrivateSpacesApi } from './spaces'; + +describe('Spaces API', () => { + let request: RequestRunner; + let teardowns: TeardownFn[]; + + beforeEach(() => { + const setup = createTestHandler(initPrivateSpacesApi); + + request = setup.request; + teardowns = setup.teardowns; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + test('POST space/{id}/select should respond with the new space location', async () => { + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + + const result = JSON.parse(payload); + expect(result.location).toEqual('/s/a-space'); + }); + + test(`returns result of routePreCheckLicense`, async () => { + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { + preCheckLicenseImpl: (req: any, reply: any) => + reply(Boom.forbidden('test forbidden message')), + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(403); + expect(JSON.parse(payload)).toMatchObject({ + message: 'test forbidden message', + }); + }); + + test('POST space/{id}/select should respond with 404 when the space is not found', async () => { + const { response } = await request('POST', '/api/spaces/v1/space/not-a-space/select'); + + const { statusCode } = response; + + expect(statusCode).toEqual(404); + }); + + test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { + const testConfig = { + 'server.basePath': '/my/base/path', + }; + + const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { + testConfig, + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + + const result = JSON.parse(payload); + expect(result.location).toEqual('/my/base/path/s/a-space'); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts new file mode 100644 index 0000000000000..0233cb76b96d8 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/api/v1/spaces.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { Space } from '../../../../common/model/space'; +import { wrapError } from '../../../lib/errors'; +import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; +import { getSpaceById } from '../../lib'; + +export function initPrivateSpacesApi(server: any, routePreCheckLicenseFn: any) { + server.route({ + method: 'POST', + path: '/api/spaces/v1/space/{id}/select', + async handler(request: any, reply: any) { + const client = request.getSavedObjectsClient(); + + const id = request.params.id; + + try { + const existingSpace: Space | null = await getSpaceById(client, id); + if (!existingSpace) { + return reply(Boom.notFound()); + } + + const config = server.config(); + + return reply({ + location: addSpaceIdToPath( + config.get('server.basePath'), + existingSpace.id, + config.get('server.defaultRoute') + ), + }); + } catch (error) { + return reply(wrapError(error)); + } + }, + config: { + pre: [routePreCheckLicenseFn], + }, + }); +} diff --git a/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts new file mode 100644 index 0000000000000..31738ff562865 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { convertSavedObjectToSpace } from './convert_saved_object_to_space'; + +describe('convertSavedObjectToSpace', () => { + it('converts a saved object representation to a Space object', () => { + const savedObject = { + id: 'foo', + attributes: { + name: 'Foo Space', + description: 'no fighting', + _reserved: false, + }, + }; + + expect(convertSavedObjectToSpace(savedObject)).toEqual({ + id: 'foo', + name: 'Foo Space', + description: 'no fighting', + _reserved: false, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts new file mode 100644 index 0000000000000..d3ee173a2e80f --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Space } from '../../../common/model/space'; + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function convertSavedObjectToSpace(savedObject: any): Space { + return { + id: savedObject.id, + ...savedObject.attributes, + }; +} diff --git a/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts b/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts new file mode 100644 index 0000000000000..4143c09a79a93 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/get_space_by_id.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Space } from '../../../common/model/space'; +import { convertSavedObjectToSpace } from './convert_saved_object_to_space'; + +export async function getSpaceById(client: any, spaceId: string): Promise { + try { + const existingSpace = await client.get('space', spaceId); + return convertSavedObjectToSpace(existingSpace); + } catch (error) { + if (client.errors.isNotFoundError(error)) { + return null; + } + throw error; + } +} diff --git a/x-pack/plugins/spaces/server/routes/lib/index.ts b/x-pack/plugins/spaces/server/routes/lib/index.ts new file mode 100644 index 0000000000000..af67388792565 --- /dev/null +++ b/x-pack/plugins/spaces/server/routes/lib/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { convertSavedObjectToSpace } from './convert_saved_object_to_space'; +export { getSpaceById } from './get_space_by_id'; From 5f02f3e4ea42044debbf87932d278e81814e0a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 6 Sep 2018 15:50:45 +0200 Subject: [PATCH 28/68] [APM] Fix broken links (#22592) * [APM] Fix broken links * Add missing basepaths * Remove basepath from getMlJobUrl --- .../Watcher/WatcherButton.js | 5 +- .../Watcher/__test__/WatcherButton.test.js | 41 ++++++++++++++ .../__snapshots__/WatcherButton.test.js.snap | 54 +++++++++++++++++++ .../DynamicBaseline/Button.js | 3 +- .../DynamicBaseline/Flyout.js | 8 +-- .../DynamicBaseline/__jest__/Button.test.js | 39 ++++++++++++++ .../__snapshots__/Button.test.js.snap | 54 +++++++++++++++++++ .../app/TransactionOverview/view.js | 8 +-- .../__test__/__snapshots__/url.test.js.snap | 2 +- .../apm/public/utils/__test__/url.test.js | 29 +++++++++- 10 files changed, 231 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/WatcherButton.test.js create mode 100644 x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/WatcherButton.test.js.snap create mode 100644 x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/Button.test.js create mode 100644 x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/__snapshots__/Button.test.js.snap diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherButton.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherButton.js index 4f682ed3a531b..a3c034ca3bc08 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherButton.js +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/WatcherButton.js @@ -5,6 +5,7 @@ */ import React, { Component } from 'react'; +import chrome from 'ui/chrome'; import { EuiButton, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui'; export default class WatcherButton extends Component { @@ -31,7 +32,9 @@ export default class WatcherButton extends Component { { name: 'View existing watches', icon: 'tableOfContents', - href: '/app/kibana#/management/elasticsearch/watcher/', + href: chrome.addBasePath( + '/app/kibana#/management/elasticsearch/watcher/' + ), target: '_blank', onClick: () => this.closePopover() } diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/WatcherButton.test.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/WatcherButton.test.js new file mode 100644 index 0000000000000..23590be5f0d38 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/WatcherButton.test.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import DetailView from '../WatcherButton'; + +jest.mock('ui/chrome', () => ({ + addBasePath: path => `myBasePath${path}` +})); + +describe('WatcherButton', () => { + let wrapper; + beforeEach(() => { + wrapper = shallow(); + }); + + it('should render initial state', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('should have correct url', () => { + const panels = wrapper.find('EuiContextMenu').prop('panels'); + expect(panels[0].items[1].href).toBe( + 'myBasePath/app/kibana#/management/elasticsearch/watcher/' + ); + }); + + it('popover should be closed', () => { + expect(wrapper.find('EuiPopover').prop('isOpen')).toBe(false); + }); + + it('should open popover', async () => { + await wrapper.instance().onButtonClick(); + wrapper.update(); + expect(wrapper.find('EuiPopover').prop('isOpen')).toBe(true); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/WatcherButton.test.js.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/WatcherButton.test.js.snap new file mode 100644 index 0000000000000..8918566a7cead --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/Watcher/__test__/__snapshots__/WatcherButton.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WatcherButton should render initial state 1`] = ` + + Integrations + + } + closePopover={[Function]} + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" +> + , + "name": "Enable error reports", + "onClick": [Function], + }, + Object { + "href": "myBasePath/app/kibana#/management/elasticsearch/watcher/", + "icon": "tableOfContents", + "name": "View existing watches", + "onClick": [Function], + "target": "_blank", + }, + ], + "title": "Watcher", + }, + ] + } + /> + +`; diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Button.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Button.js index bbba48082d3be..b7f8589dbd588 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Button.js +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Button.js @@ -5,6 +5,7 @@ */ import React, { Component } from 'react'; +import chrome from 'ui/chrome'; import { EuiButton, EuiPopover, EuiIcon, EuiContextMenu } from '@elastic/eui'; export default class DynamicBaselineButton extends Component { @@ -31,7 +32,7 @@ export default class DynamicBaselineButton extends Component { { name: 'View existing jobs', icon: 'tableOfContents', - href: '/app/ml', + href: chrome.addBasePath('/app/ml'), target: '_blank', onClick: () => this.closePopover() } diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Flyout.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Flyout.js index a48db866b41d1..1231e60e5bd80 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Flyout.js +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/Flyout.js @@ -172,9 +172,11 @@ export default class DynamicBaselineFlyout extends Component { Jobs can be created per transaction type and based on the average response time. Once a job is created, you can manage it and see more details in the{' '} - Machine Learning jobs management page. It - might take some time for the job to calculate the results. Please - refresh the graph a few minutes after creating the job. + + Machine Learning jobs management page + + . It might take some time for the job to calculate the results. + Please refresh the graph a few minutes after creating the job.

{/* Learn more about the Machine Learning integration. */} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/Button.test.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/Button.test.js new file mode 100644 index 0000000000000..c7396cf8c4847 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/Button.test.js @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import DetailView from '../Button'; + +jest.mock('ui/chrome', () => ({ + addBasePath: path => `myBasePath${path}` +})); + +describe('MLButton', () => { + let wrapper; + beforeEach(() => { + wrapper = shallow(); + }); + + it('should render initial state', () => { + expect(wrapper).toMatchSnapshot(); + }); + + it('should have correct url', () => { + const panels = wrapper.find('EuiContextMenu').prop('panels'); + expect(panels[0].items[1].href).toBe('myBasePath/app/ml'); + }); + + it('popover should be closed', () => { + expect(wrapper.find('EuiPopover').prop('isOpen')).toBe(false); + }); + + it('should open popover', async () => { + await wrapper.instance().onButtonClick(); + wrapper.update(); + expect(wrapper.find('EuiPopover').prop('isOpen')).toBe(true); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/__snapshots__/Button.test.js.snap b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/__snapshots__/Button.test.js.snap new file mode 100644 index 0000000000000..6e57f557d1bd4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/DynamicBaseline/__jest__/__snapshots__/Button.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MLButton should render initial state 1`] = ` + + Integrations + + } + closePopover={[Function]} + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" +> + , + "name": "Anomaly detection (BETA)", + "onClick": [Function], + }, + Object { + "href": "myBasePath/app/ml", + "icon": "tableOfContents", + "name": "View existing jobs", + "onClick": [Function], + "target": "_blank", + }, + ], + "title": "Machine Learning", + }, + ] + } + /> + +`; diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/view.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/view.js index ec14242232691..b250afe837965 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/view.js +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/view.js @@ -13,7 +13,7 @@ import { get } from 'lodash'; import { HeaderContainer, HeaderMedium } from '../../shared/UIComponents'; import TabNavigation from '../../shared/TabNavigation'; import Charts from '../../shared/charts/TransactionCharts'; -import { getMlJobUrl } from '../../../utils/url'; +import { getMlJobUrl, KibanaLink } from '../../../utils/url'; import List from './List'; import { units, px, fontSizes } from '../../../style/variables'; import { OverviewChartsRequest } from '../../../store/reactReduxRequest/overviewCharts'; @@ -75,15 +75,15 @@ class TransactionOverview extends Component { Machine Learning:{' '} - View Job - + ) : null; diff --git a/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap b/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap index 4e5e7a8c64042..2252f7f40ce6b 100644 --- a/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap +++ b/x-pack/plugins/apm/public/utils/__test__/__snapshots__/url.test.js.snap @@ -3,7 +3,7 @@ exports[`KibanaLinkComponent should render correct markup 1`] = ` Go to Discover diff --git a/x-pack/plugins/apm/public/utils/__test__/url.test.js b/x-pack/plugins/apm/public/utils/__test__/url.test.js index 87ed6764a1fbc..28a8a6bbd469f 100644 --- a/x-pack/plugins/apm/public/utils/__test__/url.test.js +++ b/x-pack/plugins/apm/public/utils/__test__/url.test.js @@ -15,10 +15,15 @@ import { KibanaLinkComponent, RelativeLinkComponent, encodeKibanaSearchParams, - decodeKibanaSearchParams + decodeKibanaSearchParams, + getMlJobUrl } from '../url'; import { toJson } from '../testHelpers'; +jest.mock('ui/chrome', () => ({ + addBasePath: path => `myBasePath${path}` +})); + describe('encodeKibanaSearchParams and decodeKibanaSearchParams should return the original string', () => { it('should convert string to object', () => { const search = `?_g=(ml:(jobIds:!(opbeans-node-request-high_mean_response_time)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2018-06-06T08:20:45.437Z',mode:absolute,to:'2018-06-14T21:56:58.505Z'))&_a=(filters:!(),mlSelectInterval:(interval:(display:Auto,val:auto)),mlSelectSeverity:(threshold:(display:warning,val:0)),mlTimeSeriesExplorer:(),query:(query_string:(analyze_wildcard:!t,query:'*')))`; @@ -207,7 +212,7 @@ describe('KibanaLinkComponent', () => { it('should have correct url', () => { expect(wrapper.find('a').prop('href')).toBe( - "/app/kibana#/discover?_g=&_a=(interval:auto,query:(language:lucene,query:'context.service.name:myServiceName AND error.grouping_key:myGroupId'),sort:('@timestamp':desc))" + "myBasePath/app/kibana#/discover?_g=&_a=(interval:auto,query:(language:lucene,query:'context.service.name:myServiceName AND error.grouping_key:myGroupId'),sort:('@timestamp':desc))" ); }); @@ -215,3 +220,23 @@ describe('KibanaLinkComponent', () => { expect(toJson(wrapper)).toMatchSnapshot(); }); }); + +describe('getMlJobUrl', () => { + it('should have correct url', () => { + const serviceName = 'myServiceName'; + const transactionType = 'myTransactionType'; + const location = { search: '' }; + expect(getMlJobUrl(serviceName, transactionType, location)).toBe( + '/app/ml#/timeseriesexplorer/?_g=(ml:(jobIds:!(myServiceName-myTransactionType-high_mean_response_time)))&_a=!n' + ); + }); + + it('should not contain basePath', () => { + const serviceName = 'myServiceName'; + const transactionType = 'myTransactionType'; + const location = { search: '' }; + expect(getMlJobUrl(serviceName, transactionType, location)).toBe( + '/app/ml#/timeseriesexplorer/?_g=(ml:(jobIds:!(myServiceName-myTransactionType-high_mean_response_time)))&_a=!n' + ); + }); +}); From 125cc36baec2a52cae5f06375b7ecfbf3d8dc613 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 10:21:33 -0400 Subject: [PATCH 29/68] supply space id to tutorial context (#22760) --- x-pack/plugins/spaces/index.js | 5 ++ ...es_service.js => create_spaces_service.ts} | 11 ++-- .../spaces_tutorial_context_factory.test.ts | 55 +++++++++++++++++++ .../lib/spaces_tutorial_context_factory.ts | 15 +++++ 4 files changed, 82 insertions(+), 4 deletions(-) rename x-pack/plugins/spaces/server/lib/{create_spaces_service.js => create_spaces_service.ts} (75%) create mode 100644 x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js index 3eafab6461f9e..2679a894a6661 100644 --- a/x-pack/plugins/spaces/index.js +++ b/x-pack/plugins/spaces/index.js @@ -14,6 +14,7 @@ import { createDefaultSpace } from './server/lib/create_default_space'; import { createSpacesService } from './server/lib/create_spaces_service'; import { getActiveSpace } from './server/lib/get_active_space'; import { getSpacesUsageCollector } from './server/lib/get_spaces_usage_collector'; +import { createSpacesTutorialContextFactory } from './server/lib/spaces_tutorial_context_factory'; import { wrapError } from './server/lib/errors'; import mappings from './mappings.json'; import { spacesSavedObjectsClientWrapperFactory } from './server/lib/saved_objects_client/saved_objects_client_wrapper_factory'; @@ -94,6 +95,10 @@ export const spaces = (kibana) => new kibana.Plugin({ spacesSavedObjectsClientWrapperFactory(spacesService) ); + server.addScopedTutorialContextFactory( + createSpacesTutorialContextFactory(spacesService) + ); + initPrivateApis(server); initPublicSpacesApi(server); diff --git a/x-pack/plugins/spaces/server/lib/create_spaces_service.js b/x-pack/plugins/spaces/server/lib/create_spaces_service.ts similarity index 75% rename from x-pack/plugins/spaces/server/lib/create_spaces_service.js rename to x-pack/plugins/spaces/server/lib/create_spaces_service.ts index 3ee55354da8dc..3269142a9cf17 100644 --- a/x-pack/plugins/spaces/server/lib/create_spaces_service.js +++ b/x-pack/plugins/spaces/server/lib/create_spaces_service.ts @@ -6,13 +6,16 @@ import { getSpaceIdFromPath } from './spaces_url_parser'; -export function createSpacesService(server) { +export interface SpacesService { + getSpaceId: (req: any) => string; +} +export function createSpacesService(server: any): SpacesService { const serverBasePath = server.config().get('server.basePath'); const contextCache = new WeakMap(); - function getSpaceId(request) { + function getSpaceId(request: any) { if (!contextCache.has(request)) { populateCache(request); } @@ -21,11 +24,11 @@ export function createSpacesService(server) { return spaceId; } - function populateCache(request) { + function populateCache(request: any) { const spaceId = getSpaceIdFromPath(request.getBasePath(), serverBasePath); contextCache.set(request, { - spaceId + spaceId, }); } diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts new file mode 100644 index 0000000000000..4ed548d64b574 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { createSpacesService } from './create_spaces_service'; +import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory'; + +const server = { + config: () => { + return { + get: (key: string) => { + if (key === 'server.basePath') { + return '/foo'; + } + throw new Error('unexpected key ' + key); + }, + }; + }, +}; + +describe('createSpacesTutorialContextFactory', () => { + it('should create a valid context factory', () => { + const spacesService = createSpacesService(server); + expect(typeof createSpacesTutorialContextFactory(spacesService)).toEqual('function'); + }); + + it('should create context with the current space id for space my-space-id', () => { + const spacesService = createSpacesService(server); + const contextFactory = createSpacesTutorialContextFactory(spacesService); + + const request = { + getBasePath: () => '/foo/s/my-space-id', + }; + + expect(contextFactory(request)).toEqual({ + spaceId: 'my-space-id', + }); + }); + + it('should create context with the current space id for the default space', () => { + const spacesService = createSpacesService(server); + const contextFactory = createSpacesTutorialContextFactory(spacesService); + + const request = { + getBasePath: () => '/foo', + }; + + expect(contextFactory(request)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts new file mode 100644 index 0000000000000..b3254fd3b3c07 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/spaces_tutorial_context_factory.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SpacesService } from './create_spaces_service'; + +export function createSpacesTutorialContextFactory(spacesService: SpacesService) { + return function spacesTutorialContextFactory(request: any) { + return { + spaceId: spacesService.getSpaceId(request), + }; + }; +} From 5f96c903f379c89f384aaa5397b03f0c5894c4a7 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 10:26:05 -0400 Subject: [PATCH 30/68] Deprecate xpack:defaultAdminEmail for monitoring alerts (#22195) --- docs/monitoring/cluster-alerts.asciidoc | 4 +- .../monitoring/__tests__/deprecations.js | 62 +++++++ x-pack/plugins/monitoring/common/constants.js | 9 +- x-pack/plugins/monitoring/config.js | 5 +- x-pack/plugins/monitoring/deprecations.js | 12 +- .../__tests__/check_for_email_value.js | 12 +- .../__tests__/get_default_admin_email.js | 151 ++++++++++++++---- .../collectors/get_settings_collector.js | 35 +++- 8 files changed, 245 insertions(+), 45 deletions(-) diff --git a/docs/monitoring/cluster-alerts.asciidoc b/docs/monitoring/cluster-alerts.asciidoc index 15df791ae2746..fce76965dd9ae 100644 --- a/docs/monitoring/cluster-alerts.asciidoc +++ b/docs/monitoring/cluster-alerts.asciidoc @@ -46,8 +46,6 @@ To receive email notifications for the Cluster Alerts: 1. Configure an email account as described in {xpack-ref}/actions-email.html#configuring-email[Configuring Email Accounts]. -2. Navigate to the *Management* page in {kib}. -3. Go to the *Advanced Settings* page, find the `xpack:defaultAdminEmail` -setting, and enter your email address. +2. Configure the `xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in `kibana.yml` with your email address. Email notifications are sent only when Cluster Alerts are triggered and resolved. diff --git a/x-pack/plugins/monitoring/__tests__/deprecations.js b/x-pack/plugins/monitoring/__tests__/deprecations.js index 8eb6473cfd318..6b8668b156a8f 100644 --- a/x-pack/plugins/monitoring/__tests__/deprecations.js +++ b/x-pack/plugins/monitoring/__tests__/deprecations.js @@ -112,4 +112,66 @@ describe('monitoring plugin deprecations', function () { expect(log.called).to.be(false); }); + describe('cluster_alerts.email_notifications.email_address', function () { + it(`shouldn't log when email notifications are disabled`, function () { + const settings = { + cluster_alerts: { + email_notifications: { + enabled: false + } + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it(`shouldn't log when cluster alerts are disabled`, function () { + const settings = { + cluster_alerts: { + enabled: false, + email_notifications: { + enabled: true + } + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it(`shouldn't log when email_address is specified`, function () { + const settings = { + cluster_alerts: { + enabled: true, + email_notifications: { + enabled: true, + email_address: 'foo@bar.com' + } + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(false); + }); + + it(`should log when email_address is missing, but alerts/notifications are both enabled`, function () { + const settings = { + cluster_alerts: { + enabled: true, + email_notifications: { + enabled: true + } + } + }; + + const log = sinon.spy(); + transformDeprecations(settings, log); + expect(log.called).to.be(true); + }); + }); + }); diff --git a/x-pack/plugins/monitoring/common/constants.js b/x-pack/plugins/monitoring/common/constants.js index 718606d8b6be2..7bc8164130adf 100644 --- a/x-pack/plugins/monitoring/common/constants.js +++ b/x-pack/plugins/monitoring/common/constants.js @@ -94,7 +94,7 @@ export const CALCULATE_DURATION_UNTIL = 'until'; /** * In order to show ML Jobs tab in the Elasticsearch section / tab navigation, license must be supported */ -export const ML_SUPPORTED_LICENSES = [ 'trial', 'platinum' ]; +export const ML_SUPPORTED_LICENSES = ['trial', 'platinum']; /** * Metadata service URLs for the different cloud services that have constant URLs (e.g., unlike GCP, which is a constant prefix). @@ -135,7 +135,12 @@ export const DEFAULT_NO_DATA_MESSAGE_WITH_FILTER = ( ); export const TABLE_ACTION_UPDATE_FILTER = 'UPDATE_FILTER'; -export const TABLE_ACTION_RESET_PAGING = 'RESET_PAGING'; +export const TABLE_ACTION_RESET_PAGING = 'RESET_PAGING'; export const DEBOUNCE_SLOW_MS = 17; // roughly how long it takes to render a frame at 60fps export const DEBOUNCE_FAST_MS = 10; // roughly how long it takes to render a frame at 100fps + +/** + * Configuration key for setting the email address used for cluster alert notifications. + */ +export const CLUSTER_ALERTS_ADDRESS_CONFIG_KEY = 'cluster_alerts.email_notifications.email_address'; diff --git a/x-pack/plugins/monitoring/config.js b/x-pack/plugins/monitoring/config.js index 02ac28728206a..431aed95182fb 100644 --- a/x-pack/plugins/monitoring/config.js +++ b/x-pack/plugins/monitoring/config.js @@ -13,7 +13,7 @@ import { XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS } from '../../server/li */ export const config = (Joi) => { const { array, boolean, number, object, string } = Joi; - const DEFAULT_REQUEST_HEADERS = [ 'authorization' ]; + const DEFAULT_REQUEST_HEADERS = ['authorization']; return object({ ccs: object({ @@ -49,7 +49,8 @@ export const config = (Joi) => { enabled: boolean().default(true), index: string().default('.monitoring-alerts-6'), email_notifications: object({ - enabled: boolean().default(true) + enabled: boolean().default(true), + email_address: string().email(), }).default() }).default(), xpack_api_polling_frequency_millis: number().default(XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS), diff --git a/x-pack/plugins/monitoring/deprecations.js b/x-pack/plugins/monitoring/deprecations.js index 8e882975a29cc..a6b91d4a6d604 100644 --- a/x-pack/plugins/monitoring/deprecations.js +++ b/x-pack/plugins/monitoring/deprecations.js @@ -5,6 +5,7 @@ */ import { get, has, set } from 'lodash'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from './common/constants'; /** * Re-writes deprecated user-defined config settings and logs warnings as a @@ -29,12 +30,19 @@ export const deprecations = ({ rename }) => { delete settings.elasticsearch.ssl.verify; log('Config key "xpack.monitoring.elasticsearch.ssl.verify" is deprecated. ' + - 'It has been replaced with "xpack.monitoring.elasticsearch.ssl.verificationMode"'); + 'It has been replaced with "xpack.monitoring.elasticsearch.ssl.verificationMode"'); }, (settings, log) => { if (has(settings, 'report_stats')) { log('Config key "xpack.monitoring.report_stats" is deprecated and will be removed in 7.0. ' + - 'Use "xpack.xpack_main.telemetry.enabled" instead.'); + 'Use "xpack.xpack_main.telemetry.enabled" instead.'); + } + }, + (settings, log) => { + const clusterAlertsEnabled = get(settings, 'cluster_alerts.enabled'); + const emailNotificationsEnabled = clusterAlertsEnabled && get(settings, 'cluster_alerts.email_notifications.enabled'); + if (emailNotificationsEnabled && !get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log(`Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."`); } }, ]; diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js index a2bc6f4b9419a..4189903684a9b 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/check_for_email_value.js @@ -8,27 +8,31 @@ import expect from 'expect.js'; import { checkForEmailValue } from '../get_settings_collector'; describe('getSettingsCollector / checkForEmailValue', () => { + const mockLogger = { + warn: () => { } + }; + it('ignores shouldUseNull=true value and returns email if email value if one is set', async () => { const shouldUseNull = true; const getDefaultAdminEmailMock = () => 'test@elastic.co'; - expect(await checkForEmailValue(undefined, undefined, shouldUseNull, getDefaultAdminEmailMock)).to.be('test@elastic.co'); + expect(await checkForEmailValue(undefined, undefined, mockLogger, shouldUseNull, getDefaultAdminEmailMock)).to.be('test@elastic.co'); }); it('ignores shouldUseNull=false value and returns email if email value if one is set', async () => { const shouldUseNull = false; const getDefaultAdminEmailMock = () => 'test@elastic.co'; - expect(await checkForEmailValue(undefined, undefined, shouldUseNull, getDefaultAdminEmailMock)).to.be('test@elastic.co'); + expect(await checkForEmailValue(undefined, undefined, mockLogger, shouldUseNull, getDefaultAdminEmailMock)).to.be('test@elastic.co'); }); it('returns a null if no email value is set and null is allowed', async () => { const shouldUseNull = true; const getDefaultAdminEmailMock = () => null; - expect(await checkForEmailValue(undefined, undefined, shouldUseNull, getDefaultAdminEmailMock)).to.be(null); + expect(await checkForEmailValue(undefined, undefined, mockLogger, shouldUseNull, getDefaultAdminEmailMock)).to.be(null); }); it('returns undefined if no email value is set and null is not allowed', async () => { const shouldUseNull = false; const getDefaultAdminEmailMock = () => null; - expect(await checkForEmailValue(undefined, undefined, shouldUseNull, getDefaultAdminEmailMock)).to.be(undefined); + expect(await checkForEmailValue(undefined, undefined, mockLogger, shouldUseNull, getDefaultAdminEmailMock)).to.be(undefined); }); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js index 7796cb9621d7c..434f6bc41f585 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/__tests__/get_default_admin_email.js @@ -9,16 +9,23 @@ import sinon from 'sinon'; import { set } from 'lodash'; import { XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING } from '../../../../../../server/lib/constants'; -import { getDefaultAdminEmail } from '../get_settings_collector'; +import { getDefaultAdminEmail, resetDeprecationWarning } from '../get_settings_collector'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../../../../common/constants'; describe('getSettingsCollector / getDefaultAdminEmail', () => { - function setup({ enabled = true, docExists = true, adminEmail = 'admin@email.com' }) { + function setup({ enabled = true, docExists = true, defaultAdminEmail = 'default-admin@email.com', adminEmail = null }) { const config = { get: sinon.stub() }; config.get .withArgs('xpack.monitoring.cluster_alerts.email_notifications.enabled') .returns(enabled); + if (adminEmail) { + config.get + .withArgs(`xpack.monitoring.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}`) + .returns(adminEmail); + } + config.get .withArgs('kibana.index') .returns('.kibana'); @@ -29,8 +36,8 @@ describe('getSettingsCollector / getDefaultAdminEmail', () => { const doc = {}; if (docExists) { - if (adminEmail) { - set(doc, ['_source', 'config', XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING], adminEmail); + if (defaultAdminEmail) { + set(doc, ['_source', 'config', XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING], defaultAdminEmail); } else { set(doc, '_source.config', {}); } @@ -46,41 +53,131 @@ describe('getSettingsCollector / getDefaultAdminEmail', () => { })) .returns(doc); + const log = { + warn: sinon.stub() + }; + return { config, - callCluster + callCluster, + log, }; } - describe('xpack.monitoring.cluster_alerts.email_notifications.enabled = false', () => { - it('returns null', async () => { - const { config, callCluster } = setup({ enabled: false }); - expect(await getDefaultAdminEmail(config, callCluster)).to.be(null); - sinon.assert.notCalled(callCluster); + describe('using xpack:defaultAdminEmail', () => { + beforeEach(() => { + resetDeprecationWarning(); }); - }); - describe('doc does not exist', () => { - it('returns null', async () => { - const { config, callCluster } = setup({ docExists: false }); - expect(await getDefaultAdminEmail(config, callCluster)).to.be(null); - sinon.assert.calledOnce(callCluster); + describe('xpack.monitoring.cluster_alerts.email_notifications.enabled = false', () => { + + it('returns null', async () => { + const { config, callCluster, log } = setup({ enabled: false }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be(null); + sinon.assert.notCalled(callCluster); + }); + + it('does not log a deprecation warning', async () => { + const { config, callCluster, log } = setup({ enabled: false }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.notCalled(log.warn); + }); }); - }); - describe('value is not defined', () => { - it('returns null', async () => { - const { config, callCluster } = setup({ adminEmail: false }); - expect(await getDefaultAdminEmail(config, callCluster)).to.be(null); - sinon.assert.calledOnce(callCluster); + describe('doc does not exist', () => { + it('returns null', async () => { + const { config, callCluster, log } = setup({ docExists: false }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be(null); + sinon.assert.calledOnce(callCluster); + }); + + it('logs a deprecation warning', async () => { + const { config, callCluster, log } = setup({ docExists: false }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.calledOnce(log.warn); + }); + }); + + describe('value is not defined', () => { + it('returns null', async () => { + const { config, callCluster, log } = setup({ defaultAdminEmail: false }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be(null); + sinon.assert.calledOnce(callCluster); + }); + + it('logs a deprecation warning', async () => { + const { config, callCluster, log } = setup({ defaultAdminEmail: false }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.calledOnce(log.warn); + }); + }); + + describe('value is defined', () => { + it('returns value', async () => { + const { config, callCluster, log } = setup({ defaultAdminEmail: 'hello@world' }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be('hello@world'); + sinon.assert.calledOnce(callCluster); + }); + + it('logs a deprecation warning', async () => { + const { config, callCluster, log } = setup({ defaultAdminEmail: 'hello@world' }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.calledOnce(log.warn); + }); }); }); - describe('value is defined', () => { - it('returns value', async () => { - const { config, callCluster } = setup({ adminEmail: 'hello@world' }); - expect(await getDefaultAdminEmail(config, callCluster)).to.be('hello@world'); - sinon.assert.calledOnce(callCluster); + describe('using xpack.monitoring.cluster_alerts.email_notifications.email_address', () => { + beforeEach(() => { + resetDeprecationWarning(); + }); + + describe('xpack.monitoring.cluster_alerts.email_notifications.enabled = false', () => { + it('returns null', async () => { + const { config, callCluster, log } = setup({ enabled: false }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be(null); + sinon.assert.notCalled(callCluster); + }); + + it('does not log a deprecation warning', async () => { + const { config, callCluster, log } = setup({ enabled: false }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.notCalled(log.warn); + }); + }); + + describe('value is not defined', () => { + it('returns value from xpack:defaultAdminEmail', async () => { + const { config, callCluster, log } = setup({ + defaultAdminEmail: 'default-admin@email.com', + adminEmail: false + }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be('default-admin@email.com'); + sinon.assert.calledOnce(callCluster); + }); + + it('logs a deprecation warning', async () => { + const { config, callCluster, log } = setup({ + defaultAdminEmail: 'default-admin@email.com', + adminEmail: false + }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.calledOnce(log.warn); + }); + }); + + describe('value is defined', () => { + it('returns value', async () => { + const { config, callCluster, log } = setup({ adminEmail: 'hello@world' }); + expect(await getDefaultAdminEmail(config, callCluster, log)).to.be('hello@world'); + sinon.assert.notCalled(callCluster); + }); + + it('does not log a deprecation warning', async () => { + const { config, callCluster, log } = setup({ adminEmail: 'hello@world' }); + await getDefaultAdminEmail(config, callCluster, log); + sinon.assert.notCalled(log.warn); + }); }); }); }); diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js index c0793a4bb6b9d..0deafc2c49826 100644 --- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js +++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_settings_collector.js @@ -6,24 +6,48 @@ import { get } from 'lodash'; import { XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING } from '../../../../../server/lib/constants'; -import { KIBANA_SETTINGS_TYPE } from '../../../common/constants'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY, KIBANA_SETTINGS_TYPE } from '../../../common/constants'; + +let loggedDeprecationWarning = false; + +export function resetDeprecationWarning() { + loggedDeprecationWarning = false; +} /* * Check if Cluster Alert email notifications is enabled in config * If so, use uiSettings API to fetch the X-Pack default admin email */ -export async function getDefaultAdminEmail(config, callCluster) { +export async function getDefaultAdminEmail(config, callCluster, log) { if (!config.get('xpack.monitoring.cluster_alerts.email_notifications.enabled')) { return null; } + const emailAddressConfigKey = `xpack.monitoring.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}`; + const configuredEmailAddress = config.get(emailAddressConfigKey); + + if (configuredEmailAddress) { + return configuredEmailAddress; + } + + // DEPRECATED (Remove below in 7.0): If an email address is not configured in kibana.yml, then fallback to xpack:defaultAdminEmail + if (!loggedDeprecationWarning) { + const message = ( + `Monitoring is using ${XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING} for cluster alert notifications, ` + + `which will not be supported in Kibana 7.0. Please configure ${emailAddressConfigKey} in your kibana.yml settings` + ); + + log.warn(message); + loggedDeprecationWarning = true; + } + const index = config.get('kibana.index'); const version = config.get('pkg.version'); const uiSettingsDoc = await callCluster('get', { index, type: 'doc', id: `config:${version}`, - ignore: [ 400, 404 ] // 400 if the index is closed, 404 if it does not exist + ignore: [400, 404] // 400 if the index is closed, 404 if it does not exist }); return get(uiSettingsDoc, ['_source', 'config', XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING], null); @@ -35,10 +59,11 @@ let shouldUseNull = true; export async function checkForEmailValue( config, callCluster, + log, _shouldUseNull = shouldUseNull, _getDefaultAdminEmail = getDefaultAdminEmail ) { - const defaultAdminEmail = await _getDefaultAdminEmail(config, callCluster); + const defaultAdminEmail = await _getDefaultAdminEmail(config, callCluster, log); // Allow null so clearing the advanced setting will be reflected in the data const isAcceptableNull = defaultAdminEmail === null && _shouldUseNull; @@ -61,7 +86,7 @@ export function getSettingsCollector(server) { type: KIBANA_SETTINGS_TYPE, async fetch(callCluster) { let kibanaSettingsData; - const defaultAdminEmail = await checkForEmailValue(config, callCluster); + const defaultAdminEmail = await checkForEmailValue(config, callCluster, this.log); // skip everything if defaultAdminEmail === undefined if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) { From 3d6de7cbc46aa84d327ae155bfe7d14f98e49326 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 6 Sep 2018 18:39:04 +0300 Subject: [PATCH 31/68] Implement `LegacyService`. Use `core` to start legacy Kibana. (#22190) * Implement `LegacyService`. Use `core` to start legacy Kibana. * Fix Worker tests * Do not rely on kbnServer when testing mixins. --- package.json | 6 +- .../cluster.js} | 28 +- src/cli/cluster/cluster_manager.js | 59 +-- src/cli/cluster/cluster_manager.test.js | 146 +++++++- src/cli/cluster/configure_base_path_proxy.js | 64 ---- .../cluster/configure_base_path_proxy.test.js | 163 --------- src/cli/cluster/worker.test.js | 152 ++++---- src/cli/color.js | 7 +- .../__snapshots__/invalid_config.test.js.snap | 5 +- .../integration_tests/invalid_config.test.js | 3 +- src/cli/serve/serve.js | 119 ++---- src/core/README.md | 29 +- src/core/index.ts | 20 -- .../server/__snapshots__/index.test.ts.snap | 21 ++ src/core/server/bootstrap.ts | 113 ++++++ .../server/config/__tests__/__mocks__/env.ts | 11 +- .../__tests__/__snapshots__/env.test.ts.snap | 122 +++++-- .../config/__tests__/apply_argv.test.ts | 2 +- src/core/server/config/__tests__/env.test.ts | 75 ++-- src/core/server/config/env.ts | 29 +- src/core/server/config/index.ts | 2 +- .../config/schema/byte_size_value/index.ts | 3 +- .../__snapshots__/http_config.test.ts.snap | 1 + .../__snapshots__/http_server.test.ts.snap | 2 +- .../server/http/__tests__/http_server.test.ts | 107 +++--- .../http/__tests__/http_service.test.ts | 51 ++- .../server/http/base_path_proxy_server.ts | 64 ++-- src/core/server/http/http_config.ts | 3 + src/core/server/http/http_server.ts | 31 +- src/core/server/http/http_service.ts | 11 +- src/core/server/http/index.ts | 12 +- src/core/server/index.test.ts | 121 +++++++ src/core/server/index.ts | 34 +- .../legacy_platform_proxifier.test.ts.snap | 21 -- .../__snapshots__/legacy_service.test.ts.snap | 137 +++++++ ....test.ts => legacy_platform_proxy.test.ts} | 74 +--- .../__tests__/legacy_service.test.ts | 339 ++++++++++++++++++ ...gacy_object_to_config_adapter.test.ts.snap | 1 + .../config/legacy_object_to_config_adapter.ts | 3 +- src/core/server/legacy_compat/index.ts | 55 +-- .../legacy_platform_proxifier.ts | 172 --------- .../legacy_compat/legacy_platform_proxy.ts | 107 ++++++ .../server/legacy_compat/legacy_service.ts | 204 +++++++++++ src/core/server/logging/logging_service.ts | 2 +- src/core/server/root/base_path_proxy_root.ts | 80 ----- src/core/server/root/index.ts | 59 ++- src/core/types/core_service.ts | 4 +- .../server/lib/__tests__/manage_uuid.js | 2 +- .../__tests__/lib/index.js | 20 -- .../__tests__/lib/kibana.js | 36 -- .../config/__tests__/deprecation_warnings.js | 3 +- .../fixtures/run_kbn_server_startup.js | 8 +- .../max_payload_size.test.js.snap | 3 - .../http/__snapshots__/xsrf.test.js.snap | 7 - src/server/http/index.js | 30 +- .../max_payload_size.test.js | 52 +++ .../version_check.test.js | 63 ++-- .../http/{ => integration_tests}/xsrf.test.js | 132 ++----- src/server/http/max_payload_size.test.js | 70 ---- src/server/http/setup_connection.js | 0 src/server/kbn_server.js | 32 +- src/test_utils/base_auth.js | 23 -- src/test_utils/kbn_server.js | 155 -------- src/test_utils/kbn_server.ts | 184 ++++++++++ .../ui_exports_replace_injected_vars.js | 84 +++-- .../__tests__/field_formats_mixin.js | 24 +- src/ui/tutorials_mixin.test.js | 27 +- x-pack/package.json | 4 +- x-pack/yarn.lock | 12 +- yarn.lock | 18 +- 70 files changed, 2152 insertions(+), 1711 deletions(-) rename src/cli/cluster/{_mock_cluster_fork.js => __mocks__/cluster.js} (75%) delete mode 100644 src/cli/cluster/configure_base_path_proxy.js delete mode 100644 src/cli/cluster/configure_base_path_proxy.test.js delete mode 100644 src/core/index.ts create mode 100644 src/core/server/__snapshots__/index.test.ts.snap create mode 100644 src/core/server/bootstrap.ts create mode 100644 src/core/server/index.test.ts delete mode 100644 src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap create mode 100644 src/core/server/legacy_compat/__tests__/__snapshots__/legacy_service.test.ts.snap rename src/core/server/legacy_compat/__tests__/{legacy_platform_proxifier.test.ts => legacy_platform_proxy.test.ts} (51%) create mode 100644 src/core/server/legacy_compat/__tests__/legacy_service.test.ts delete mode 100644 src/core/server/legacy_compat/legacy_platform_proxifier.ts create mode 100644 src/core/server/legacy_compat/legacy_platform_proxy.ts create mode 100644 src/core/server/legacy_compat/legacy_service.ts delete mode 100644 src/core/server/root/base_path_proxy_root.ts delete mode 100644 src/functional_test_runner/__tests__/lib/index.js delete mode 100644 src/functional_test_runner/__tests__/lib/kibana.js delete mode 100644 src/server/http/__snapshots__/max_payload_size.test.js.snap delete mode 100644 src/server/http/__snapshots__/xsrf.test.js.snap create mode 100644 src/server/http/integration_tests/max_payload_size.test.js rename src/server/http/{ => integration_tests}/version_check.test.js (53%) rename src/server/http/{ => integration_tests}/xsrf.test.js (55%) delete mode 100644 src/server/http/max_payload_size.test.js delete mode 100644 src/server/http/setup_connection.js delete mode 100644 src/test_utils/base_auth.js delete mode 100644 src/test_utils/kbn_server.js create mode 100644 src/test_utils/kbn_server.ts diff --git a/package.json b/package.json index 5ea6728b6458e..0e372f5a65513 100644 --- a/package.json +++ b/package.json @@ -239,7 +239,7 @@ "@types/redux-actions": "^2.2.1", "@types/sinon": "^5.0.0", "@types/strip-ansi": "^3.0.0", - "@types/supertest": "^2.0.4", + "@types/supertest": "^2.0.5", "@types/type-detect": "^4.0.1", "angular-mocks": "1.4.7", "babel-eslint": "8.1.2", @@ -318,8 +318,8 @@ "simple-git": "1.37.0", "sinon": "^5.0.7", "strip-ansi": "^3.0.1", - "supertest": "3.0.0", - "supertest-as-promised": "4.0.2", + "supertest": "^3.1.0", + "supertest-as-promised": "^4.0.2", "tree-kill": "^1.1.0", "ts-jest": "^22.4.6", "ts-loader": "^3.5.0", diff --git a/src/cli/cluster/_mock_cluster_fork.js b/src/cli/cluster/__mocks__/cluster.js similarity index 75% rename from src/cli/cluster/_mock_cluster_fork.js rename to src/cli/cluster/__mocks__/cluster.js index 4312f6a85c53a..14efc4b6f0150 100644 --- a/src/cli/cluster/_mock_cluster_fork.js +++ b/src/cli/cluster/__mocks__/cluster.js @@ -16,15 +16,14 @@ * specific language governing permissions and limitations * under the License. */ +/* eslint-env jest */ import EventEmitter from 'events'; import { assign, random } from 'lodash'; -import sinon from 'sinon'; -import cluster from 'cluster'; import { delay } from 'bluebird'; -export default class MockClusterFork extends EventEmitter { - constructor() { +class MockClusterFork extends EventEmitter { + constructor(cluster) { super(); let dead = true; @@ -35,7 +34,7 @@ export default class MockClusterFork extends EventEmitter { assign(this, { process: { - kill: sinon.spy(() => { + kill: jest.fn(() => { (async () => { await wait(); this.emit('disconnect'); @@ -46,13 +45,13 @@ export default class MockClusterFork extends EventEmitter { })(); }), }, - isDead: sinon.spy(() => dead), - send: sinon.stub() + isDead: jest.fn(() => dead), + send: jest.fn() }); - sinon.spy(this, 'on'); - sinon.spy(this, 'removeListener'); - sinon.spy(this, 'emit'); + jest.spyOn(this, 'on'); + jest.spyOn(this, 'removeListener'); + jest.spyOn(this, 'emit'); (async () => { await wait(); @@ -61,3 +60,12 @@ export default class MockClusterFork extends EventEmitter { })(); } } + +class MockCluster extends EventEmitter { + fork = jest.fn(() => new MockClusterFork(this)); + setupMaster = jest.fn(); +} + +export function mockCluster() { + return new MockCluster(); +} diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 0a514138b09f2..1ea8a91eb21ef 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -19,31 +19,30 @@ import { resolve } from 'path'; import { debounce, invoke, bindAll, once, uniq } from 'lodash'; +import { fromEvent, race } from 'rxjs'; +import { first } from 'rxjs/operators'; import Log from '../log'; import Worker from './worker'; import { Config } from '../../server/config/config'; import { transformDeprecations } from '../../server/config/transform_deprecations'; -import { configureBasePathProxy } from './configure_base_path_proxy'; process.env.kbnWorkerType = 'managr'; export default class ClusterManager { - static async create(opts = {}, settings = {}) { - const transformedSettings = transformDeprecations(settings); - const config = Config.withDefaultSchema(transformedSettings); - - const basePathProxy = opts.basePath - ? await configureBasePathProxy(config) - : undefined; - - return new ClusterManager(opts, config, basePathProxy); + static create(opts, settings = {}, basePathProxy) { + return new ClusterManager( + opts, + Config.withDefaultSchema(transformDeprecations(settings)), + basePathProxy + ); } constructor(opts, config, basePathProxy) { this.log = new Log(opts.quiet, opts.silent); this.addedCount = 0; this.inReplMode = !!opts.repl; + this.basePathProxy = basePathProxy; const serverArgv = []; const optimizerArgv = [ @@ -51,17 +50,15 @@ export default class ClusterManager { '--server.autoListen=false', ]; - if (basePathProxy) { - this.basePathProxy = basePathProxy; - + if (this.basePathProxy) { optimizerArgv.push( - `--server.basePath=${this.basePathProxy.getBasePath()}`, + `--server.basePath=${this.basePathProxy.basePath}`, '--server.rewriteBasePath=true', ); serverArgv.push( - `--server.port=${this.basePathProxy.getTargetPort()}`, - `--server.basePath=${this.basePathProxy.getBasePath()}`, + `--server.port=${this.basePathProxy.targetPort}`, + `--server.basePath=${this.basePathProxy.basePath}`, '--server.rewriteBasePath=true', ); } @@ -82,12 +79,6 @@ export default class ClusterManager { }) ]; - if (basePathProxy) { - // Pass server worker to the basepath proxy so that it can hold off the - // proxying until server worker is ready. - this.basePathProxy.serverWorker = this.server; - } - // broker messages between workers this.workers.forEach((worker) => { worker.on('broadcast', (msg) => { @@ -130,7 +121,10 @@ export default class ClusterManager { this.setupManualRestart(); invoke(this.workers, 'start'); if (this.basePathProxy) { - this.basePathProxy.start(); + this.basePathProxy.start({ + blockUntil: this.blockUntil.bind(this), + shouldRedirectFromOldBasePath: this.shouldRedirectFromOldBasePath.bind(this), + }); } } @@ -222,4 +216,23 @@ export default class ClusterManager { this.log.bad('failed to watch files!\n', err.stack); process.exit(1); // eslint-disable-line no-process-exit } + + shouldRedirectFromOldBasePath(path) { + 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 race( + fromEvent(this.server, 'listening'), + fromEvent(this.server, 'crashed') + ).pipe(first()).toPromise(); + } } diff --git a/src/cli/cluster/cluster_manager.test.js b/src/cli/cluster/cluster_manager.test.js index b80ee62da29c3..ab42c4a369bb8 100644 --- a/src/cli/cluster/cluster_manager.test.js +++ b/src/cli/cluster/cluster_manager.test.js @@ -17,36 +17,43 @@ * under the License. */ -import sinon from 'sinon'; +import { mockCluster } from './__mocks__/cluster'; +jest.mock('cluster', () => mockCluster()); +jest.mock('readline', () => ({ + createInterface: jest.fn(() => ({ + on: jest.fn(), + prompt: jest.fn(), + setPrompt: jest.fn(), + })), +})); + import cluster from 'cluster'; import { sample } from 'lodash'; import ClusterManager from './cluster_manager'; import Worker from './worker'; -describe('CLI cluster manager', function () { - const sandbox = sinon.createSandbox(); - - beforeEach(function () { - sandbox.stub(cluster, 'fork').callsFake(() => { +describe('CLI cluster manager', () => { + beforeEach(() => { + cluster.fork.mockImplementation(() => { return { process: { - kill: sinon.stub(), + kill: jest.fn(), }, - isDead: sinon.stub().returns(false), - removeListener: sinon.stub(), - on: sinon.stub(), - send: sinon.stub() + isDead: jest.fn().mockReturnValue(false), + removeListener: jest.fn(), + addListener: jest.fn(), + send: jest.fn() }; }); }); - afterEach(function () { - sandbox.restore(); + afterEach(() => { + cluster.fork.mockReset(); }); - it('has two workers', async function () { - const manager = await ClusterManager.create({}); + test('has two workers', () => { + const manager = ClusterManager.create({}); expect(manager.workers).toHaveLength(2); for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker); @@ -55,8 +62,8 @@ describe('CLI cluster manager', function () { expect(manager.server).toBeInstanceOf(Worker); }); - it('delivers broadcast messages to other workers', async function () { - const manager = await ClusterManager.create({}); + test('delivers broadcast messages to other workers', () => { + const manager = ClusterManager.create({}); for (const worker of manager.workers) { Worker.prototype.start.call(worker);// bypass the debounced start method @@ -69,10 +76,111 @@ describe('CLI cluster manager', function () { messenger.emit('broadcast', football); for (const worker of manager.workers) { if (worker === messenger) { - expect(worker.fork.send.callCount).toBe(0); + expect(worker.fork.send).not.toHaveBeenCalled(); } else { - expect(worker.fork.send.firstCall.args[0]).toBe(football); + expect(worker.fork.send).toHaveBeenCalledTimes(1); + expect(worker.fork.send).toHaveBeenCalledWith(football); } } }); + + describe('interaction with BasePathProxy', () => { + test('correctly configures `BasePathProxy`.', async () => { + const basePathProxyMock = { start: jest.fn() }; + + ClusterManager.create({}, {}, basePathProxyMock); + + expect(basePathProxyMock.start).toHaveBeenCalledWith({ + shouldRedirectFromOldBasePath: expect.any(Function), + blockUntil: expect.any(Function), + }); + }); + + describe('proxy is configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', () => { + let clusterManager; + let shouldRedirectFromOldBasePath; + let blockUntil; + beforeEach(async () => { + const basePathProxyMock = { start: jest.fn() }; + + clusterManager = ClusterManager.create({}, {}, basePathProxyMock); + + jest.spyOn(clusterManager.server, 'addListener'); + jest.spyOn(clusterManager.server, 'removeListener'); + + [[{ 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); + }); + + 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.addListener).not.toHaveBeenCalled(); + expect(clusterManager.server.removeListener).not.toHaveBeenCalled(); + }); + + test('`blockUntil()` resolves immediately if worker is already listening.', async () => { + clusterManager.server.listening = true; + + await expect(blockUntil()).resolves.not.toBeDefined(); + expect(clusterManager.server.addListener).not.toHaveBeenCalled(); + expect(clusterManager.server.removeListener).not.toHaveBeenCalled(); + }); + + test('`blockUntil()` resolves when worker crashes.', async () => { + const blockUntilPromise = blockUntil(); + + expect(clusterManager.server.addListener).toHaveBeenCalledTimes(2); + expect(clusterManager.server.addListener).toHaveBeenCalledWith( + 'crashed', + expect.any(Function) + ); + + const [, [eventName, onCrashed]] = clusterManager.server.addListener.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.removeListener).not.toHaveBeenCalled(); + + onCrashed(); + await expect(blockUntilPromise).resolves.not.toBeDefined(); + + expect(clusterManager.server.removeListener).toHaveBeenCalledTimes(2); + }); + + test('`blockUntil()` resolves when worker starts listening.', async () => { + const blockUntilPromise = blockUntil(); + + expect(clusterManager.server.addListener).toHaveBeenCalledTimes(2); + expect(clusterManager.server.addListener).toHaveBeenCalledWith( + 'listening', + expect.any(Function) + ); + + const [[eventName, onListening]] = clusterManager.server.addListener.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.removeListener).not.toHaveBeenCalled(); + + onListening(); + await expect(blockUntilPromise).resolves.not.toBeDefined(); + + expect(clusterManager.server.removeListener).toHaveBeenCalledTimes(2); + }); + }); + }); }); diff --git a/src/cli/cluster/configure_base_path_proxy.js b/src/cli/cluster/configure_base_path_proxy.js deleted file mode 100644 index 477b10053d1e6..0000000000000 --- a/src/cli/cluster/configure_base_path_proxy.js +++ /dev/null @@ -1,64 +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 { Server } from 'hapi'; -import { createBasePathProxy } from '../../core'; -import { setupLogging } from '../../server/logging'; - -export async function configureBasePathProxy(config) { - // New platform forwards all logs to the legacy platform so we need HapiJS server - // here just for logging purposes and nothing else. - const server = new Server(); - setupLogging(server, config); - - const basePathProxy = createBasePathProxy({ server, config }); - - await basePathProxy.configure({ - shouldRedirectFromOldBasePath: path => { - const isApp = path.startsWith('app/'); - const isKnownShortPath = ['login', 'logout', 'status'].includes(path); - - return isApp || isKnownShortPath; - }, - - blockUntil: () => { - // Wait until `serverWorker either crashes or starts to listen. - // The `serverWorker` property should be set by the ClusterManager - // once it creates the worker. - const serverWorker = basePathProxy.serverWorker; - if (serverWorker.listening || serverWorker.crashed) { - return Promise.resolve(); - } - - return new Promise(resolve => { - const done = () => { - serverWorker.removeListener('listening', done); - serverWorker.removeListener('crashed', done); - - resolve(); - }; - - serverWorker.on('listening', done); - serverWorker.on('crashed', done); - }); - }, - }); - - return basePathProxy; -} diff --git a/src/cli/cluster/configure_base_path_proxy.test.js b/src/cli/cluster/configure_base_path_proxy.test.js deleted file mode 100644 index 01cbaf0bcc900..0000000000000 --- a/src/cli/cluster/configure_base_path_proxy.test.js +++ /dev/null @@ -1,163 +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. - */ - -jest.mock('../../core', () => ({ - createBasePathProxy: jest.fn(), -})); - -jest.mock('../../server/logging', () => ({ - setupLogging: jest.fn(), -})); - -import { Server } from 'hapi'; -import { createBasePathProxy as createBasePathProxyMock } from '../../core'; -import { setupLogging as setupLoggingMock } from '../../server/logging'; -import { configureBasePathProxy } from './configure_base_path_proxy'; - -describe('configureBasePathProxy()', () => { - it('returns `BasePathProxy` instance.', async () => { - const basePathProxyMock = { configure: jest.fn() }; - createBasePathProxyMock.mockReturnValue(basePathProxyMock); - - const basePathProxy = await configureBasePathProxy({}); - - expect(basePathProxy).toBe(basePathProxyMock); - }); - - it('correctly configures `BasePathProxy`.', async () => { - const configMock = {}; - const basePathProxyMock = { configure: jest.fn() }; - createBasePathProxyMock.mockReturnValue(basePathProxyMock); - - await configureBasePathProxy(configMock); - - // Check that logging is configured with the right parameters. - expect(setupLoggingMock).toHaveBeenCalledWith( - expect.any(Server), - configMock - ); - - const [[server]] = setupLoggingMock.mock.calls; - expect(createBasePathProxyMock).toHaveBeenCalledWith({ - config: configMock, - server, - }); - - expect(basePathProxyMock.configure).toHaveBeenCalledWith({ - shouldRedirectFromOldBasePath: expect.any(Function), - blockUntil: expect.any(Function), - }); - }); - - describe('configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', async () => { - let serverWorkerMock; - let shouldRedirectFromOldBasePath; - let blockUntil; - beforeEach(async () => { - serverWorkerMock = { - listening: false, - crashed: false, - on: jest.fn(), - removeListener: jest.fn(), - }; - - const basePathProxyMock = { - configure: jest.fn(), - serverWorker: serverWorkerMock, - }; - - createBasePathProxyMock.mockReturnValue(basePathProxyMock); - - await configureBasePathProxy({}); - - [[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.configure.mock.calls; - }); - - it('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', async () => { - expect(shouldRedirectFromOldBasePath('')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); - }); - - it('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', async () => { - expect(shouldRedirectFromOldBasePath('app/')).toBe(true); - expect(shouldRedirectFromOldBasePath('login')).toBe(true); - expect(shouldRedirectFromOldBasePath('logout')).toBe(true); - expect(shouldRedirectFromOldBasePath('status')).toBe(true); - }); - - it('`blockUntil()` resolves immediately if worker has already crashed.', async () => { - serverWorkerMock.crashed = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(serverWorkerMock.on).not.toHaveBeenCalled(); - expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - }); - - it('`blockUntil()` resolves immediately if worker is already listening.', async () => { - serverWorkerMock.listening = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(serverWorkerMock.on).not.toHaveBeenCalled(); - expect(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - }); - - it('`blockUntil()` resolves when worker crashes.', async () => { - const blockUntilPromise = blockUntil(); - - expect(serverWorkerMock.on).toHaveBeenCalledTimes(2); - expect(serverWorkerMock.on).toHaveBeenCalledWith( - 'crashed', - expect.any(Function) - ); - - const [, [eventName, onCrashed]] = serverWorkerMock.on.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(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - - onCrashed(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2); - }); - - it('`blockUntil()` resolves when worker starts listening.', async () => { - const blockUntilPromise = blockUntil(); - - expect(serverWorkerMock.on).toHaveBeenCalledTimes(2); - expect(serverWorkerMock.on).toHaveBeenCalledWith( - 'listening', - expect.any(Function) - ); - - const [[eventName, onListening]] = serverWorkerMock.on.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(serverWorkerMock.removeListener).not.toHaveBeenCalled(); - - onListening(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/src/cli/cluster/worker.test.js b/src/cli/cluster/worker.test.js index c166956bcbf34..24687d640438a 100644 --- a/src/cli/cluster/worker.test.js +++ b/src/cli/cluster/worker.test.js @@ -17,26 +17,25 @@ * under the License. */ -import sinon from 'sinon'; +import { mockCluster } from './__mocks__/cluster'; +jest.mock('cluster', () => mockCluster()); + import cluster from 'cluster'; -import { findIndex } from 'lodash'; -import MockClusterFork from './_mock_cluster_fork'; import Worker from './worker'; import Log from '../log'; const workersToShutdown = []; function assertListenerAdded(emitter, event) { - sinon.assert.calledWith(emitter.on, event); + expect(emitter.on).toHaveBeenCalledWith(event, expect.any(Function)); } function assertListenerRemoved(emitter, event) { - sinon.assert.calledWith( - emitter.removeListener, - event, - emitter.on.args[findIndex(emitter.on.args, { 0: event })][1] - ); + const [, onEventListener] = emitter.on.mock.calls.find(([eventName]) => { + return eventName === event; + }); + expect(emitter.removeListener).toHaveBeenCalledWith(event, onEventListener); } function setup(opts = {}) { @@ -50,81 +49,82 @@ function setup(opts = {}) { return worker; } -describe('CLI cluster manager', function () { - const sandbox = sinon.createSandbox(); - - beforeEach(function () { - sandbox.stub(cluster, 'fork').callsFake(() => new MockClusterFork()); - }); - - afterEach(async function () { - sandbox.restore(); +describe('CLI cluster manager', () => { + afterEach(async () => { + while(workersToShutdown.length > 0) { + const worker = workersToShutdown.pop(); + // If `fork` exists we should set `exitCode` to the non-zero value to + // prevent worker from auto restart. + if (worker.fork) { + worker.fork.exitCode = 1; + } - for (const worker of workersToShutdown) { await worker.shutdown(); } + + cluster.fork.mockClear(); }); - describe('#onChange', function () { - describe('opts.watch = true', function () { - it('restarts the fork', function () { + describe('#onChange', () => { + describe('opts.watch = true', () => { + test('restarts the fork', () => { const worker = setup({ watch: true }); - sinon.stub(worker, 'start'); + jest.spyOn(worker, 'start').mockImplementation(() => {}); worker.onChange('/some/path'); expect(worker.changes).toEqual(['/some/path']); - sinon.assert.calledOnce(worker.start); + expect(worker.start).toHaveBeenCalledTimes(1); }); }); - describe('opts.watch = false', function () { - it('does not restart the fork', function () { + describe('opts.watch = false', () => { + test('does not restart the fork', () => { const worker = setup({ watch: false }); - sinon.stub(worker, 'start'); + jest.spyOn(worker, 'start').mockImplementation(() => {}); worker.onChange('/some/path'); expect(worker.changes).toEqual([]); - sinon.assert.notCalled(worker.start); + expect(worker.start).not.toHaveBeenCalled(); }); }); }); - describe('#shutdown', function () { - describe('after starting()', function () { - it('kills the worker and unbinds from message, online, and disconnect events', async function () { + describe('#shutdown', () => { + describe('after starting()', () => { + test('kills the worker and unbinds from message, online, and disconnect events', async () => { const worker = setup(); await worker.start(); expect(worker).toHaveProperty('online', true); const fork = worker.fork; - sinon.assert.notCalled(fork.process.kill); + expect(fork.process.kill).not.toHaveBeenCalled(); assertListenerAdded(fork, 'message'); assertListenerAdded(fork, 'online'); assertListenerAdded(fork, 'disconnect'); worker.shutdown(); - sinon.assert.calledOnce(fork.process.kill); + expect(fork.process.kill).toHaveBeenCalledTimes(1); assertListenerRemoved(fork, 'message'); assertListenerRemoved(fork, 'online'); assertListenerRemoved(fork, 'disconnect'); }); }); - describe('before being started', function () { - it('does nothing', function () { + describe('before being started', () => { + test('does nothing', () => { const worker = setup(); worker.shutdown(); }); }); }); - describe('#parseIncomingMessage()', function () { - describe('on a started worker', function () { - it(`is bound to fork's message event`, async function () { + describe('#parseIncomingMessage()', () => { + describe('on a started worker', () => { + test(`is bound to fork's message event`, async () => { const worker = setup(); await worker.start(); - sinon.assert.calledWith(worker.fork.on, 'message'); + expect(worker.fork.on).toHaveBeenCalledWith('message', expect.any(Function)); }); }); - describe('do after', function () { - it('ignores non-array messages', function () { + describe('do after', () => { + test('ignores non-array messages', () => { const worker = setup(); worker.parseIncomingMessage('some string thing'); worker.parseIncomingMessage(0); @@ -134,39 +134,39 @@ describe('CLI cluster manager', function () { worker.parseIncomingMessage(/weird/); }); - it('calls #onMessage with message parts', function () { + test('calls #onMessage with message parts', () => { const worker = setup(); - const stub = sinon.stub(worker, 'onMessage'); + jest.spyOn(worker, 'onMessage').mockImplementation(() => {}); worker.parseIncomingMessage([10, 100, 1000, 10000]); - sinon.assert.calledWith(stub, 10, 100, 1000, 10000); + expect(worker.onMessage).toHaveBeenCalledWith(10, 100, 1000, 10000); }); }); }); - describe('#onMessage', function () { - describe('when sent WORKER_BROADCAST message', function () { - it('emits the data to be broadcasted', function () { + describe('#onMessage', () => { + describe('when sent WORKER_BROADCAST message', () => { + test('emits the data to be broadcasted', () => { const worker = setup(); const data = {}; - const stub = sinon.stub(worker, 'emit'); + jest.spyOn(worker, 'emit').mockImplementation(() => {}); worker.onMessage('WORKER_BROADCAST', data); - sinon.assert.calledWithExactly(stub, 'broadcast', data); + expect(worker.emit).toHaveBeenCalledWith('broadcast', data); }); }); - describe('when sent WORKER_LISTENING message', function () { - it('sets the listening flag and emits the listening event', function () { + describe('when sent WORKER_LISTENING message', () => { + test('sets the listening flag and emits the listening event', () => { const worker = setup(); - const stub = sinon.stub(worker, 'emit'); + jest.spyOn(worker, 'emit').mockImplementation(() => {}); expect(worker).toHaveProperty('listening', false); worker.onMessage('WORKER_LISTENING'); expect(worker).toHaveProperty('listening', true); - sinon.assert.calledWithExactly(stub, 'listening'); + expect(worker.emit).toHaveBeenCalledWith('listening'); }); }); - describe('when passed an unknown message', function () { - it('does nothing', function () { + describe('when passed an unknown message', () => { + test('does nothing', () => { const worker = setup(); worker.onMessage('asdlfkajsdfahsdfiohuasdofihsdoif'); worker.onMessage({}); @@ -175,46 +175,46 @@ describe('CLI cluster manager', function () { }); }); - describe('#start', function () { - describe('when not started', function () { - // TODO This test is flaky, see https://github.com/elastic/kibana/issues/15888 - it.skip('creates a fork and waits for it to come online', async function () { + describe('#start', () => { + describe('when not started', () => { + test('creates a fork and waits for it to come online', async () => { const worker = setup(); - sinon.spy(worker, 'on'); + jest.spyOn(worker, 'on'); await worker.start(); - sinon.assert.calledOnce(cluster.fork); - sinon.assert.calledWith(worker.on, 'fork:online'); + expect(cluster.fork).toHaveBeenCalledTimes(1); + expect(worker.on).toHaveBeenCalledWith('fork:online', expect.any(Function)); }); - // TODO This test is flaky, see https://github.com/elastic/kibana/issues/15888 - it.skip('listens for cluster and process "exit" events', async function () { + test('listens for cluster and process "exit" events', async () => { const worker = setup(); - sinon.spy(process, 'on'); - sinon.spy(cluster, 'on'); + jest.spyOn(process, 'on'); + jest.spyOn(cluster, 'on'); await worker.start(); - sinon.assert.calledOnce(cluster.on); - sinon.assert.calledWith(cluster.on, 'exit'); - sinon.assert.calledOnce(process.on); - sinon.assert.calledWith(process.on, 'exit'); + expect(cluster.on).toHaveBeenCalledTimes(1); + expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(process.on).toHaveBeenCalledTimes(1); + expect(process.on).toHaveBeenCalledWith('exit', expect.any(Function)); }); }); - describe('when already started', function () { - it('calls shutdown and waits for the graceful shutdown to cause a restart', async function () { + describe('when already started', () => { + test('calls shutdown and waits for the graceful shutdown to cause a restart', async () => { const worker = setup(); await worker.start(); - sinon.spy(worker, 'shutdown'); - sinon.spy(worker, 'on'); + + jest.spyOn(worker, 'shutdown'); + jest.spyOn(worker, 'on'); worker.start(); - sinon.assert.calledOnce(worker.shutdown); - sinon.assert.calledWith(worker.on, 'online'); + + expect(worker.shutdown).toHaveBeenCalledTimes(1); + expect(worker.on).toHaveBeenCalledWith('online', expect.any(Function)); }); }); }); diff --git a/src/cli/color.js b/src/cli/color.js index b678376ef7c24..a02fb551c4181 100644 --- a/src/cli/color.js +++ b/src/cli/color.js @@ -17,9 +17,8 @@ * under the License. */ -import _ from 'lodash'; import chalk from 'chalk'; -export const green = _.flow(chalk.black, chalk.bgGreen); -export const red = _.flow(chalk.white, chalk.bgRed); -export const yellow = _.flow(chalk.black, chalk.bgYellow); +export const green = chalk.black.bgGreen; +export const red = chalk.white.bgRed; +export const yellow = chalk.black.bgYellow; diff --git a/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap b/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap index 0e702ed6123bd..47b98f740af58 100644 --- a/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap +++ b/src/cli/serve/integration_tests/__snapshots__/invalid_config.test.js.snap @@ -4,12 +4,15 @@ exports[`cli invalid config support exits with statusCode 64 and logs a single l Array [ Object { "@timestamp": "## @timestamp ##", + "error": "## Error with stack trace ##", + "level": "fatal", "message": "\\"unknown.key\\", \\"other.unknown.key\\", \\"other.third\\", \\"some.flat.key\\", and \\"some.array\\" settings were not applied. Check for spelling errors and ensure that expected plugins are installed.", "pid": "## PID ##", "tags": Array [ "fatal", + "root", ], - "type": "log", + "type": "error", }, ] `; diff --git a/src/cli/serve/integration_tests/invalid_config.test.js b/src/cli/serve/integration_tests/invalid_config.test.js index 335fb1dbcaf9f..495bfbeaa939e 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.js +++ b/src/cli/serve/integration_tests/invalid_config.test.js @@ -39,7 +39,8 @@ describe('cli invalid config support', function () { .map(obj => ({ ...obj, pid: '## PID ##', - '@timestamp': '## @timestamp ##' + '@timestamp': '## @timestamp ##', + error: '## Error with stack trace ##', })); expect(error).toBe(undefined); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 08495566d845e..2820ac6a64ea4 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -19,20 +19,15 @@ import _ from 'lodash'; import { statSync, lstatSync, realpathSync } from 'fs'; -import { isWorker } from 'cluster'; import { resolve } from 'path'; import { fromRoot } from '../../utils'; import { getConfig } from '../../server/path'; -import { Config } from '../../server/config/config'; -import { getConfigFromFiles } from '../../core/server/config'; +import { bootstrap } from '../../core/server'; import { readKeystore } from './read_keystore'; -import { transformDeprecations } from '../../server/config/transform_deprecations'; import { DEV_SSL_CERT_PATH, DEV_SSL_KEY_PATH } from '../dev_ssl'; -const { startRepl } = canRequire('../repl') ? require('../repl') : { }; - function canRequire(path) { try { require.resolve(path); @@ -60,6 +55,9 @@ function isSymlinkTo(link, dest) { const CLUSTER_MANAGER_PATH = resolve(__dirname, '../cluster/cluster_manager'); const CAN_CLUSTER = canRequire(CLUSTER_MANAGER_PATH); +const REPL_PATH = resolve(__dirname, '../repl'); +const CAN_REPL = canRequire(REPL_PATH); + // xpack is installed in both dev and the distributable, it's optional if // install is a link to the source, not an actual install const XPACK_INSTALLED_DIR = resolve(__dirname, '../../../node_modules/x-pack'); @@ -79,12 +77,11 @@ const configPathCollector = pathCollector(); const pluginDirCollector = pathCollector(); const pluginPathCollector = pathCollector(); -function readServerSettings(opts, extraCliOptions) { - const settings = getConfigFromFiles([].concat(opts.config || [])); - const set = _.partial(_.set, settings); - const get = _.partial(_.get, settings); - const has = _.partial(_.has, settings); - const merge = _.partial(_.merge, settings); +function applyConfigOverrides(rawConfig, opts, extraCliOptions) { + const set = _.partial(_.set, rawConfig); + const get = _.partial(_.get, rawConfig); + const has = _.partial(_.has, rawConfig); + const merge = _.partial(_.merge, rawConfig); if (opts.dev) { set('env', 'development'); @@ -133,7 +130,7 @@ function readServerSettings(opts, extraCliOptions) { merge(extraCliOptions); merge(readKeystore(get('path.data'))); - return settings; + return rawConfig; } export default function (program) { @@ -175,7 +172,7 @@ export default function (program) { ) .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector); - if (!!startRepl) { + if (CAN_REPL) { command.option('--repl', 'Run the server with a REPL prompt and access to the server object'); } @@ -205,81 +202,25 @@ export default function (program) { } } - const getCurrentSettings = () => readServerSettings(opts, this.getUnknownOptions()); - const settings = getCurrentSettings(); - - if (CAN_CLUSTER && opts.dev && !isWorker) { - // stop processing the action and handoff to cluster manager - const ClusterManager = require(CLUSTER_MANAGER_PATH); - await ClusterManager.create(opts, settings); - return; - } - - let kbnServer = {}; - const KbnServer = require('../../server/kbn_server'); - try { - kbnServer = new KbnServer(settings); - if (shouldStartRepl(opts)) { - startRepl(kbnServer); - } - await kbnServer.ready(); - } catch (error) { - const { server } = kbnServer; - - switch (error.code) { - case 'EADDRINUSE': - logFatal(`Port ${error.port} is already in use. Another instance of Kibana may be running!`, server); - break; - - case 'InvalidConfig': - logFatal(error.message, server); - break; - - default: - logFatal(error, server); - break; - } - - kbnServer.close(); - const exitCode = error.processExitCode == null ? 1 : error.processExitCode; - // eslint-disable-next-line no-process-exit - process.exit(exitCode); - } - - process.on('SIGHUP', async function reloadConfig() { - const settings = transformDeprecations(getCurrentSettings()); - const config = new Config(kbnServer.config.getSchema(), settings); - - kbnServer.server.log(['info', 'config'], 'Reloading logging configuration due to SIGHUP.'); - await kbnServer.applyLoggingConfiguration(config); - kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.'); - - // If new platform config subscription is active, let's notify it with the updated config. - if (kbnServer.newPlatform) { - kbnServer.newPlatform.updateConfig(config.get()); - } + const unknownOptions = this.getUnknownOptions(); + await bootstrap({ + configs: [].concat(opts.config || []), + cliArgs: { + dev: !!opts.dev, + envName: unknownOptions.env ? unknownOptions.env.name : undefined, + quiet: !!opts.quiet, + silent: !!opts.silent, + watch: !!opts.watch, + repl: !!opts.repl, + basePath: !!opts.basePath, + }, + features: { + isClusterModeSupported: CAN_CLUSTER, + isOssModeSupported: XPACK_OPTIONAL, + isXPackInstalled: XPACK_INSTALLED, + isReplModeSupported: CAN_REPL, + }, + applyConfigOverrides: rawConfig => applyConfigOverrides(rawConfig, opts, unknownOptions), }); - - return kbnServer; }); } - -function shouldStartRepl(opts) { - if (opts.repl && !startRepl) { - throw new Error('Kibana REPL mode can only be run in development mode.'); - } - - // The kbnWorkerType check is necessary to prevent the repl - // from being started multiple times in different processes. - // We only want one REPL. - return opts.repl && process.env.kbnWorkerType === 'server'; -} - -function logFatal(message, server) { - if (server) { - server.log(['fatal'], message); - } - - // It's possible for the Hapi logger to not be setup - console.error('FATAL', message); -} diff --git a/src/core/README.md b/src/core/README.md index c3b056f981726..196946ed9e4a3 100644 --- a/src/core/README.md +++ b/src/core/README.md @@ -5,26 +5,17 @@ Core is a set of systems (frontend, backend etc.) that Kibana and its plugins ar ## Integration with the "legacy" Kibana Most of the existing core functionality is still spread over "legacy" Kibana and it will take some time to upgrade it. -Kibana is still started using existing "legacy" CLI and bootstraps `core` only when needed. At the moment `core` manages -HTTP connections, handles TLS configuration and base path proxy. All requests to Kibana server will hit HTTP server -exposed by the `core` first and it will decide whether request can be solely handled by the new platform or request should -be proxied to the "legacy" Kibana. This setup allows `core` to gradually introduce any "pre-route" processing -logic, expose new routes or replace old ones handled by the "legacy" Kibana currently. +Kibana is started using existing "legacy" CLI that bootstraps `core` which in turn creates the "legacy" Kibana server. +At the moment `core` manages HTTP connections, handles TLS configuration and base path proxy. All requests to Kibana server +will hit HTTP server exposed by the `core` first and it will decide whether request can be solely handled by the new +platform or request should be proxied to the "legacy" Kibana. This setup allows `core` to gradually introduce any "pre-route" +processing logic, expose new routes or replace old ones handled by the "legacy" Kibana currently. -Once config has been loaded and validated by the "legacy" Kibana it's passed to the `core` where some of its parts will -be additionally validated so that we can make config validation stricter with the new config validation system. Even though -the new validation system provided by the `core` is also based on Joi internally it is complemented with custom rules -tailored to our needs (e.g. `byteSize`, `duration` etc.). That means that config values that are accepted by the "legacy" -Kibana may be rejected by the `core`. - -One can also define new configuration keys under `__newPlatform` if these keys are supposed to be used by the `core` only -and should not be validated by the "legacy" Kibana, e.g. - -```yaml -__newPlatform: - plugins: - scanDirs: ['./example_plugins'] -``` +Once config has been loaded and some of its parts were validated by the `core` it's passed to the "legacy" Kibana where +it will be additionally validated so that we can make config validation stricter with the new config validation system. +Even though the new validation system provided by the `core` is also based on Joi internally it is complemented with custom +rules tailored to our needs (e.g. `byteSize`, `duration` etc.). That means that config values that were previously accepted +by the "legacy" Kibana may be rejected by the `core` now. Even though `core` has its own logging system it doesn't output log records directly (e.g. to file or terminal), but instead forward them to the "legacy" Kibana so that they look the same as the rest of the log records throughout Kibana. diff --git a/src/core/index.ts b/src/core/index.ts deleted file mode 100644 index 326d08e0ec43f..0000000000000 --- a/src/core/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { injectIntoKbnServer, createBasePathProxy } from './server/legacy_compat'; diff --git a/src/core/server/__snapshots__/index.test.ts.snap b/src/core/server/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000000..8c3022a07d074 --- /dev/null +++ b/src/core/server/__snapshots__/index.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`does not fail on "start" if there are unused paths detected: unused paths logs 1`] = ` +Object { + "debug": Array [ + Array [ + "starting server", + ], + ], + "error": Array [], + "fatal": Array [], + "info": Array [], + "log": Array [], + "trace": Array [ + Array [ + "some config paths are not handled by the core: [\\"some.path\\",\\"another.path\\"]", + ], + ], + "warn": Array [], +} +`; diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts new file mode 100644 index 0000000000000..69b1d751010c9 --- /dev/null +++ b/src/core/server/bootstrap.ts @@ -0,0 +1,113 @@ +/* + * 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 { isMaster } from 'cluster'; +import { CliArgs, Env, RawConfigService } from './config'; +import { LegacyObjectToConfigAdapter } from './legacy_compat'; +import { Root } from './root'; + +interface KibanaFeatures { + // Indicates whether we can run Kibana in a so called cluster mode in which + // Kibana is run as a "worker" process together with optimizer "worker" process + // that are orchestrated by the "master" process (dev mode only feature). + isClusterModeSupported: boolean; + + // Indicates whether we can run Kibana without X-Pack plugin pack even if it's + // installed (dev mode only feature). + isOssModeSupported: boolean; + + // Indicates whether we can run Kibana in REPL mode (dev mode only feature). + isReplModeSupported: boolean; + + // Indicates whether X-Pack plugin pack is installed and available. + isXPackInstalled: boolean; +} + +interface BootstrapArgs { + configs: string[]; + cliArgs: CliArgs; + applyConfigOverrides: (config: Record) => Record; + features: KibanaFeatures; +} + +export async function bootstrap({ + configs, + cliArgs, + applyConfigOverrides, + features, +}: BootstrapArgs) { + if (cliArgs.repl && !features.isReplModeSupported) { + onRootShutdown('Kibana REPL mode can only be run in development mode.'); + } + + const env = Env.createDefault({ + configs, + cliArgs, + isDevClusterMaster: isMaster && cliArgs.dev && features.isClusterModeSupported, + }); + + const rawConfigService = new RawConfigService( + env.configs, + rawConfig => new LegacyObjectToConfigAdapter(applyConfigOverrides(rawConfig)) + ); + + rawConfigService.loadConfig(); + + const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown); + + function shutdown(reason?: Error) { + rawConfigService.stop(); + return root.shutdown(reason); + } + + try { + await root.start(); + } catch (err) { + await shutdown(err); + } + + process.on('SIGHUP', () => { + const cliLogger = root.logger.get('cli'); + cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] }); + + try { + rawConfigService.reloadConfig(); + } catch (err) { + return shutdown(err); + } + + cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] }); + }); + + process.on('SIGINT', () => shutdown()); + process.on('SIGTERM', () => shutdown()); +} + +function onRootShutdown(reason?: any) { + if (reason !== undefined) { + // There is a chance that logger wasn't configured properly and error that + // that forced root to shut down could go unnoticed. To prevent this we always + // mirror such fatal errors in standard output with `console.error`. + // tslint:disable no-console + console.error(`\n${chalk.white.bgRed(' FATAL ')} ${reason}\n`); + } + + process.exit(reason === undefined ? 0 : (reason as any).processExitCode || 1); +} diff --git a/src/core/server/config/__tests__/__mocks__/env.ts b/src/core/server/config/__tests__/__mocks__/env.ts index fe33fd32f4648..e90c33f19ee49 100644 --- a/src/core/server/config/__tests__/__mocks__/env.ts +++ b/src/core/server/config/__tests__/__mocks__/env.ts @@ -21,11 +21,20 @@ import { EnvOptions } from '../../env'; -export function getEnvOptions(options: Partial = {}): EnvOptions { +type DeepPartial = { + [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial +}; + +export function getEnvOptions(options: DeepPartial = {}): EnvOptions { return { configs: options.configs || [], cliArgs: { dev: true, + quiet: false, + silent: false, + watch: false, + repl: false, + basePath: false, ...(options.cliArgs || {}), }, isDevClusterMaster: diff --git a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap b/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap index db2917da5406f..5931b0697d79c 100644 --- a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap +++ b/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap @@ -1,12 +1,85 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`correctly creates default environment if \`--env.name\` is supplied.: dev env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "basePath": false, + "dev": true, + "envName": "development", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": false, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + +exports[`correctly creates default environment if \`--env.name\` is supplied.: prod env properties 1`] = ` +Env { + "binDir": "/test/cwd/bin", + "cliArgs": Object { + "basePath": false, + "dev": false, + "envName": "production", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": "/test/cwd/config", + "configs": Array [ + "/some/other/path/some-kibana.yml", + ], + "corePluginsDir": "/test/cwd/core_plugins", + "homeDir": "/test/cwd", + "isDevClusterMaster": false, + "logDir": "/test/cwd/log", + "mode": Object { + "dev": false, + "name": "production", + "prod": true, + }, + "packageInfo": Object { + "branch": "feature-v1", + "buildNum": 9007199254740991, + "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "version": "v1", + }, + "staticFilesDir": "/test/cwd/ui", +} +`; + exports[`correctly creates default environment in dev mode.: env properties 1`] = ` Env { "binDir": "/test/cwd/bin", "cliArgs": Object { + "basePath": false, "dev": true, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/test/cwd/config", "configs": Array [ @@ -15,12 +88,6 @@ Env { "corePluginsDir": "/test/cwd/core_plugins", "homeDir": "/test/cwd", "isDevClusterMaster": true, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/test/cwd/log", "mode": Object { "dev": true, @@ -41,9 +108,12 @@ exports[`correctly creates default environment in prod distributable mode.: env Env { "binDir": "/test/cwd/bin", "cliArgs": Object { + "basePath": false, "dev": false, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/test/cwd/config", "configs": Array [ @@ -52,12 +122,6 @@ Env { "corePluginsDir": "/test/cwd/core_plugins", "homeDir": "/test/cwd", "isDevClusterMaster": false, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/test/cwd/log", "mode": Object { "dev": false, @@ -78,9 +142,12 @@ exports[`correctly creates default environment in prod non-distributable mode.: Env { "binDir": "/test/cwd/bin", "cliArgs": Object { + "basePath": false, "dev": false, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/test/cwd/config", "configs": Array [ @@ -89,12 +156,6 @@ Env { "corePluginsDir": "/test/cwd/core_plugins", "homeDir": "/test/cwd", "isDevClusterMaster": false, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/test/cwd/log", "mode": Object { "dev": false, @@ -115,9 +176,12 @@ exports[`correctly creates environment with constructor.: env properties 1`] = ` Env { "binDir": "/some/home/dir/bin", "cliArgs": Object { + "basePath": false, "dev": false, - "someArg": 1, - "someOtherArg": "2", + "quiet": false, + "repl": false, + "silent": false, + "watch": false, }, "configDir": "/some/home/dir/config", "configs": Array [ @@ -126,12 +190,6 @@ Env { "corePluginsDir": "/some/home/dir/core_plugins", "homeDir": "/some/home/dir", "isDevClusterMaster": false, - "legacy": EventEmitter { - "_events": Object {}, - "_eventsCount": 0, - "_maxListeners": undefined, - "domain": null, - }, "logDir": "/some/home/dir/log", "mode": Object { "dev": false, diff --git a/src/core/server/config/__tests__/apply_argv.test.ts b/src/core/server/config/__tests__/apply_argv.test.ts index 7908dd2468021..b3d2f3749271a 100644 --- a/src/core/server/config/__tests__/apply_argv.test.ts +++ b/src/core/server/config/__tests__/apply_argv.test.ts @@ -22,7 +22,7 @@ import { Config, ObjectToConfigAdapter } from '..'; /** * Overrides some config values with ones from argv. * - * @param config `RawConfig` instance to update config values for. + * @param config `Config` instance to update config values for. * @param argv Argv object with key/value pairs. */ export function overrideConfigWithArgv(config: Config, argv: { [key: string]: any }) { diff --git a/src/core/server/config/__tests__/env.test.ts b/src/core/server/config/__tests__/env.test.ts index 26163c82c8464..381273a1f8ffb 100644 --- a/src/core/server/config/__tests__/env.test.ts +++ b/src/core/server/config/__tests__/env.test.ts @@ -33,6 +33,7 @@ const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[ jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); import { Env } from '../env'; +import { getEnvOptions } from './__mocks__/env'; test('correctly creates default environment in dev mode.', () => { mockPackage.raw = { @@ -40,11 +41,12 @@ test('correctly creates default environment in dev mode.', () => { version: 'some-version', }; - const defaultEnv = Env.createDefault({ - cliArgs: { dev: true, someArg: 1, someOtherArg: '2' }, - configs: ['/test/cwd/config/kibana.yml'], - isDevClusterMaster: true, - }); + const defaultEnv = Env.createDefault( + getEnvOptions({ + configs: ['/test/cwd/config/kibana.yml'], + isDevClusterMaster: true, + }) + ); expect(defaultEnv).toMatchSnapshot('env properties'); }); @@ -60,11 +62,12 @@ test('correctly creates default environment in prod distributable mode.', () => }, }; - const defaultEnv = Env.createDefault({ - cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, - configs: ['/some/other/path/some-kibana.yml'], - isDevClusterMaster: false, - }); + const defaultEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); expect(defaultEnv).toMatchSnapshot('env properties'); }); @@ -80,15 +83,45 @@ test('correctly creates default environment in prod non-distributable mode.', () }, }; - const defaultEnv = Env.createDefault({ - cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, - configs: ['/some/other/path/some-kibana.yml'], - isDevClusterMaster: false, - }); + const defaultEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); expect(defaultEnv).toMatchSnapshot('env properties'); }); +test('correctly creates default environment if `--env.name` is supplied.', () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: false, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; + + const defaultDevEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { envName: 'development' }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); + + const defaultProdEnv = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false, envName: 'production' }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); + + expect(defaultDevEnv).toMatchSnapshot('dev env properties'); + expect(defaultProdEnv).toMatchSnapshot('prod env properties'); +}); + test('correctly creates environment with constructor.', () => { mockPackage.raw = { branch: 'feature-v1', @@ -100,11 +133,13 @@ test('correctly creates environment with constructor.', () => { }, }; - const env = new Env('/some/home/dir', { - cliArgs: { dev: false, someArg: 1, someOtherArg: '2' }, - configs: ['/some/other/path/some-kibana.yml'], - isDevClusterMaster: false, - }); + const env = new Env( + '/some/home/dir', + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + }) + ); expect(env).toMatchSnapshot('env properties'); }); diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index 56d6c1ae94a0c..f7b497403a28a 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -17,7 +17,6 @@ * under the License. */ -import { EventEmitter } from 'events'; import { resolve } from 'path'; import process from 'process'; @@ -38,10 +37,20 @@ interface EnvironmentMode { export interface EnvOptions { configs: string[]; - cliArgs: Record; + cliArgs: CliArgs; isDevClusterMaster: boolean; } +export interface CliArgs { + dev: boolean; + envName?: string; + quiet: boolean; + silent: boolean; + watch: boolean; + repl: boolean; + basePath: boolean; +} + export class Env { /** * @internal @@ -66,15 +75,10 @@ export class Env { */ public readonly mode: Readonly; - /** - * @internal - */ - public readonly legacy: EventEmitter; - /** * Arguments provided through command line. */ - public readonly cliArgs: Readonly>; + public readonly cliArgs: Readonly; /** * Paths to the configuration files. @@ -100,10 +104,11 @@ export class Env { this.configs = Object.freeze(options.configs); this.isDevClusterMaster = options.isDevClusterMaster; + const isDevMode = this.cliArgs.dev || this.cliArgs.envName === 'development'; this.mode = Object.freeze({ - dev: this.cliArgs.dev, - name: this.cliArgs.dev ? 'development' : 'production', - prod: !this.cliArgs.dev, + dev: isDevMode, + name: isDevMode ? 'development' : 'production', + prod: !isDevMode, }); const isKibanaDistributable = pkg.build && pkg.build.distributable === true; @@ -113,7 +118,5 @@ export class Env { buildSha: isKibanaDistributable ? pkg.build.sha : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', version: pkg.version, }); - - this.legacy = new EventEmitter(); } } diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index a5f535d12db1e..c63a8d6aa7c04 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -22,6 +22,6 @@ export { RawConfigService } from './raw_config_service'; export { Config, ConfigPath } from './config'; /** @internal */ export { ObjectToConfigAdapter } from './object_to_config_adapter'; -export { Env } from './env'; +export { Env, CliArgs } from './env'; export { ConfigWithSchema } from './config_with_schema'; export { getConfigFromFiles } from './read_config'; diff --git a/src/core/server/config/schema/byte_size_value/index.ts b/src/core/server/config/schema/byte_size_value/index.ts index 61ba879a5c926..fb0105503a149 100644 --- a/src/core/server/config/schema/byte_size_value/index.ts +++ b/src/core/server/config/schema/byte_size_value/index.ts @@ -36,8 +36,7 @@ export class ByteSizeValue { const match = /([1-9][0-9]*)(b|kb|mb|gb)/.exec(text); if (!match) { throw new Error( - `could not parse byte size value [${text}]. value must start with a ` + - `number and end with bytes size unit, e.g. 10kb, 23mb, 3gb, 239493b` + `could not parse byte size value [${text}]. Value must be a safe positive integer.` ); } diff --git a/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap index 6c38ae7ecf5d6..d7fe10b1c417b 100644 --- a/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap @@ -11,6 +11,7 @@ Object { exports[`has defaults for config 1`] = ` Object { + "autoListen": true, "cors": false, "host": "localhost", "maxPayload": ByteSizeValue { diff --git a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap b/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap index 3060d7b468960..8e868e803602f 100644 --- a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap +++ b/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`broadcasts server and connection options to the legacy "channel" 1`] = ` +exports[`returns server and connection options on start 1`] = ` Object { "host": "127.0.0.1", "port": 12345, diff --git a/src/core/server/http/__tests__/http_server.test.ts b/src/core/server/http/__tests__/http_server.test.ts index 7f49d153163a9..42f93a13e1c80 100644 --- a/src/core/server/http/__tests__/http_server.test.ts +++ b/src/core/server/http/__tests__/http_server.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; +import { Server } from 'http'; jest.mock('fs', () => ({ readFileSync: jest.fn(), @@ -26,7 +26,6 @@ jest.mock('fs', () => ({ import Chance from 'chance'; import supertest from 'supertest'; -import { Env } from '../../config'; import { ByteSizeValue } from '../../config/schema'; import { logger } from '../../logging/__mocks__'; import { HttpConfig } from '../http_config'; @@ -35,14 +34,9 @@ import { Router } from '../router'; const chance = new Chance(); -let env: Env; let server: HttpServer; let config: HttpConfig; -function getServerListener(httpServer: HttpServer) { - return (httpServer as any).server.listener; -} - beforeEach(() => { config = { host: '127.0.0.1', @@ -51,8 +45,7 @@ beforeEach(() => { ssl: {}, } as HttpConfig; - env = new Env('/kibana', getEnvOptions()); - server = new HttpServer(logger.get(), env); + server = new HttpServer(logger.get()); }); afterEach(async () => { @@ -77,9 +70,9 @@ test('200 OK with body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(200) .then(res => { @@ -96,9 +89,9 @@ test('202 Accepted with body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(202) .then(res => { @@ -115,9 +108,9 @@ test('204 No content', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(204) .then(res => { @@ -136,9 +129,9 @@ test('400 Bad request with error', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/') .expect(400) .then(res => { @@ -165,9 +158,9 @@ test('valid params', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/some-string') .expect(200) .then(res => { @@ -194,9 +187,9 @@ test('invalid params', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/some-string') .expect(400) .then(res => { @@ -226,9 +219,9 @@ test('valid query', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/?bar=test&quux=123') .expect(200) .then(res => { @@ -255,9 +248,9 @@ test('invalid query', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/?bar=test') .expect(400) .then(res => { @@ -287,9 +280,9 @@ test('valid body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .post('/foo/') .send({ bar: 'test', @@ -320,9 +313,9 @@ test('invalid body', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .post('/foo/') .send({ bar: 'test' }) .expect(400) @@ -352,9 +345,9 @@ test('handles putting', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .put('/foo/') .send({ key: 'new value' }) .expect(200) @@ -382,9 +375,9 @@ test('handles deleting', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .delete('/foo/3') .expect(200) .then(res => { @@ -407,9 +400,9 @@ test('filtered headers', async () => { server.registerRouter(router); - await server.start(config); + const { server: innerServer } = await server.start(config); - await supertest(getServerListener(server)) + await supertest(innerServer.listener) .get('/foo/?bar=quux') .set('x-kibana-foo', 'bar') .set('x-kibana-bar', 'quux'); @@ -422,6 +415,7 @@ test('filtered headers', async () => { describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { let configWithBasePath: HttpConfig; + let innerServerListener: Server; beforeEach(async () => { configWithBasePath = { @@ -438,29 +432,30 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { server.registerRouter(router); - await server.start(configWithBasePath); + const { server: innerServer } = await server.start(configWithBasePath); + innerServerListener = innerServer.listener; }); test('/bar => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar') .expect(404); }); test('/bar/ => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/') .expect(404); }); test('/bar/foo => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/foo') .expect(404); }); test('/ => /', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/') .expect(200) .then(res => { @@ -469,7 +464,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { }); test('/foo => /foo', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/foo') .expect(200) .then(res => { @@ -480,6 +475,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { let configWithBasePath: HttpConfig; + let innerServerListener: Server; beforeEach(async () => { configWithBasePath = { @@ -496,11 +492,12 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { server.registerRouter(router); - await server.start(configWithBasePath); + const { server: innerServer } = await server.start(configWithBasePath); + innerServerListener = innerServer.listener; }); test('/bar => /', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar') .expect(200) .then(res => { @@ -509,7 +506,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('/bar/ => /', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/') .expect(200) .then(res => { @@ -518,7 +515,7 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('/bar/foo => /foo', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/bar/foo') .expect(200) .then(res => { @@ -527,13 +524,13 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('/ => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/') .expect(404); }); test('/foo => 404', async () => { - await supertest(getServerListener(server)) + await supertest(innerServerListener) .get('/foo') .expect(404); }); @@ -564,21 +561,13 @@ describe('with defined `redirectHttpFromPort`', () => { }); }); -test('broadcasts server and connection options to the legacy "channel"', async () => { - const onConnectionListener = jest.fn(); - env.legacy.on('connection', onConnectionListener); - - expect(onConnectionListener).not.toHaveBeenCalled(); - - await server.start({ +test('returns server and connection options on start', async () => { + const { server: innerServer, options } = await server.start({ ...config, port: 12345, }); - expect(onConnectionListener).toHaveBeenCalledTimes(1); - - const [[{ options, server: rawServer }]] = onConnectionListener.mock.calls; - expect(rawServer).toBeDefined(); - expect(rawServer).toBe((server as any).server); + expect(innerServer).toBeDefined(); + expect(innerServer).toBe((server as any).server); expect(options).toMatchSnapshot(); }); diff --git a/src/core/server/http/__tests__/http_service.test.ts b/src/core/server/http/__tests__/http_service.test.ts index 0cacad8817468..1c6d259848117 100644 --- a/src/core/server/http/__tests__/http_service.test.ts +++ b/src/core/server/http/__tests__/http_service.test.ts @@ -17,8 +17,6 @@ * under the License. */ -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; - const mockHttpServer = jest.fn(); jest.mock('../http_server', () => ({ @@ -27,8 +25,6 @@ jest.mock('../http_server', () => ({ import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; - -import { Env } from '../../config'; import { logger } from '../../logging/__mocks__'; import { HttpConfig } from '../http_config'; import { HttpService } from '../http_service'; @@ -55,11 +51,7 @@ test('creates and starts http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); expect(mockHttpServer.mock.instances.length).toBe(1); expect(httpServer.start).not.toHaveBeenCalled(); @@ -81,11 +73,7 @@ test('logs error if already started', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); await service.start(); @@ -104,11 +92,7 @@ test('stops http server', async () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); await service.start(); @@ -132,11 +116,7 @@ test('register route handler', () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); const router = new Router('/foo'); service.registerRouter(router); @@ -159,11 +139,7 @@ test('throws if registering route handler after http server is started', () => { }; mockHttpServer.mockImplementation(() => httpServer); - const service = new HttpService( - config$.asObservable(), - logger, - new Env('/kibana', getEnvOptions()) - ); + const service = new HttpService(config$.asObservable(), logger); const router = new Router('/foo'); service.registerRouter(router); @@ -171,3 +147,20 @@ test('throws if registering route handler after http server is started', () => { expect(httpServer.registerRouter).toHaveBeenCalledTimes(0); expect(logger.mockCollect()).toMatchSnapshot(); }); + +test('returns http server contract on start', async () => { + const httpServerContract = { + server: {}, + options: { someOption: true }, + }; + + mockHttpServer.mockImplementation(() => ({ + isListening: () => false, + start: jest.fn().mockReturnValue(httpServerContract), + stop: noop, + })); + + const service = new HttpService(new BehaviorSubject({ ssl: {} } as HttpConfig), logger); + + expect(await service.start()).toBe(httpServerContract); +}); diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index f4a9b59b77b10..b0c2144d7189a 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -29,8 +29,6 @@ import { createServer, getServerOptions } from './http_tools'; const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); export interface BasePathProxyServerOptions { - httpConfig: HttpConfig; - devConfig: DevConfig; shouldRedirectFromOldBasePath: (path: string) => boolean; blockUntil: () => Promise; } @@ -40,34 +38,38 @@ export class BasePathProxyServer { private httpsAgent?: HttpsAgent; get basePath() { - return this.options.httpConfig.basePath; + return this.httpConfig.basePath; } get targetPort() { - return this.options.devConfig.basePathProxyTargetPort; + return this.devConfig.basePathProxyTargetPort; } - constructor(private readonly log: Logger, private readonly options: BasePathProxyServerOptions) { + constructor( + private readonly log: Logger, + private readonly httpConfig: HttpConfig, + private readonly devConfig: DevConfig + ) { const ONE_GIGABYTE = 1024 * 1024 * 1024; - options.httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); + httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE); - if (!options.httpConfig.basePath) { - options.httpConfig.basePath = `/${sample(alphabet, 3).join('')}`; + if (!httpConfig.basePath) { + httpConfig.basePath = `/${sample(alphabet, 3).join('')}`; } } - public async start() { - const { httpConfig } = this.options; + public async start(options: Readonly) { + this.log.debug('starting basepath proxy server'); - const options = getServerOptions(httpConfig); - this.server = createServer(options); + const serverOptions = getServerOptions(this.httpConfig); + this.server = createServer(serverOptions); // Register hapi plugin that adds proxying functionality. It can be configured // through the route configuration object (see { handler: { proxy: ... } }). await this.server.register({ plugin: require('h2o2-latest') }); - if (httpConfig.ssl.enabled) { - const tlsOptions = options.tls as TlsOptions; + if (this.httpConfig.ssl.enabled) { + const tlsOptions = serverOptions.tls as TlsOptions; this.httpsAgent = new HttpsAgent({ ca: tlsOptions.ca, cert: tlsOptions.cert, @@ -77,40 +79,42 @@ export class BasePathProxyServer { }); } - this.setupRoutes(); + this.setupRoutes(options); + + await this.server.start(); this.log.info( - `starting basepath proxy server at ${this.server.info.uri}${httpConfig.basePath}` + `basepath proxy server running at ${this.server.info.uri}${this.httpConfig.basePath}` ); - - await this.server.start(); } public async stop() { - this.log.info('stopping basepath proxy server'); - - if (this.server !== undefined) { - await this.server.stop(); - this.server = undefined; + if (this.server === undefined) { + return; } + this.log.debug('stopping basepath proxy server'); + await this.server.stop(); + this.server = undefined; + if (this.httpsAgent !== undefined) { this.httpsAgent.destroy(); this.httpsAgent = undefined; } } - private setupRoutes() { + private setupRoutes({ + blockUntil, + shouldRedirectFromOldBasePath, + }: Readonly) { if (this.server === undefined) { throw new Error(`Routes cannot be set up since server is not initialized.`); } - const { httpConfig, devConfig, blockUntil, shouldRedirectFromOldBasePath } = this.options; - // Always redirect from root URL to the URL with basepath. this.server.route({ handler: (request, responseToolkit) => { - return responseToolkit.redirect(httpConfig.basePath); + return responseToolkit.redirect(this.httpConfig.basePath); }, method: 'GET', path: '/', @@ -122,7 +126,7 @@ export class BasePathProxyServer { agent: this.httpsAgent, host: this.server.info.host, passThrough: true, - port: devConfig.basePathProxyTargetPort, + port: this.devConfig.basePathProxyTargetPort, protocol: this.server.info.protocol, xforward: true, }, @@ -138,7 +142,7 @@ export class BasePathProxyServer { }, ], }, - path: `${httpConfig.basePath}/{kbnPath*}`, + path: `${this.httpConfig.basePath}/{kbnPath*}`, }); // It may happen that basepath has changed, but user still uses the old one, @@ -152,7 +156,7 @@ export class BasePathProxyServer { const isBasepathLike = oldBasePath.length === 3; return isGet && isBasepathLike && shouldRedirectFromOldBasePath(kbnPath) - ? responseToolkit.redirect(`${httpConfig.basePath}/${kbnPath}`) + ? responseToolkit.redirect(`${this.httpConfig.basePath}/${kbnPath}`) : responseToolkit.response('Not Found').code(404); }, method: '*', diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 5d1504008027b..67578ecc1559c 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -28,6 +28,7 @@ const match = (regex: RegExp, errorMsg: string) => (str: string) => const createHttpSchema = schema.object( { + autoListen: schema.boolean({ defaultValue: true }), basePath: schema.maybe( schema.string({ validate: match(validBasePathRegex, "must start with a slash, don't end with one"), @@ -90,6 +91,7 @@ export class HttpConfig { */ public static schema = createHttpSchema; + public autoListen: boolean; public host: string; public port: number; public cors: boolean | { origin: string[] }; @@ -103,6 +105,7 @@ export class HttpConfig { * @internal */ constructor(config: HttpConfigType, env: Env) { + this.autoListen = config.autoListen; this.host = config.host; this.port = config.port; this.cors = config.cors; diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 21cde147b8ea2..c828ff4df5408 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,20 +17,24 @@ * under the License. */ -import { Server } from 'hapi-latest'; +import { Server, ServerOptions } from 'hapi-latest'; import { modifyUrl } from '../../utils'; -import { Env } from '../config'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; import { createServer, getServerOptions } from './http_tools'; import { Router } from './router'; +export interface HttpServerInfo { + server: Server; + options: ServerOptions; +} + export class HttpServer { private server?: Server; private registeredRouters: Set = new Set(); - constructor(private readonly log: Logger, private readonly env: Env) {} + constructor(private readonly log: Logger) {} public isListening() { return this.server !== undefined && this.server.listener.listening; @@ -62,21 +66,18 @@ export class HttpServer { } } - // Notify legacy compatibility layer about HTTP(S) connection providing server - // instance with connection options so that we can properly bridge core and - // the "legacy" Kibana internally. - this.env.legacy.emit('connection', { - options: serverOptions, - server: this.server, - }); - await this.server.start(); - this.log.info( - `Server running at ${this.server.info.uri}${config.rewriteBasePath ? config.basePath : ''}`, - // The "legacy" Kibana will output log records with `listening` tag even if `quiet` logging mode is enabled. - { tags: ['listening'] } + this.log.debug( + `http server running at ${this.server.info.uri}${ + config.rewriteBasePath ? config.basePath : '' + }` ); + + // Return server instance with the connection options so that we can properly + // bridge core and the "legacy" Kibana internally. Once this bridge isn't + // needed anymore we shouldn't return anything from this method. + return { server: this.server, options: serverOptions }; } public async stop() { diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 3caae18e857b3..6972dfffbb1dd 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -21,24 +21,23 @@ import { Observable, Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreService } from '../../types/core_service'; -import { Env } from '../config'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; -import { HttpServer } from './http_server'; +import { HttpServer, HttpServerInfo } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; import { Router } from './router'; -export class HttpService implements CoreService { +export class HttpService implements CoreService { private readonly httpServer: HttpServer; private readonly httpsRedirectServer: HttpsRedirectServer; private configSubscription?: Subscription; private readonly log: Logger; - constructor(private readonly config$: Observable, logger: LoggerFactory, env: Env) { + constructor(private readonly config$: Observable, logger: LoggerFactory) { this.log = logger.get('http'); - this.httpServer = new HttpServer(logger.get('http', 'server'), env); + this.httpServer = new HttpServer(logger.get('http', 'server')); this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } @@ -61,7 +60,7 @@ export class HttpService implements CoreService { await this.httpsRedirectServer.start(config); } - await this.httpServer.start(config); + return await this.httpServer.start(config); } public async stop() { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index e636fcd801eb5..3fd3715083416 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,20 +19,26 @@ import { Observable } from 'rxjs'; -import { Env } from '../config'; import { LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; import { HttpService } from './http_service'; +import { Router } from './router'; export { Router, KibanaRequest } from './router'; export { HttpService }; +export { HttpServerInfo } from './http_server'; +export { BasePathProxyServer } from './base_path_proxy_server'; export { HttpConfig }; export class HttpModule { public readonly service: HttpService; - constructor(readonly config$: Observable, logger: LoggerFactory, env: Env) { - this.service = new HttpService(this.config$, logger, env); + constructor(readonly config$: Observable, logger: LoggerFactory) { + this.service = new HttpService(this.config$, logger); + + const router = new Router('/core'); + router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' })); + this.service.registerRouter(router); } } diff --git a/src/core/server/index.test.ts b/src/core/server/index.test.ts new file mode 100644 index 0000000000000..8a83d8d500b81 --- /dev/null +++ b/src/core/server/index.test.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. + */ + +const mockHttpService = { start: jest.fn(), stop: jest.fn(), registerRouter: jest.fn() }; +jest.mock('./http/http_service', () => ({ + HttpService: jest.fn(() => mockHttpService), +})); + +const mockLegacyService = { start: jest.fn(), stop: jest.fn() }; +jest.mock('./legacy_compat/legacy_service', () => ({ + LegacyService: jest.fn(() => mockLegacyService), +})); + +import { BehaviorSubject } from 'rxjs'; +import { Server } from '.'; +import { Env } from './config'; +import { getEnvOptions } from './config/__tests__/__mocks__/env'; +import { logger } from './logging/__mocks__'; + +const mockConfigService = { atPath: jest.fn(), getUnusedPaths: jest.fn().mockReturnValue([]) }; +const env = new Env('.', getEnvOptions()); + +beforeEach(() => { + mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); +}); + +afterEach(() => { + logger.mockClear(); + mockConfigService.atPath.mockReset(); + mockHttpService.start.mockReset(); + mockHttpService.stop.mockReset(); + mockLegacyService.start.mockReset(); + mockLegacyService.stop.mockReset(); +}); + +test('starts services on "start"', async () => { + const mockHttpServiceStartContract = { something: true }; + mockHttpService.start.mockReturnValue(Promise.resolve(mockHttpServiceStartContract)); + + const server = new Server(mockConfigService as any, logger, env); + + expect(mockHttpService.start).not.toHaveBeenCalled(); + expect(mockLegacyService.start).not.toHaveBeenCalled(); + + await server.start(); + + expect(mockHttpService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledWith(mockHttpServiceStartContract); +}); + +test('does not fail on "start" if there are unused paths detected', async () => { + mockConfigService.getUnusedPaths.mockReturnValue(['some.path', 'another.path']); + + const server = new Server(mockConfigService as any, logger, env); + await expect(server.start()).resolves.toBeUndefined(); + expect(logger.mockCollect()).toMatchSnapshot('unused paths logs'); +}); + +test('does not start http service is `autoListen:false`', async () => { + mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); + + const server = new Server(mockConfigService as any, logger, env); + + expect(mockLegacyService.start).not.toHaveBeenCalled(); + + await server.start(); + + expect(mockHttpService.start).not.toHaveBeenCalled(); + expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledWith(undefined); +}); + +test('does not start http service if process is dev cluster master', async () => { + const server = new Server( + mockConfigService as any, + logger, + new Env('.', getEnvOptions({ isDevClusterMaster: true })) + ); + + expect(mockLegacyService.start).not.toHaveBeenCalled(); + + await server.start(); + + expect(mockHttpService.start).not.toHaveBeenCalled(); + expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockLegacyService.start).toHaveBeenCalledWith(undefined); +}); + +test('stops services on "stop"', async () => { + const mockHttpServiceStartContract = { something: true }; + mockHttpService.start.mockReturnValue(Promise.resolve(mockHttpServiceStartContract)); + + const server = new Server(mockConfigService as any, logger, env); + + await server.start(); + + expect(mockHttpService.stop).not.toHaveBeenCalled(); + expect(mockLegacyService.stop).not.toHaveBeenCalled(); + + await server.stop(); + + expect(mockHttpService.stop).toHaveBeenCalledTimes(1); + expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); +}); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 7d55670239f5e..ac645b2280041 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -17,29 +17,44 @@ * under the License. */ +export { bootstrap } from './bootstrap'; + +import { first } from 'rxjs/operators'; import { ConfigService, Env } from './config'; -import { HttpConfig, HttpModule, Router } from './http'; +import { HttpConfig, HttpModule, HttpServerInfo } from './http'; +import { LegacyCompatModule } from './legacy_compat'; import { Logger, LoggerFactory } from './logging'; export class Server { private readonly http: HttpModule; + private readonly legacy: LegacyCompatModule; private readonly log: Logger; - constructor(private readonly configService: ConfigService, logger: LoggerFactory, env: Env) { + constructor( + private readonly configService: ConfigService, + logger: LoggerFactory, + private readonly env: Env + ) { this.log = logger.get('server'); - const httpConfig$ = configService.atPath('server', HttpConfig); - this.http = new HttpModule(httpConfig$, logger, env); + this.http = new HttpModule(configService.atPath('server', HttpConfig), logger); + this.legacy = new LegacyCompatModule(configService, logger, env); } public async start() { - this.log.debug('starting server :tada:'); + this.log.debug('starting server'); - const router = new Router('/core'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' })); - this.http.service.registerRouter(router); + // We shouldn't start http service in two cases: + // 1. If `server.autoListen` is explicitly set to `false`. + // 2. When the process is run as dev cluster master in which case cluster manager + // will fork a dedicated process where http service will be started instead. + let httpServerInfo: HttpServerInfo | undefined; + const httpConfig = await this.http.config$.pipe(first()).toPromise(); + if (!this.env.isDevClusterMaster && httpConfig.autoListen) { + httpServerInfo = await this.http.service.start(); + } - await this.http.service.start(); + await this.legacy.service.start(httpServerInfo); const unhandledConfigPaths = await this.configService.getUnusedPaths(); if (unhandledConfigPaths.length > 0) { @@ -54,6 +69,7 @@ export class Server { public async stop() { this.log.debug('stopping server'); + await this.legacy.service.stop(); await this.http.service.stop(); } } diff --git a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap deleted file mode 100644 index eb58ca8cbc5fd..0000000000000 --- a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_platform_proxifier.test.ts.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`correctly binds to the server.: proxy route options 1`] = ` -Array [ - Array [ - Object { - "handler": [Function], - "method": "*", - "options": Object { - "payload": Object { - "maxBytes": 9007199254740991, - "output": "stream", - "parse": false, - "timeout": false, - }, - }, - "path": "/{p*}", - }, - ], -] -`; diff --git a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_service.test.ts.snap new file mode 100644 index 0000000000000..4e10f1449d1cf --- /dev/null +++ b/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_service.test.ts.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`once LegacyService is started in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: cluster manager with base path proxy 1`] = ` +Array [ + Array [ + Object { + "basePath": true, + "dev": true, + "quiet": true, + "repl": false, + "silent": false, + "watch": false, + }, + Object { + "server": Object { + "autoListen": true, + }, + }, + BasePathProxyServer { + "devConfig": Object { + "basePathProxyTargetPort": 100500, + }, + "httpConfig": Object { + "basePath": "/abc", + "maxPayload": ByteSizeValue { + "valueInBytes": 1073741824, + }, + }, + "log": Object { + "debug": [MockFunction] { + "calls": Array [ + Array [ + "starting legacy service", + ], + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, + }, + ], +] +`; + +exports[`once LegacyService is started in \`devClusterMaster\` mode creates ClusterManager without base path proxy.: cluster manager without base path proxy 1`] = ` +Array [ + Array [ + Object { + "basePath": false, + "dev": true, + "quiet": false, + "repl": false, + "silent": true, + "watch": false, + }, + Object { + "server": Object { + "autoListen": true, + }, + }, + undefined, + ], +] +`; + +exports[`once LegacyService is started with connection info creates legacy kbnServer and closes it if \`listen\` fails. 1`] = `"something failed"`; + +exports[`once LegacyService is started with connection info proxy route responds with \`503\` if \`kbnServer\` is not ready yet.: 503 response 1`] = ` +Object { + "body": Array [ + Array [ + "Kibana server is not ready yet", + ], + ], + "code": Array [ + Array [ + 503, + ], + ], + "header": Array [ + Array [ + "Retry-After", + "30", + ], + ], +} +`; + +exports[`once LegacyService is started with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` +Array [ + Array [ + Object { + "logging": Object { + "verbose": true, + }, + }, + ], +] +`; + +exports[`once LegacyService is started with connection info register proxy route.: proxy route options 1`] = ` +Array [ + Array [ + Object { + "handler": [Function], + "method": "*", + "options": Object { + "payload": Object { + "maxBytes": 9007199254740991, + "output": "stream", + "parse": false, + "timeout": false, + }, + }, + "path": "/{p*}", + }, + ], +] +`; + +exports[`once LegacyService is started with connection info throws if fails to retrieve initial config. 1`] = `"something failed"`; + +exports[`once LegacyService is started without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` +Array [ + Array [ + Object { + "logging": Object { + "verbose": true, + }, + }, + ], +] +`; diff --git a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts b/src/core/server/legacy_compat/__tests__/legacy_platform_proxy.test.ts similarity index 51% rename from src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts rename to src/core/server/legacy_compat/__tests__/legacy_platform_proxy.test.ts index 27db835a0ecf3..8330bbb8d74db 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_platform_proxifier.test.ts +++ b/src/core/server/legacy_compat/__tests__/legacy_platform_proxy.test.ts @@ -17,17 +17,12 @@ * under the License. */ -import { Server as HapiServer } from 'hapi-latest'; import { Server } from 'net'; -import { LegacyPlatformProxifier } from '..'; -import { Env } from '../../config'; -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; -import { logger } from '../../logging/__mocks__'; + +import { LegacyPlatformProxy } from '../legacy_platform_proxy'; let server: jest.Mocked; -let mockHapiServer: jest.Mocked; -let root: any; -let proxifier: LegacyPlatformProxifier; +let proxy: LegacyPlatformProxy; beforeEach(() => { server = { addListener: jest.fn(), @@ -36,29 +31,7 @@ beforeEach(() => { .mockReturnValue({ port: 1234, family: 'test-family', address: 'test-address' }), getConnections: jest.fn(), } as any; - - mockHapiServer = { listener: server, route: jest.fn() } as any; - - root = { - logger, - shutdown: jest.fn(), - start: jest.fn(), - } as any; - - const env = new Env('/kibana', getEnvOptions()); - proxifier = new LegacyPlatformProxifier(root, env); - env.legacy.emit('connection', { - server: mockHapiServer, - options: { someOption: 'foo', someAnotherOption: 'bar' }, - }); -}); - -test('correctly binds to the server.', () => { - expect(mockHapiServer.route.mock.calls).toMatchSnapshot('proxy route options'); - expect(server.addListener).toHaveBeenCalledTimes(6); - for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) { - expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); - } + proxy = new LegacyPlatformProxy({ debug: jest.fn() } as any, server); }); test('correctly redirects server events.', () => { @@ -66,7 +39,7 @@ test('correctly redirects server events.', () => { expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); const listener = jest.fn(); - proxifier.addListener(eventName, listener); + proxy.addListener(eventName, listener); // Emit several events, to make sure that server is not being listened with `once`. const [, serverListener] = server.addListener.mock.calls.find( @@ -78,68 +51,47 @@ test('correctly redirects server events.', () => { expect(listener).toHaveBeenCalledTimes(2); expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); - expect(listener).toHaveBeenCalledWith(5, 6, 7, 8); - proxifier.removeListener(eventName, listener); + proxy.removeListener(eventName, listener); } }); test('returns `address` from the underlying server.', () => { - expect(proxifier.address()).toEqual({ + expect(proxy.address()).toEqual({ address: 'test-address', family: 'test-family', port: 1234, }); }); -test('`listen` starts the `root`.', async () => { +test('`listen` calls callback immediately.', async () => { const onListenComplete = jest.fn(); - await proxifier.listen(1234, 'host-1', onListenComplete); + await proxy.listen(1234, 'host-1', onListenComplete); - expect(root.start).toHaveBeenCalledTimes(1); expect(onListenComplete).toHaveBeenCalledTimes(1); }); -test('`close` shuts down the `root`.', async () => { +test('`close` calls callback immediately.', async () => { const onCloseComplete = jest.fn(); - await proxifier.close(onCloseComplete); + await proxy.close(onCloseComplete); - expect(root.shutdown).toHaveBeenCalledTimes(1); expect(onCloseComplete).toHaveBeenCalledTimes(1); }); test('returns connection count from the underlying server.', () => { server.getConnections.mockImplementation(callback => callback(null, 0)); const onGetConnectionsComplete = jest.fn(); - proxifier.getConnections(onGetConnectionsComplete); + proxy.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 0); onGetConnectionsComplete.mockReset(); server.getConnections.mockImplementation(callback => callback(null, 100500)); - proxifier.getConnections(onGetConnectionsComplete); + proxy.getConnections(onGetConnectionsComplete); expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 100500); }); - -test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { - const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; - const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; - - const onRequest = jest.fn(); - proxifier.addListener('request', onRequest); - - const [[{ handler }]] = mockHapiServer.route.mock.calls; - const response = await handler(mockRequest, mockResponseToolkit); - - expect(response).toBe(mockResponseToolkit.abandon); - expect(mockResponseToolkit.response).not.toHaveBeenCalled(); - - // Make sure request hasn't been passed to the legacy platform. - expect(onRequest).toHaveBeenCalledTimes(1); - expect(onRequest).toHaveBeenCalledWith(mockRequest.raw.req, mockRequest.raw.res); -}); diff --git a/src/core/server/legacy_compat/__tests__/legacy_service.test.ts b/src/core/server/legacy_compat/__tests__/legacy_service.test.ts new file mode 100644 index 0000000000000..dc16709861084 --- /dev/null +++ b/src/core/server/legacy_compat/__tests__/legacy_service.test.ts @@ -0,0 +1,339 @@ +/* + * 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 { BehaviorSubject, Subject, throwError } from 'rxjs'; + +jest.mock('../legacy_platform_proxy'); +jest.mock('../../../../server/kbn_server'); +jest.mock('../../../../cli/cluster/cluster_manager'); + +import { first } from 'rxjs/operators'; +// @ts-ignore: implicit any for JS file +import MockClusterManager from '../../../../cli/cluster/cluster_manager'; +// @ts-ignore: implicit any for JS file +import MockKbnServer from '../../../../server/kbn_server'; +import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config'; +import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; +import { logger } from '../../logging/__mocks__'; +import { LegacyPlatformProxy } from '../legacy_platform_proxy'; +import { LegacyService } from '../legacy_service'; + +const MockLegacyPlatformProxy: jest.Mock = LegacyPlatformProxy as any; + +let legacyService: LegacyService; +let configService: jest.Mocked; +let env: Env; +let mockHttpServerInfo: any; +let config$: BehaviorSubject; +beforeEach(() => { + env = Env.createDefault(getEnvOptions()); + + MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); + + mockHttpServerInfo = { + server: { listener: { addListener: jest.fn() }, route: jest.fn() }, + options: { someOption: 'foo', someAnotherOption: 'bar' }, + }; + + config$ = new BehaviorSubject( + new ObjectToConfigAdapter({ + server: { autoListen: true }, + }) + ); + + configService = { + getConfig$: jest.fn().mockReturnValue(config$), + atPath: jest.fn().mockReturnValue(new BehaviorSubject({})), + } as any; + legacyService = new LegacyService(env, logger, configService); +}); + +afterEach(() => { + MockLegacyPlatformProxy.mockClear(); + MockKbnServer.mockClear(); + MockClusterManager.create.mockClear(); + logger.mockClear(); +}); + +describe('once LegacyService is started with connection info', () => { + test('register proxy route.', async () => { + await legacyService.start(mockHttpServerInfo); + + expect(mockHttpServerInfo.server.route.mock.calls).toMatchSnapshot('proxy route options'); + }); + + test('proxy route responds with `503` if `kbnServer` is not ready yet.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + + const kbnServerListen$ = new Subject(); + MockKbnServer.prototype.listen = jest.fn(() => { + kbnServerListen$.next(); + return kbnServerListen$.toPromise(); + }); + + // Wait until listen is called and proxy route is registered, but don't allow + // listen to complete and make kbnServer available. + const legacyStartPromise = legacyService.start(mockHttpServerInfo); + await kbnServerListen$.pipe(first()).toPromise(); + + const mockResponse: any = { + code: jest.fn().mockImplementation(() => mockResponse), + header: jest.fn().mockImplementation(() => mockResponse), + }; + const mockResponseToolkit = { + response: jest.fn().mockReturnValue(mockResponse), + abandon: Symbol('abandon'), + }; + const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; + + const [[{ handler }]] = mockHttpServerInfo.server.route.mock.calls; + const response503 = await handler(mockRequest, mockResponseToolkit); + + expect(response503).toBe(mockResponse); + expect({ + body: mockResponseToolkit.response.mock.calls, + code: mockResponse.code.mock.calls, + header: mockResponse.header.mock.calls, + }).toMatchSnapshot('503 response'); + + // Make sure request hasn't been passed to the legacy platform. + const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances; + expect(mockedLegacyPlatformProxy.emit).not.toHaveBeenCalled(); + + // Now wait until kibana is ready and try to request once again. + kbnServerListen$.complete(); + await legacyStartPromise; + mockResponseToolkit.response.mockClear(); + + const responseProxy = await handler(mockRequest, mockResponseToolkit); + expect(responseProxy).toBe(mockResponseToolkit.abandon); + expect(mockResponseToolkit.response).not.toHaveBeenCalled(); + + // Make sure request has been passed to the legacy platform. + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1); + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith( + 'request', + mockRequest.raw.req, + mockRequest.raw.res + ); + }); + + test('creates legacy kbnServer and calls `listen`.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + + await legacyService.start(mockHttpServerInfo); + + expect(MockKbnServer).toHaveBeenCalledTimes(1); + expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, + { + serverOptions: { + listener: expect.any(LegacyPlatformProxy), + someAnotherOption: 'bar', + someOption: 'foo', + }, + } + ); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); + expect(mockKbnServer.close).not.toHaveBeenCalled(); + }); + + test('creates legacy kbnServer but does not call `listen` if `autoListen: false`.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: false })); + + await legacyService.start(mockHttpServerInfo); + + expect(MockKbnServer).toHaveBeenCalledTimes(1); + expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, + { + serverOptions: { + listener: expect.any(LegacyPlatformProxy), + someAnotherOption: 'bar', + someOption: 'foo', + }, + } + ); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); + expect(mockKbnServer.listen).not.toHaveBeenCalled(); + expect(mockKbnServer.close).not.toHaveBeenCalled(); + }); + + test('creates legacy kbnServer and closes it if `listen` fails.', async () => { + configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + MockKbnServer.prototype.listen.mockRejectedValue(new Error('something failed')); + + await expect(legacyService.start(mockHttpServerInfo)).rejects.toThrowErrorMatchingSnapshot(); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.listen).toHaveBeenCalled(); + expect(mockKbnServer.close).toHaveBeenCalled(); + }); + + test('throws if fails to retrieve initial config.', async () => { + configService.getConfig$.mockReturnValue(throwError(new Error('something failed'))); + + await expect(legacyService.start(mockHttpServerInfo)).rejects.toThrowErrorMatchingSnapshot(); + + expect(MockKbnServer).not.toHaveBeenCalled(); + expect(MockClusterManager).not.toHaveBeenCalled(); + }); + + test('reconfigures logging configuration if new config is received.', async () => { + await legacyService.start(mockHttpServerInfo); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + + config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + + expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( + `applyLoggingConfiguration params` + ); + }); + + test('logs error if re-configuring fails.', async () => { + await legacyService.start(mockHttpServerInfo); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + expect(logger.mockCollect().error).toEqual([]); + + const configError = new Error('something went wrong'); + mockKbnServer.applyLoggingConfiguration.mockImplementation(() => { + throw configError; + }); + + config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + + expect(logger.mockCollect().error).toEqual([[configError]]); + }); + + test('logs error if config service fails.', async () => { + await legacyService.start(mockHttpServerInfo); + + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + expect(logger.mockCollect().error).toEqual([]); + + const configError = new Error('something went wrong'); + config$.error(configError); + + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + expect(logger.mockCollect().error).toEqual([[configError]]); + }); + + test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { + const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; + const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; + + await legacyService.start(mockHttpServerInfo); + + const [[{ handler }]] = mockHttpServerInfo.server.route.mock.calls; + const response = await handler(mockRequest, mockResponseToolkit); + + expect(response).toBe(mockResponseToolkit.abandon); + expect(mockResponseToolkit.response).not.toHaveBeenCalled(); + + // Make sure request has been passed to the legacy platform. + const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances; + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1); + expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith( + 'request', + mockRequest.raw.req, + mockRequest.raw.res + ); + }); +}); + +describe('once LegacyService is started without connection info', () => { + beforeEach(async () => await legacyService.start()); + + test('creates legacy kbnServer with `autoListen: false`.', () => { + expect(mockHttpServerInfo.server.route).not.toHaveBeenCalled(); + expect(MockKbnServer).toHaveBeenCalledTimes(1); + expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, + { serverOptions: { autoListen: false } } + ); + }); + + test('reconfigures logging configuration if new config is received.', async () => { + const [mockKbnServer] = MockKbnServer.mock.instances; + expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); + + config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } })); + + expect(mockKbnServer.applyLoggingConfiguration.mock.calls).toMatchSnapshot( + `applyLoggingConfiguration params` + ); + }); +}); + +describe('once LegacyService is started in `devClusterMaster` mode', () => { + beforeEach(() => { + configService.atPath.mockImplementation(path => { + return new BehaviorSubject( + path === 'dev' ? { basePathProxyTargetPort: 100500 } : { basePath: '/abc' } + ); + }); + }); + + test('creates ClusterManager without base path proxy.', async () => { + const devClusterLegacyService = new LegacyService( + Env.createDefault( + getEnvOptions({ + cliArgs: { silent: true, basePath: false }, + isDevClusterMaster: true, + }) + ), + logger, + configService + ); + + await devClusterLegacyService.start(); + + expect(MockClusterManager.create.mock.calls).toMatchSnapshot( + 'cluster manager without base path proxy' + ); + }); + + test('creates ClusterManager with base path proxy.', async () => { + const devClusterLegacyService = new LegacyService( + Env.createDefault( + getEnvOptions({ + cliArgs: { quiet: true, basePath: true }, + isDevClusterMaster: true, + }) + ), + logger, + configService + ); + + await devClusterLegacyService.start(); + + expect(MockClusterManager.create.mock.calls).toMatchSnapshot( + 'cluster manager with base path proxy' + ); + }); +}); diff --git a/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index d03398e173e40..af2bfff0abfe3 100644 --- a/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -2,6 +2,7 @@ exports[`#get correctly handles server config. 1`] = ` Object { + "autoListen": true, "basePath": "/abc", "cors": false, "host": "host", diff --git a/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts index ef07e86e5fefc..483e156f4697d 100644 --- a/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.ts @@ -32,7 +32,7 @@ interface LegacyLoggingConfig { } /** - * Represents adapter between config provided by legacy platform and `RawConfig` + * Represents adapter between config provided by legacy platform and `Config` * supported by the current platform. */ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { @@ -59,6 +59,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { // TODO: New platform uses just a subset of `server` config from the legacy platform, // new values will be exposed once we need them (eg. customResponseHeaders or xsrf). return { + autoListen: configValue.autoListen, basePath: configValue.basePath, cors: configValue.cors, host: configValue.host, diff --git a/src/core/server/legacy_compat/index.ts b/src/core/server/legacy_compat/index.ts index dc5db0ab1fb7a..3e10928aa3456 100644 --- a/src/core/server/legacy_compat/index.ts +++ b/src/core/server/legacy_compat/index.ts @@ -17,54 +17,17 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { ConfigService, Env } from '../config'; +import { LoggerFactory } from '../logging'; +import { LegacyService } from './legacy_service'; -/** @internal */ -export { LegacyPlatformProxifier } from './legacy_platform_proxifier'; -/** @internal */ export { LegacyObjectToConfigAdapter } from './config/legacy_object_to_config_adapter'; +export { LegacyService } from './legacy_service'; -import { LegacyObjectToConfigAdapter, LegacyPlatformProxifier } from '.'; -import { Env } from '../config'; -import { Root } from '../root'; -import { BasePathProxyRoot } from '../root/base_path_proxy_root'; +export class LegacyCompatModule { + public readonly service: LegacyService; -function initEnvironment(rawKbnServer: any, isDevClusterMaster = false) { - const env = Env.createDefault({ - // The core doesn't work with configs yet, everything is provided by the - // "legacy" Kibana, so we can have empty array here. - configs: [], - // `dev` is the only CLI argument we currently use. - cliArgs: { dev: rawKbnServer.config.get('env.dev') }, - isDevClusterMaster, - }); - - const legacyConfig$ = new BehaviorSubject>(rawKbnServer.config.get()); - return { - config$: legacyConfig$.pipe(map(legacyConfig => new LegacyObjectToConfigAdapter(legacyConfig))), - env, - // Propagates legacy config updates to the new platform. - updateConfig(legacyConfig: Record) { - legacyConfig$.next(legacyConfig); - }, - }; + constructor(private readonly configService: ConfigService, logger: LoggerFactory, env: Env) { + this.service = new LegacyService(env, logger, this.configService); + } } - -/** - * @internal - */ -export const injectIntoKbnServer = (rawKbnServer: any) => { - const { env, config$, updateConfig } = initEnvironment(rawKbnServer); - - rawKbnServer.newPlatform = { - // Custom HTTP Listener that will be used within legacy platform by HapiJS server. - proxyListener: new LegacyPlatformProxifier(new Root(config$, env), env), - updateConfig, - }; -}; - -export const createBasePathProxy = (rawKbnServer: any) => { - const { env, config$ } = initEnvironment(rawKbnServer, true /*isDevClusterMaster*/); - return new BasePathProxyRoot(config$, env); -}; diff --git a/src/core/server/legacy_compat/legacy_platform_proxifier.ts b/src/core/server/legacy_compat/legacy_platform_proxifier.ts deleted file mode 100644 index 8baa156266ef0..0000000000000 --- a/src/core/server/legacy_compat/legacy_platform_proxifier.ts +++ /dev/null @@ -1,172 +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 { EventEmitter } from 'events'; -import { Server } from 'net'; - -import { Server as HapiServer, ServerOptions as HapiServerOptions } from 'hapi-latest'; -import { Env } from '../config'; -import { Logger } from '../logging'; -import { Root } from '../root'; - -interface ConnectionInfo { - server: HapiServer; - options: HapiServerOptions; -} - -/** - * List of the server events to be forwarded to the legacy platform. - */ -const ServerEventsToForward = [ - 'clientError', - 'close', - 'connection', - 'error', - 'listening', - 'upgrade', -]; - -/** - * Represents "proxy" between legacy and current platform. - * @internal - */ -export class LegacyPlatformProxifier extends EventEmitter { - private readonly eventHandlers: Map void>; - private readonly log: Logger; - private server?: Server; - - constructor(private readonly root: Root, private readonly env: Env) { - super(); - - this.log = root.logger.get('legacy-platform-proxifier'); - - // HapiJS expects that the following events will be generated by `listener`, see: - // https://github.com/hapijs/hapi/blob/v14.2.0/lib/connection.js. - this.eventHandlers = new Map( - ServerEventsToForward.map(eventName => { - return [ - eventName, - (...args: any[]) => { - this.log.debug(`Event is being forwarded: ${eventName}`); - this.emit(eventName, ...args); - }, - ] as [string, (...args: any[]) => void]; - }) - ); - - // Once core HTTP service is ready it broadcasts the internal server it relies on - // and server options that were used to create that server so that we can properly - // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is managed - // by ClusterManager or optimizer) then this event will never fire. - this.env.legacy.once('connection', (connectionInfo: ConnectionInfo) => - this.onConnection(connectionInfo) - ); - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public address() { - return this.server && this.server.address(); - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public async listen(port: number, host: string, callback?: (error?: Error) => void) { - this.log.debug(`"listen" has been called (${host}:${port}).`); - - let error: Error | undefined; - try { - await this.root.start(); - } catch (err) { - error = err; - this.emit('error', err); - } - - if (callback !== undefined) { - callback(error); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public async close(callback?: (error?: Error) => void) { - this.log.debug('"close" has been called.'); - - let error: Error | undefined; - try { - await this.root.shutdown(); - } catch (err) { - error = err; - this.emit('error', err); - } - - if (callback !== undefined) { - callback(error); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public getConnections(callback: (error: Error | null, count?: number) => void) { - // This method is used by `even-better` (before we start platform). - // It seems that the latest version of parent `good` doesn't use this anymore. - if (this.server) { - this.server.getConnections(callback); - } else { - callback(null, 0); - } - } - - private onConnection({ server }: ConnectionInfo) { - this.server = server.listener; - - for (const [eventName, eventHandler] of this.eventHandlers) { - this.server.addListener(eventName, eventHandler); - } - - // We register Kibana proxy middleware right before we start server to allow - // all new platform plugins register their routes, so that `legacyProxy` - // handles only requests that aren't handled by the new platform. - server.route({ - path: '/{p*}', - method: '*', - options: { - payload: { - output: 'stream', - parse: false, - timeout: false, - // Having such a large value here will allow legacy routes to override - // maximum allowed payload size set in the core http server if needed. - maxBytes: Number.MAX_SAFE_INTEGER, - }, - }, - handler: async ({ raw: { req, res } }, responseToolkit) => { - this.log.trace(`Request will be handled by proxy ${req.method}:${req.url}.`); - // Forward request and response objects to the legacy platform. This method - // is used whenever new platform doesn't know how to handle the request. - this.emit('request', req, res); - return responseToolkit.abandon; - }, - }); - } -} diff --git a/src/core/server/legacy_compat/legacy_platform_proxy.ts b/src/core/server/legacy_compat/legacy_platform_proxy.ts new file mode 100644 index 0000000000000..e91d661e30238 --- /dev/null +++ b/src/core/server/legacy_compat/legacy_platform_proxy.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 { EventEmitter } from 'events'; +import { Server } from 'net'; + +import { Logger } from '../logging'; + +/** + * List of the server events to be forwarded to the legacy platform. + */ +const ServerEventsToForward = [ + 'clientError', + 'close', + 'connection', + 'error', + 'listening', + 'upgrade', +]; + +/** + * Represents "proxy" between legacy and current platform. + * @internal + */ +export class LegacyPlatformProxy extends EventEmitter { + private readonly eventHandlers: Map void>; + + constructor(private readonly log: Logger, private readonly server: Server) { + super(); + + // HapiJS expects that the following events will be generated by `listener`, see: + // https://github.com/hapijs/hapi/blob/v14.2.0/lib/connection.js. + this.eventHandlers = new Map( + ServerEventsToForward.map(eventName => { + return [ + eventName, + (...args: any[]) => { + this.log.debug(`Event is being forwarded: ${eventName}`); + this.emit(eventName, ...args); + }, + ] as [string, (...args: any[]) => void]; + }) + ); + + for (const [eventName, eventHandler] of this.eventHandlers) { + this.server.addListener(eventName, eventHandler); + } + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public address() { + this.log.debug('"address" has been called.'); + + return this.server.address(); + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public listen(port: number, host: string, callback?: (error?: Error) => void) { + this.log.debug(`"listen" has been called (${host}:${port}).`); + + if (callback !== undefined) { + callback(); + } + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public close(callback?: (error?: Error) => void) { + this.log.debug('"close" has been called.'); + + if (callback !== undefined) { + callback(); + } + } + + /** + * Neither new nor legacy platform should use this method directly. + */ + public getConnections(callback: (error: Error | null, count?: number) => void) { + this.log.debug('"getConnections" has been called.'); + + // This method is used by `even-better` (before we start platform). + // It seems that the latest version of parent `good` doesn't use this anymore. + this.server.getConnections(callback); + } +} diff --git a/src/core/server/legacy_compat/legacy_service.ts b/src/core/server/legacy_compat/legacy_service.ts new file mode 100644 index 0000000000000..092057874fa73 --- /dev/null +++ b/src/core/server/legacy_compat/legacy_service.ts @@ -0,0 +1,204 @@ +/* + * 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 { Server as HapiServer } from 'hapi-latest'; +import { combineLatest, ConnectableObservable, EMPTY, Subscription } from 'rxjs'; +import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators'; +import { CoreService } from '../../types/core_service'; +import { Config, ConfigService, Env } from '../config'; +import { DevConfig } from '../dev'; +import { BasePathProxyServer, HttpConfig, HttpServerInfo } from '../http'; +import { Logger, LoggerFactory } from '../logging'; +import { LegacyPlatformProxy } from './legacy_platform_proxy'; + +interface LegacyKbnServer { + applyLoggingConfiguration: (settings: Readonly>) => void; + listen: () => Promise; + ready: () => Promise; + close: () => Promise; +} + +export class LegacyService implements CoreService { + private readonly log: Logger; + private kbnServer?: LegacyKbnServer; + private configSubscription?: Subscription; + + constructor( + private readonly env: Env, + private readonly logger: LoggerFactory, + private readonly configService: ConfigService + ) { + this.log = logger.get('legacy', 'service'); + } + + public async start(httpServerInfo?: HttpServerInfo) { + this.log.debug('starting legacy service'); + + const update$ = this.configService.getConfig$().pipe( + tap(config => { + if (this.kbnServer !== undefined) { + this.kbnServer.applyLoggingConfiguration(config.toRaw()); + } + }), + tap({ error: err => this.log.error(err) }), + publishReplay(1) + ) as ConnectableObservable; + + this.configSubscription = update$.connect(); + + // Receive initial config and create kbnServer/ClusterManager. + this.kbnServer = await update$ + .pipe( + first(), + mergeMap(async config => { + if (this.env.isDevClusterMaster) { + await this.createClusterManager(config); + return; + } + + return await this.createKbnServer(config, httpServerInfo); + }) + ) + .toPromise(); + } + + public async stop() { + this.log.debug('stopping legacy service'); + + if (this.configSubscription !== undefined) { + this.configSubscription.unsubscribe(); + this.configSubscription = undefined; + } + + if (this.kbnServer !== undefined) { + await this.kbnServer.close(); + this.kbnServer = undefined; + } + } + + private async createClusterManager(config: Config) { + const basePathProxy$ = this.env.cliArgs.basePath + ? combineLatest( + this.configService.atPath('dev', DevConfig), + this.configService.atPath('server', HttpConfig) + ).pipe( + first(), + map(([devConfig, httpConfig]) => { + return new BasePathProxyServer(this.logger.get('server'), httpConfig, devConfig); + }) + ) + : EMPTY; + + require('../../../cli/cluster/cluster_manager').create( + this.env.cliArgs, + config.toRaw(), + await basePathProxy$.toPromise() + ); + } + + private async createKbnServer(config: Config, httpServerInfo?: HttpServerInfo) { + const KbnServer = require('../../../server/kbn_server'); + const kbnServer: LegacyKbnServer = new KbnServer(config.toRaw(), { + // If core HTTP service is run we'll receive internal server reference and + // options that were used to create that server so that we can properly + // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is + // managed by ClusterManager or optimizer) then we won't have that info, + // so we can't start "legacy" server either. + serverOptions: + httpServerInfo !== undefined + ? { + ...httpServerInfo.options, + listener: this.setupProxyListener(httpServerInfo.server), + } + : { autoListen: false }, + }); + + // The kbnWorkerType check is necessary to prevent the repl + // from being started multiple times in different processes. + // We only want one REPL. + if (this.env.cliArgs.repl && process.env.kbnWorkerType === 'server') { + require('../../../cli/repl').startRepl(kbnServer); + } + + const httpConfig = await this.configService + .atPath('server', HttpConfig) + .pipe(first()) + .toPromise(); + + if (httpConfig.autoListen) { + try { + await kbnServer.listen(); + } catch (err) { + await kbnServer.close(); + throw err; + } + } else { + await kbnServer.ready(); + } + + return kbnServer; + } + + private setupProxyListener(server: HapiServer) { + const legacyProxy = new LegacyPlatformProxy( + this.logger.get('legacy', 'proxy'), + server.listener + ); + + // We register Kibana proxy middleware right before we start server to allow + // all new platform plugins register their routes, so that `legacyProxy` + // handles only requests that aren't handled by the new platform. + server.route({ + path: '/{p*}', + method: '*', + options: { + payload: { + output: 'stream', + parse: false, + timeout: false, + // Having such a large value here will allow legacy routes to override + // maximum allowed payload size set in the core http server if needed. + maxBytes: Number.MAX_SAFE_INTEGER, + }, + }, + handler: async ({ raw: { req, res } }, responseToolkit) => { + if (this.kbnServer === undefined) { + this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`); + + // If legacy server is not ready yet (e.g. it's still in optimization phase), + // we should let client know that and ask to retry after 30 seconds. + return responseToolkit + .response('Kibana server is not ready yet') + .code(503) + .header('Retry-After', '30'); + } + + this.log.trace(`Request will be handled by proxy ${req.method}:${req.url}.`); + + // Forward request and response objects to the legacy platform. This method + // is used whenever new platform doesn't know how to handle the request. + legacyProxy.emit('request', req, res); + + return responseToolkit.abandon; + }, + }); + + return legacyProxy; + } +} diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index 90ee9524381de..966bd74a0df41 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -71,7 +71,7 @@ export class LoggingService implements LoggerFactory { this.appenders.set(appenderKey, Appenders.create(appenderConfig)); } - for (const [loggerKey, loggerAdapter] of this.loggers.entries()) { + for (const [loggerKey, loggerAdapter] of this.loggers) { loggerAdapter.updateLogger(this.createLogger(loggerKey, config)); } diff --git a/src/core/server/root/base_path_proxy_root.ts b/src/core/server/root/base_path_proxy_root.ts deleted file mode 100644 index 80ab7d1c60677..0000000000000 --- a/src/core/server/root/base_path_proxy_root.ts +++ /dev/null @@ -1,80 +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 { first } from 'rxjs/operators'; - -import { Root } from '.'; -import { DevConfig } from '../dev'; -import { HttpConfig } from '../http'; -import { BasePathProxyServer, BasePathProxyServerOptions } from '../http/base_path_proxy_server'; - -/** - * Top-level entry point to start BasePathProxy server. - */ -export class BasePathProxyRoot extends Root { - private basePathProxy?: BasePathProxyServer; - - public async configure({ - blockUntil, - shouldRedirectFromOldBasePath, - }: Pick) { - const [devConfig, httpConfig] = await Promise.all([ - this.configService - .atPath('dev', DevConfig) - .pipe(first()) - .toPromise(), - this.configService - .atPath('server', HttpConfig) - .pipe(first()) - .toPromise(), - ]); - - this.basePathProxy = new BasePathProxyServer(this.logger.get('server'), { - blockUntil, - devConfig, - httpConfig, - shouldRedirectFromOldBasePath, - }); - } - - public getBasePath() { - return this.getBasePathProxy().basePath; - } - - public getTargetPort() { - return this.getBasePathProxy().targetPort; - } - - protected async startServer() { - return this.getBasePathProxy().start(); - } - - protected async stopServer() { - await this.getBasePathProxy().stop(); - this.basePathProxy = undefined; - } - - private getBasePathProxy() { - if (this.basePathProxy === undefined) { - throw new Error('BasePathProxyRoot is not configured!'); - } - - return this.basePathProxy; - } -} diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index 935d11a83e963..02a5c9e559544 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -18,38 +18,34 @@ */ import { ConnectableObservable, Observable, Subscription } from 'rxjs'; -import { catchError, first, map, publishReplay } from 'rxjs/operators'; +import { first, map, publishReplay, tap } from 'rxjs/operators'; import { Server } from '..'; import { Config, ConfigService, Env } from '../config'; - import { Logger, LoggerFactory, LoggingConfig, LoggingService } from '../logging'; -export type OnShutdown = (reason?: Error) => void; - /** * Top-level entry point to kick off the app and start the Kibana server. */ export class Root { public readonly logger: LoggerFactory; - protected readonly configService: ConfigService; + private readonly configService: ConfigService; private readonly log: Logger; - private server?: Server; + private readonly server: Server; private readonly loggingService: LoggingService; private loggingConfigSubscription?: Subscription; constructor( config$: Observable, private readonly env: Env, - private readonly onShutdown: OnShutdown = () => { - // noop - } + private readonly onShutdown?: (reason?: Error | string) => void ) { this.loggingService = new LoggingService(); this.logger = this.loggingService.asLoggerFactory(); - this.log = this.logger.get('root'); + this.configService = new ConfigService(config$, env, this.logger); + this.server = new Server(this.configService, this.logger, this.env); } public async start() { @@ -57,53 +53,46 @@ export class Root { try { await this.setupLogging(); - await this.startServer(); + await this.server.start(); } catch (e) { await this.shutdown(e); throw e; } } - public async shutdown(reason?: Error) { + public async shutdown(reason?: any) { this.log.debug('shutting root down'); - await this.stopServer(); + if (reason) { + if (reason.code === 'EADDRINUSE' && Number.isInteger(reason.port)) { + reason = new Error( + `Port ${reason.port} is already in use. Another instance of Kibana may be running!` + ); + } + + this.log.fatal(reason); + } + + await this.server.stop(); if (this.loggingConfigSubscription !== undefined) { this.loggingConfigSubscription.unsubscribe(); this.loggingConfigSubscription = undefined; } - await this.loggingService.stop(); - this.onShutdown(reason); - } - - protected async startServer() { - this.server = new Server(this.configService, this.logger, this.env); - return this.server.start(); - } - - protected async stopServer() { - if (this.server === undefined) { - return; + if (this.onShutdown !== undefined) { + this.onShutdown(reason); } - - await this.server.stop(); - this.server = undefined; } private async setupLogging() { // Stream that maps config updates to logger updates, including update failures. const update$ = this.configService.atPath('logging', LoggingConfig).pipe( map(config => this.loggingService.upgrade(config)), - catchError(err => { - // This specifically console.logs because we were not able to configure the logger. - // tslint:disable-next-line no-console - console.error('Configuring logger failed:', err); - - throw err; - }), + // This specifically console.logs because we were not able to configure the logger. + // tslint:disable-next-line no-console + tap({ error: err => console.error('Configuring logger failed:', err) }), publishReplay(1) ) as ConnectableObservable; diff --git a/src/core/types/core_service.ts b/src/core/types/core_service.ts index b6031e0deb7ba..8a8ac92b93ccc 100644 --- a/src/core/types/core_service.ts +++ b/src/core/types/core_service.ts @@ -17,7 +17,7 @@ * under the License. */ -export interface CoreService { - start(): Promise; +export interface CoreService { + start(): Promise; stop(): Promise; } diff --git a/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js b/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js index 54720cf98a827..2aa3953a78bd9 100644 --- a/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js +++ b/src/core_plugins/kibana/server/lib/__tests__/manage_uuid.js @@ -19,7 +19,7 @@ import expect from 'expect.js'; import sinon from 'sinon'; -import { startTestServers } from '../../../../../test_utils/kbn_server.js'; +import { startTestServers } from '../../../../../test_utils/kbn_server'; import manageUuid from '../manage_uuid'; describe('core_plugins/kibana/server/lib', function () { diff --git a/src/functional_test_runner/__tests__/lib/index.js b/src/functional_test_runner/__tests__/lib/index.js deleted file mode 100644 index a92d22e2738bb..0000000000000 --- a/src/functional_test_runner/__tests__/lib/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { startupKibana } from './kibana'; diff --git a/src/functional_test_runner/__tests__/lib/kibana.js b/src/functional_test_runner/__tests__/lib/kibana.js deleted file mode 100644 index df046e34b2658..0000000000000 --- a/src/functional_test_runner/__tests__/lib/kibana.js +++ /dev/null @@ -1,36 +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 { createServerWithCorePlugins } from '../../../test_utils/kbn_server'; - -export async function startupKibana({ port, esUrl }) { - const server = createServerWithCorePlugins({ - server: { - port, - autoListen: true, - }, - - elasticsearch: { - url: esUrl - } - }); - - await server.ready(); - return server; -} diff --git a/src/server/config/__tests__/deprecation_warnings.js b/src/server/config/__tests__/deprecation_warnings.js index 4e90708456b25..9935a2e4bddbf 100644 --- a/src/server/config/__tests__/deprecation_warnings.js +++ b/src/server/config/__tests__/deprecation_warnings.js @@ -40,7 +40,8 @@ describe('config/deprecation warnings mixin', function () { env: { CREATE_SERVER_OPTS: JSON.stringify({ logging: { - quiet: false + quiet: false, + silent: false }, uiSettings: { enabled: true diff --git a/src/server/config/__tests__/fixtures/run_kbn_server_startup.js b/src/server/config/__tests__/fixtures/run_kbn_server_startup.js index 46eb6b4661f49..d6622cf69ddb0 100644 --- a/src/server/config/__tests__/fixtures/run_kbn_server_startup.js +++ b/src/server/config/__tests__/fixtures/run_kbn_server_startup.js @@ -17,18 +17,18 @@ * under the License. */ -import { createServer } from '../../../../test_utils/kbn_server'; +import { createRoot } from '../../../../test_utils/kbn_server'; (async function run() { - const server = createServer(JSON.parse(process.env.CREATE_SERVER_OPTS)); + const root = createRoot(JSON.parse(process.env.CREATE_SERVER_OPTS)); // We just need the server to run through startup so that it will // log the deprecation messages. Once it has started up we close it // to allow the process to exit naturally try { - await server.ready(); + await root.start(); } finally { - await server.close(); + await root.shutdown(); } }()); diff --git a/src/server/http/__snapshots__/max_payload_size.test.js.snap b/src/server/http/__snapshots__/max_payload_size.test.js.snap deleted file mode 100644 index 12e9ab278e1fb..0000000000000 --- a/src/server/http/__snapshots__/max_payload_size.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`fails with 400 if payload size is larger than default and route config allows 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Payload content length greater than maximum allowed: 200\\"}"`; diff --git a/src/server/http/__snapshots__/xsrf.test.js.snap b/src/server/http/__snapshots__/xsrf.test.js.snap deleted file mode 100644 index 2113d27927dce..0000000000000 --- a/src/server/http/__snapshots__/xsrf.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`xsrf request filter destructiveMethod: DELETE rejects requests without either an xsrf or version header: DELETE reject response 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Request must contain a kbn-xsrf header.\\"}"`; - -exports[`xsrf request filter destructiveMethod: POST rejects requests without either an xsrf or version header: POST reject response 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Request must contain a kbn-xsrf header.\\"}"`; - -exports[`xsrf request filter destructiveMethod: PUT rejects requests without either an xsrf or version header: PUT reject response 1`] = `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"Request must contain a kbn-xsrf header.\\"}"`; diff --git a/src/server/http/index.js b/src/server/http/index.js index 3b16cec484c30..7012b095a8658 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -30,35 +30,7 @@ export default async function (kbnServer, server, config) { kbnServer.server = new Hapi.Server(); server = kbnServer.server; - // Note that all connection options configured here should be exactly the same - // as in `getServerOptions()` in the new platform (see `src/core/server/http/http_tools`). - // - // The only exception is `tls` property: TLS is entirely handled by the new - // platform and we don't have to duplicate all TLS related settings here, we just need - // to indicate to Hapi connection that TLS is used so that it can use correct protocol - // name in `server.info` and `request.connection.info` that are used throughout Kibana. - // - // Any change SHOULD BE applied in both places. - server.connection({ - host: config.get('server.host'), - port: config.get('server.port'), - tls: config.get('server.ssl.enabled'), - listener: kbnServer.newPlatform.proxyListener, - state: { - strictHeader: false, - }, - routes: { - cors: config.get('server.cors'), - payload: { - maxBytes: config.get('server.maxPayloadBytes'), - }, - validate: { - options: { - abortEarly: false, - }, - }, - }, - }); + server.connection(kbnServer.core.serverOptions); registerHapiPlugins(server); diff --git a/src/server/http/integration_tests/max_payload_size.test.js b/src/server/http/integration_tests/max_payload_size.test.js new file mode 100644 index 0000000000000..3fa7ca721e1ef --- /dev/null +++ b/src/server/http/integration_tests/max_payload_size.test.js @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as kbnTestServer from '../../../test_utils/kbn_server'; + +let root; +beforeAll(async () => { + root = kbnTestServer.createRoot({ server: { maxPayloadBytes: 100 } }); + + await root.start(); + + kbnTestServer.getKbnServer(root).server.route({ + path: '/payload_size_check/test/route', + method: 'POST', + config: { payload: { maxBytes: 200 } }, + handler: (req, reply) => reply(null, req.payload.data.slice(0, 5)), + }); +}, 30000); + +afterAll(async () => await root.shutdown()); + +test('accepts payload with a size larger than default but smaller than route config allows', async () => { + await kbnTestServer.request.post(root, '/payload_size_check/test/route') + .send({ data: Array(150).fill('+').join('') }) + .expect(200, '+++++'); +}); + +test('fails with 400 if payload size is larger than default and route config allows', async () => { + await kbnTestServer.request.post(root, '/payload_size_check/test/route') + .send({ data: Array(250).fill('+').join('') }) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'Payload content length greater than maximum allowed: 200' + }); +}); diff --git a/src/server/http/version_check.test.js b/src/server/http/integration_tests/version_check.test.js similarity index 53% rename from src/server/http/version_check.test.js rename to src/server/http/integration_tests/version_check.test.js index e5257f814e8ae..676391ce3233b 100644 --- a/src/server/http/version_check.test.js +++ b/src/server/http/integration_tests/version_check.test.js @@ -18,71 +18,48 @@ */ import { resolve } from 'path'; -import * as kbnTestServer from '../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_utils/kbn_server'; -const src = resolve.bind(null, __dirname, '../../../src'); +const src = resolve.bind(null, __dirname, '../../../../src'); const versionHeader = 'kbn-version'; const version = require(src('../package.json')).version; describe('version_check request filter', function () { - async function makeRequest(kbnServer, opts) { - return await kbnTestServer.makeRequest(kbnServer, opts); - } + let root; + beforeAll(async () => { + root = kbnTestServer.createRoot(); - async function makeServer() { - const kbnServer = kbnTestServer.createServer(); + await root.start(); - await kbnServer.ready(); - - kbnServer.server.route({ + kbnTestServer.getKbnServer(root).server.route({ path: '/version_check/test/route', method: 'GET', handler: function (req, reply) { reply(null, 'ok'); } }); + }, 30000); - return kbnServer; - } - - let kbnServer; - beforeEach(async () => kbnServer = await makeServer()); - afterEach(async () => await kbnServer.close()); + afterAll(async () => await root.shutdown()); it('accepts requests with the correct version passed in the version header', async function () { - const resp = await makeRequest(kbnServer, { - url: '/version_check/test/route', - method: 'GET', - headers: { - [versionHeader]: version, - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, '/version_check/test/route') + .set(versionHeader, version) + .expect(200, 'ok'); }); it('rejects requests with an incorrect version passed in the version header', async function () { - const resp = await makeRequest(kbnServer, { - url: '/version_check/test/route', - method: 'GET', - headers: { - [versionHeader]: `invalid:${version}`, - }, - }); - - expect(resp.statusCode).toBe(400); - expect(resp.payload).toMatch(/"Browser client is out of date/); + await kbnTestServer.request + .get(root, '/version_check/test/route') + .set(versionHeader, `invalid:${version}`) + .expect(400, /"Browser client is out of date/); }); it('accepts requests that do not include a version header', async function () { - const resp = await makeRequest(kbnServer, { - url: '/version_check/test/route', - method: 'GET' - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, '/version_check/test/route') + .expect(200, 'ok'); }); }); diff --git a/src/server/http/xsrf.test.js b/src/server/http/integration_tests/xsrf.test.js similarity index 55% rename from src/server/http/xsrf.test.js rename to src/server/http/integration_tests/xsrf.test.js index 2fc6dba4703ef..a8c87653e9b40 100644 --- a/src/server/http/xsrf.test.js +++ b/src/server/http/integration_tests/xsrf.test.js @@ -18,10 +18,10 @@ */ import { resolve } from 'path'; -import * as kbnTestServer from '../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_utils/kbn_server'; const destructiveMethods = ['POST', 'PUT', 'DELETE']; -const src = resolve.bind(null, __dirname, '../../../src'); +const src = resolve.bind(null, __dirname, '../../../../src'); const xsrfHeader = 'kbn-xsrf'; const versionHeader = 'kbn-version'; @@ -29,23 +29,18 @@ const testPath = '/xsrf/test/route'; const whitelistedTestPath = '/xsrf/test/route/whitelisted'; const actualVersion = require(src('../package.json')).version; -describe('xsrf request filter', function () { - async function inject(kbnServer, opts) { - return await kbnTestServer.makeRequest(kbnServer, opts); - } - - const makeServer = async function () { - const kbnServer = kbnTestServer.createServer({ +describe('xsrf request filter', () => { + let root; + beforeAll(async () => { + root = kbnTestServer.createRoot({ server: { - xsrf: { - disableProtection: false, - whitelist: [whitelistedTestPath] - } + xsrf: { disableProtection: false, whitelist: [whitelistedTestPath] } } }); - await kbnServer.ready(); + await root.start(); + const kbnServer = kbnTestServer.getKbnServer(root); kbnServer.server.route({ path: testPath, method: 'GET', @@ -81,117 +76,68 @@ describe('xsrf request filter', function () { reply(null, 'ok'); } }); + }, 30000); - return kbnServer; - }; - - let kbnServer; - beforeEach(async () => { - kbnServer = await makeServer(); - }); - - afterEach(async () => { - await kbnServer.close(); - }); + afterAll(async () => await root.shutdown()); describe(`nonDestructiveMethod: GET`, function () { it('accepts requests without a token', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'GET' - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, testPath) + .expect(200, 'ok'); }); it('accepts requests with the xsrf header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'GET', - headers: { - [xsrfHeader]: 'anything', - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request + .get(root, testPath) + .set(xsrfHeader, 'anything') + .expect(200, 'ok'); }); }); describe(`nonDestructiveMethod: HEAD`, function () { it('accepts requests without a token', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'HEAD' - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toHaveLength(0); + await kbnTestServer.request + .head(root, testPath) + .expect(200, undefined); }); it('accepts requests with the xsrf header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: 'HEAD', - headers: { - [xsrfHeader]: 'anything', - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toHaveLength(0); + await kbnTestServer.request + .head(root, testPath) + .set(xsrfHeader, 'anything') + .expect(200, undefined); }); }); for (const method of destructiveMethods) { describe(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func it('accepts requests with the xsrf header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: method, - headers: { - [xsrfHeader]: 'anything', - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request[method.toLowerCase()](root, testPath) + .set(xsrfHeader, 'anything') + .expect(200, 'ok'); }); // this is still valid for existing csrf protection support // it does not actually do any validation on the version value itself it('accepts requests with the version header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: method, - headers: { - [versionHeader]: actualVersion, - }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request[method.toLowerCase()](root, testPath) + .set(versionHeader, actualVersion) + .expect(200, 'ok'); }); it('rejects requests without either an xsrf or version header', async function () { - const resp = await inject(kbnServer, { - url: testPath, - method: method - }); - - expect(resp.statusCode).toBe(400); - expect(resp.result).toMatchSnapshot(`${method} reject response`); + await kbnTestServer.request[method.toLowerCase()](root, testPath) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: 'Request must contain a kbn-xsrf header.' + }); }); it('accepts whitelisted requests without either an xsrf or version header', async function () { - const resp = await inject(kbnServer, { - url: whitelistedTestPath, - method: method - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('ok'); + await kbnTestServer.request[method.toLowerCase()](root, whitelistedTestPath) + .expect(200, 'ok'); }); }); } diff --git a/src/server/http/max_payload_size.test.js b/src/server/http/max_payload_size.test.js deleted file mode 100644 index 499ce43b8d09a..0000000000000 --- a/src/server/http/max_payload_size.test.js +++ /dev/null @@ -1,70 +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 * as kbnTestServer from '../../test_utils/kbn_server'; - -let kbnServer; -async function makeServer({ maxPayloadBytesDefault, maxPayloadBytesRoute }) { - kbnServer = kbnTestServer.createServer({ - server: { maxPayloadBytes: maxPayloadBytesDefault } - }); - - await kbnServer.ready(); - - kbnServer.server.route({ - path: '/payload_size_check/test/route', - method: 'POST', - config: { payload: { maxBytes: maxPayloadBytesRoute } }, - handler: function (req, reply) { - reply(null, req.payload.data.slice(0, 5)); - } - }); -} - -async function makeRequest(opts) { - return await kbnTestServer.makeRequest(kbnServer, opts); -} - -afterEach(async () => await kbnServer.close()); - -test('accepts payload with a size larger than default but smaller than route config allows', async () => { - await makeServer({ maxPayloadBytesDefault: 100, maxPayloadBytesRoute: 200 }); - - const resp = await makeRequest({ - url: '/payload_size_check/test/route', - method: 'POST', - payload: { data: Array(150).fill('+').join('') }, - }); - - expect(resp.statusCode).toBe(200); - expect(resp.payload).toBe('+++++'); -}); - -test('fails with 400 if payload size is larger than default and route config allows', async () => { - await makeServer({ maxPayloadBytesDefault: 100, maxPayloadBytesRoute: 200 }); - - const resp = await makeRequest({ - url: '/payload_size_check/test/route', - method: 'POST', - payload: { data: Array(250).fill('+').join('') }, - }); - - expect(resp.statusCode).toBe(400); - expect(resp.payload).toMatchSnapshot(); -}); diff --git a/src/server/http/setup_connection.js b/src/server/http/setup_connection.js deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js index 7279a8f407b11..4f4334d764ddb 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -21,6 +21,7 @@ import { constant, once, compact, flatten } from 'lodash'; import { fromNode } from 'bluebird'; import { isWorker } from 'cluster'; import { fromRoot, pkg } from '../utils'; +import { Config } from './config'; import loggingConfiguration from './logging/configuration'; import configSetupMixin from './config/setup'; import httpMixin from './http'; @@ -30,6 +31,7 @@ import { usageMixin } from './usage'; import { statusMixin } from './status'; import pidMixin from './pid'; import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; +import { transformDeprecations } from './config/transform_deprecations'; import configCompleteMixin from './config/complete'; import optimizeMixin from '../optimize'; import * as Plugins from './plugins'; @@ -41,27 +43,26 @@ import { urlShorteningMixin } from './url_shortening'; import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { sassMixin } from './sass'; -import { injectIntoKbnServer as newPlatformMixin } from '../core'; import { i18nMixin } from './i18n'; const rootDir = fromRoot('.'); export default class KbnServer { - constructor(settings) { + constructor(settings, core) { this.name = pkg.name; this.version = pkg.version; this.build = pkg.build || false; this.rootDir = rootDir; this.settings = settings || {}; + this.core = core; + this.ready = constant(this.mixin( Plugins.waitForInitSetupMixin, // sets this.config, reads this.settings configSetupMixin, - newPlatformMixin, - // sets this.server httpMixin, @@ -111,13 +112,6 @@ export default class KbnServer { // notify any deferred setup logic that plugins have initialized Plugins.waitForInitResolveMixin, - - () => { - if (this.config.get('server.autoListen')) { - this.ready = constant(Promise.resolve()); - return this.listen(); - } - } )); this.listen = once(this.listen); @@ -148,14 +142,17 @@ export default class KbnServer { async listen() { await this.ready(); - const { server } = this; - await fromNode(cb => server.start(cb)); - if (isWorker) { // help parent process know when we are ready process.send(['WORKER_LISTENING']); } + const { server, config } = this; + server.log(['listening', 'info'], `Server running at ${server.info.uri}${ + config.get('server.rewriteBasePath') + ? config.get('server.basePath') + : '' + }`); return server; } @@ -171,7 +168,12 @@ export default class KbnServer { return await this.server.inject(opts); } - async applyLoggingConfiguration(config) { + applyLoggingConfiguration(settings) { + const config = new Config( + this.config.getSchema(), + transformDeprecations(settings) + ); + const loggingOptions = loggingConfiguration(config); const subset = { ops: config.get('ops'), diff --git a/src/test_utils/base_auth.js b/src/test_utils/base_auth.js deleted file mode 100644 index 270ed7563e7c1..0000000000000 --- a/src/test_utils/base_auth.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function header(user, pass) { - const encoded = new Buffer(`${user}:${pass}`).toString('base64'); - return `Basic ${encoded}`; -} diff --git a/src/test_utils/kbn_server.js b/src/test_utils/kbn_server.js deleted file mode 100644 index a0c802ce052e5..0000000000000 --- a/src/test_utils/kbn_server.js +++ /dev/null @@ -1,155 +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 { resolve } from 'path'; -import { defaultsDeep, set } from 'lodash'; -import { header as basicAuthHeader } from './base_auth'; -import { createEsTestCluster, esTestConfig, kibanaTestUser, kibanaServerTestUser } from '@kbn/test'; -import KbnServer from '../../src/server/kbn_server'; -import { ToolingLog } from '@kbn/dev-utils'; - -const DEFAULTS_SETTINGS = { - server: { - autoListen: true, - // Use the ephemeral port to make sure that tests use the first available - // port and aren't affected by the timing issues in test environment. - port: 0, - xsrf: { - disableProtection: true - } - }, - logging: { - quiet: true - }, - plugins: {}, - optimize: { - enabled: false - }, -}; - -const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { - plugins: { - scanDirs: [ - resolve(__dirname, '../core_plugins'), - ], - }, - elasticsearch: { - url: esTestConfig.getUrl(), - username: kibanaServerTestUser.username, - password: kibanaServerTestUser.password - }, -}; - -/** - * Creates an instance of KbnServer with default configuration - * tailored for unit tests - * - * @param {Object} [settings={}] Any config overrides for this instance - * @return {KbnServer} - */ -export function createServer(settings = {}) { - return new KbnServer(defaultsDeep({}, settings, DEFAULTS_SETTINGS)); -} - -/** - * Creates an instance of KbnServer, including all of the core plugins, - * with default configuration tailored for unit tests, and starts es. - * - * @param {Object} options - * @prop {Object} settings Any config overrides for this instance - * @prop {function} adjustTimeout A function(t) => this.timeout(t) that adjust the timeout of a test, - * ensuring the test properly waits for the server to boot without timing out. - * @return {KbnServer} - */ -export async function startTestServers({ adjustTimeout, settings = {} }) { - if (!adjustTimeout) { - throw new Error('adjustTimeout is required in order to avoid flaky tests'); - } - - const log = new ToolingLog({ - level: 'debug', - writeTo: process.stdout - }); - - log.indent(6); - log.info('starting elasticsearch'); - log.indent(4); - - const es = createEsTestCluster({ log }); - - log.indent(-4); - - adjustTimeout(es.getStartTimeout()); - - await es.start(); - - const kbnServer = createServerWithCorePlugins(settings); - - await kbnServer.ready(); - await kbnServer.server.plugins.elasticsearch.waitUntilReady(); - - return { - kbnServer, - es, - - async stop() { - await this.kbnServer.close(); - await es.cleanup(); - }, - }; -} - -/** - * Creates an instance of KbnServer, including all of the core plugins, - * with default configuration tailored for unit tests - * - * @param {Object} [settings={}] Any config overrides for this instance - * @return {KbnServer} - */ -export function createServerWithCorePlugins(settings = {}) { - return new KbnServer(defaultsDeep({}, settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS, DEFAULTS_SETTINGS)); -} - -/** - * Creates request configuration with a basic auth header - */ -export function authOptions() { - const { username, password } = kibanaTestUser; - const authHeader = basicAuthHeader(username, password); - return set({}, 'headers.Authorization', authHeader); -} - -/** - * Makes a request with test headers via hapi server inject() - * - * The given options are decorated with default testing options, so it's - * recommended to use this function instead of using inject() directly whenever - * possible throughout the tests. - * - * @param {KbnServer} kbnServer - * @param {object} options Any additional options or overrides for inject() - */ -export async function makeRequest(kbnServer, options) { - // Since all requests to Kibana hit core http server first and only after that - // are proxied to the "legacy" Kibana we should inject requests through the top - // level Hapi server used by the core. - return await kbnServer.newPlatform.proxyListener.root.server.http.service.httpServer.server.inject( - defaultsDeep({}, authOptions(), options) - ); -} diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts new file mode 100644 index 0000000000000..3b841c2b6ac02 --- /dev/null +++ b/src/test_utils/kbn_server.ts @@ -0,0 +1,184 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '@kbn/dev-utils'; +// @ts-ignore: implicit any for JS file +import { createEsTestCluster, esTestConfig, kibanaServerTestUser, kibanaTestUser } from '@kbn/test'; +import { defaultsDeep } from 'lodash'; +import { resolve } from 'path'; +import { BehaviorSubject } from 'rxjs'; +import supertest from 'supertest'; +import { Env } from '../core/server/config'; +import { LegacyObjectToConfigAdapter } from '../core/server/legacy_compat'; +import { Root } from '../core/server/root'; + +type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; + +const DEFAULTS_SETTINGS = { + server: { + autoListen: true, + // Use the ephemeral port to make sure that tests use the first available + // port and aren't affected by the timing issues in test environment. + port: 0, + xsrf: { disableProtection: true }, + }, + logging: { silent: true }, + plugins: {}, + optimize: { enabled: false }, +}; + +const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { + plugins: { scanDirs: [resolve(__dirname, '../core_plugins')] }, + elasticsearch: { + url: esTestConfig.getUrl(), + username: kibanaServerTestUser.username, + password: kibanaServerTestUser.password, + }, +}; + +export function createRootWithSettings(...settings: Array>) { + const env = Env.createDefault({ + configs: [], + cliArgs: { + dev: false, + quiet: false, + silent: false, + watch: false, + repl: false, + basePath: false, + }, + isDevClusterMaster: false, + }); + + return new Root( + new BehaviorSubject( + new LegacyObjectToConfigAdapter(defaultsDeep({}, ...settings, DEFAULTS_SETTINGS)) + ), + env + ); +} + +/** + * Returns supertest request attached to the core's internal native Node server. + * @param root + * @param method + * @param path + */ +function getSupertest(root: Root, method: HttpMethod, path: string) { + const testUserCredentials = new Buffer(`${kibanaTestUser.username}:${kibanaTestUser.password}`); + return supertest((root as any).server.http.service.httpServer.server.listener) + [method](path) + .set('Authorization', `Basic ${testUserCredentials.toString('base64')}`); +} + +/** + * Creates an instance of Root with default configuration + * tailored for unit tests. + * + * @param {Object} [settings={}] Any config overrides for this instance. + * @returns {Root} + */ +export function createRoot(settings = {}) { + return createRootWithSettings(settings); +} + +/** + * Creates an instance of Root, including all of the core plugins, + * with default configuration tailored for unit tests. + * + * @param {Object} [settings={}] Any config overrides for this instance. + * @returns {Root} + */ +export function createRootWithCorePlugins(settings = {}) { + return createRootWithSettings(settings, DEFAULT_SETTINGS_WITH_CORE_PLUGINS); +} + +/** + * Returns `kbnServer` instance used in the "legacy" Kibana. + * @param root + */ +export function getKbnServer(root: Root) { + return (root as any).server.legacy.service.kbnServer; +} + +export const request: Record< + HttpMethod, + (root: Root, path: string) => ReturnType +> = { + delete: (root, path) => getSupertest(root, 'delete', path), + get: (root, path) => getSupertest(root, 'get', path), + head: (root, path) => getSupertest(root, 'head', path), + post: (root, path) => getSupertest(root, 'post', path), + put: (root, path) => getSupertest(root, 'put', path), +}; + +/** + * Creates an instance of the Root, including all of the core "legacy" plugins, + * with default configuration tailored for unit tests, and starts es. + * + * @param options + * @prop settings Any config overrides for this instance. + * @prop adjustTimeout A function(t) => this.timeout(t) that adjust the timeout of a + * test, ensuring the test properly waits for the server to boot without timing out. + */ +export async function startTestServers({ + adjustTimeout, + settings = {}, +}: { + adjustTimeout: (timeout: number) => void; + settings: Record; +}) { + if (!adjustTimeout) { + throw new Error('adjustTimeout is required in order to avoid flaky tests'); + } + + const log = new ToolingLog({ + level: 'debug', + writeTo: process.stdout, + }); + + log.indent(6); + log.info('starting elasticsearch'); + log.indent(4); + + const es = createEsTestCluster({ log }); + + log.indent(-4); + + adjustTimeout(es.getStartTimeout()); + + await es.start(); + + const root = createRootWithCorePlugins(settings); + await root.start(); + + const kbnServer = getKbnServer(root); + await kbnServer.server.plugins.elasticsearch.waitUntilReady(); + + return { + kbnServer, + root, + es, + + async stop() { + await root.shutdown(); + await es.cleanup(); + }, + }; +} diff --git a/src/ui/__tests__/ui_exports_replace_injected_vars.js b/src/ui/__tests__/ui_exports_replace_injected_vars.js index b7762ef104b90..5cb05ac1dbeeb 100644 --- a/src/ui/__tests__/ui_exports_replace_injected_vars.js +++ b/src/ui/__tests__/ui_exports_replace_injected_vars.js @@ -25,10 +25,10 @@ import sinon from 'sinon'; import cheerio from 'cheerio'; import { noop } from 'lodash'; -import KbnServer from '../../server/kbn_server'; +import { createRoot, getKbnServer, request } from '../../test_utils/kbn_server'; const getInjectedVarsFromResponse = (resp) => { - const $ = cheerio.load(resp.payload); + const $ = cheerio.load(resp.text); const data = $('kbn-injected-metadata').attr('data'); return JSON.parse(data).legacyMetadata.vars; }; @@ -45,45 +45,46 @@ const injectReplacer = (kbnServer, replacer) => { }; describe('UiExports', function () { - describe('#replaceInjectedVars', function () { + let root; + let kbnServer; + before(async () => { this.slow(2000); - this.timeout(10000); - - let kbnServer; - beforeEach(async () => { - kbnServer = new KbnServer({ - server: { port: 0 }, // pick a random open port - logging: { silent: true }, // no logs - optimize: { enabled: false }, - plugins: { - paths: [resolve(__dirname, './fixtures/test_app')] // inject an app so we can hit /app/{id} - }, - }); + this.timeout(30000); - await kbnServer.ready(); - - // TODO: hopefully we can add better support for something - // like this in the new platform - kbnServer.server._requestor._decorations.getUiSettingsService = { - apply: undefined, - method() { - return { - getDefaults: noop, - getUserProvided: noop - }; - } - }; + root = root = createRoot({ + // inject an app so we can hit /app/{id} + plugins: { paths: [resolve(__dirname, './fixtures/test_app')] }, }); - afterEach(async () => { - await kbnServer.close(); - kbnServer = null; - }); + await root.start(); + + kbnServer = getKbnServer(root); + + // TODO: hopefully we can add better support for something + // like this in the new platform + kbnServer.server._requestor._decorations.getUiSettingsService = { + apply: undefined, + method: () => ({ getDefaults: noop, getUserProvided: noop }) + }; + }); + + after(async () => await root.shutdown()); + let originalInjectedVarsReplacers; + beforeEach(() => { + originalInjectedVarsReplacers = kbnServer.uiExports.injectedVarsReplacers; + }); + + afterEach(() => { + kbnServer.uiExports.injectedVarsReplacers = originalInjectedVarsReplacers; + }); + + describe('#replaceInjectedVars', function () { it('allows sync replacing of injected vars', async () => { injectReplacer(kbnServer, () => ({ a: 1 })); - const resp = await kbnServer.inject('/app/test_app'); + const resp = await request.get(root, '/app/test_app') + .expect(200); const injectedVars = getInjectedVarsFromResponse(resp); expect(injectedVars).to.eql({ a: 1 }); @@ -98,7 +99,8 @@ describe('UiExports', function () { }; }); - const resp = await kbnServer.inject('/app/test_app'); + const resp = await request.get(root, '/app/test_app') + .expect(200); const injectedVars = getInjectedVarsFromResponse(resp); expect(injectedVars).to.eql({ @@ -111,7 +113,8 @@ describe('UiExports', function () { injectReplacer(kbnServer, () => ({ foo: 'bar' })); injectReplacer(kbnServer, stub); - await kbnServer.inject('/app/test_app'); + await await request.get(root, '/app/test_app') + .expect(200); sinon.assert.calledOnce(stub); expect(stub.firstCall.args[0]).to.eql({ foo: 'bar' }); // originalInjectedVars @@ -126,7 +129,8 @@ describe('UiExports', function () { injectReplacer(kbnServer, orig => ({ name: orig.name + 'a' })); injectReplacer(kbnServer, orig => ({ name: orig.name + 'm' })); - const resp = await kbnServer.inject('/app/test_app'); + const resp = await request.get(root, '/app/test_app') + .expect(200); const injectedVars = getInjectedVarsFromResponse(resp); expect(injectedVars).to.eql({ name: 'sam' }); @@ -138,15 +142,17 @@ describe('UiExports', function () { throw new Error('replacer failed'); }); - const resp = await kbnServer.inject('/app/test_app'); - expect(resp).to.have.property('statusCode', 500); + await request.get(root, '/app/test_app') + .expect(500); }); it('starts off with the injected vars for the app merged with the default injected vars', async () => { const stub = sinon.stub(); injectReplacer(kbnServer, stub); - await kbnServer.inject('/app/test_app'); + await request.get(root, '/app/test_app') + .expect(200); + sinon.assert.calledOnce(stub); expect(stub.firstCall.args[0]).to.eql({ from_defaults: true, from_test_app: true }); }); diff --git a/src/ui/field_formats/__tests__/field_formats_mixin.js b/src/ui/field_formats/__tests__/field_formats_mixin.js index 58c61962c2414..3159705f3d8ed 100644 --- a/src/ui/field_formats/__tests__/field_formats_mixin.js +++ b/src/ui/field_formats/__tests__/field_formats_mixin.js @@ -22,31 +22,31 @@ import sinon from 'sinon'; import { FieldFormat } from '../field_format'; import * as FieldFormatsServiceNS from '../field_formats_service'; -import { createServer } from '../../../test_utils/kbn_server'; +import { fieldFormatsMixin } from '../field_formats_mixin'; describe('server.registerFieldFormat(createFormat)', () => { const sandbox = sinon.createSandbox(); - let kbnServer; + let registerFieldFormat; + let fieldFormatServiceFactory; + const serverMock = { decorate() {} }; beforeEach(async () => { - kbnServer = createServer(); - await kbnServer.ready(); + sandbox.stub(serverMock); + await fieldFormatsMixin({}, serverMock); + [[,, fieldFormatServiceFactory], [,, registerFieldFormat]] = serverMock.decorate.args; }); - afterEach(async () => { - sandbox.restore(); - await kbnServer.close(); - }); + afterEach(() => sandbox.restore()); it('throws if createFormat is not a function', () => { - expect(() => kbnServer.server.registerFieldFormat()).to.throwError(error => { + expect(() => registerFieldFormat()).to.throwError(error => { expect(error.message).to.match(/createFormat is not a function/i); }); }); it('calls the createFormat() function with the FieldFormat class', () => { const createFormat = sinon.stub(); - kbnServer.server.registerFieldFormat(createFormat); + registerFieldFormat(createFormat); sinon.assert.calledOnce(createFormat); sinon.assert.calledWithExactly(createFormat, sinon.match.same(FieldFormat)); }); @@ -61,9 +61,9 @@ describe('server.registerFieldFormat(createFormat)', () => { class FooFormat { static id = 'foo' } - kbnServer.server.registerFieldFormat(() => FooFormat); + registerFieldFormat(() => FooFormat); - const fieldFormats = await kbnServer.server.fieldFormatServiceFactory({ + const fieldFormats = await fieldFormatServiceFactory({ getAll: () => ({}), getDefaults: () => ({}) }); diff --git a/src/ui/tutorials_mixin.test.js b/src/ui/tutorials_mixin.test.js index b29ff10a4d798..6d0bc46aeb714 100644 --- a/src/ui/tutorials_mixin.test.js +++ b/src/ui/tutorials_mixin.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { createServer } from '../test_utils/kbn_server'; +import { tutorialsMixin } from './tutorials_mixin'; const validTutorial = { id: 'spec1', @@ -42,15 +42,22 @@ const validTutorial = { }; describe('tutorial mixins', () => { - - let kbnServer; + let getTutorials; + let registerTutorial; + let addScopedTutorialContextFactory; + const serverMock = { decorate: jest.fn() }; beforeEach(async () => { - kbnServer = createServer(); - await kbnServer.ready(); + await tutorialsMixin({}, serverMock); + + [ + [,, getTutorials], + [,, registerTutorial], + [,, addScopedTutorialContextFactory] + ] = serverMock.decorate.mock.calls; }); - afterEach(async () => { - await kbnServer.close(); + afterEach(() => { + jest.clearAllMocks(); }); describe('scoped context', () => { @@ -70,12 +77,12 @@ describe('tutorial mixins', () => { return tutorial; }; beforeEach(async () => { - kbnServer.server.addScopedTutorialContextFactory(spacesContextFactory); - kbnServer.server.registerTutorial(specProvider); + addScopedTutorialContextFactory(spacesContextFactory); + registerTutorial(specProvider); }); test('passes scoped context to specProviders', () => { - const tutorials = kbnServer.server.getTutorials(mockRequest); + const tutorials = getTutorials(mockRequest); expect(tutorials.length).toBe(1); expect(tutorials[0].shortDescription).toBe('I have been provided with scoped context, spaceId: my-space'); }); diff --git a/x-pack/package.json b/x-pack/package.json index 17cef7c7d79ba..c2bd527701fed 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -69,8 +69,8 @@ "run-sequence": "^2.2.1", "simple-git": "1.37.0", "sinon": "^5.0.7", - "supertest": "3.0.0", - "supertest-as-promised": "4.0.2", + "supertest": "^3.1.0", + "supertest-as-promised": "^4.0.2", "tmp": "0.0.31", "tree-kill": "^1.1.0", "typescript": "^2.9.2", diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 41adacfd61a70..7c73f4699a7ec 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -7479,7 +7479,7 @@ subtext@4.x.x: pez "2.x.x" wreck "12.x.x" -superagent@^3.0.0: +superagent@3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" dependencies: @@ -7494,19 +7494,19 @@ superagent@^3.0.0: qs "^6.5.1" readable-stream "^2.0.5" -supertest-as-promised@4.0.2: +supertest-as-promised@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/supertest-as-promised/-/supertest-as-promised-4.0.2.tgz#0464f2bd256568d4a59bce84269c0548f6879f1a" dependencies: bluebird "^3.3.1" methods "^1.1.1" -supertest@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" +supertest@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" dependencies: methods "~1.1.2" - superagent "^3.0.0" + superagent "3.8.2" supports-color@1.2.0: version "1.2.0" diff --git a/yarn.lock b/yarn.lock index 25c1cab071a68..8c22fee2635e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -537,9 +537,9 @@ "@types/cookiejar" "*" "@types/node" "*" -"@types/supertest@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.4.tgz#28770e13293365e240a842d7d5c5a1b3d2dee593" +"@types/supertest@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.5.tgz#18d082a667eaed22759be98f4923e0061ae70c62" dependencies: "@types/superagent" "*" @@ -12703,7 +12703,7 @@ suffix@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/suffix/-/suffix-0.1.1.tgz#cc58231646a0ef1102f79478ef3a9248fd9c842f" -superagent@^3.0.0: +superagent@3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" dependencies: @@ -12718,19 +12718,19 @@ superagent@^3.0.0: qs "^6.5.1" readable-stream "^2.0.5" -supertest-as-promised@4.0.2: +supertest-as-promised@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/supertest-as-promised/-/supertest-as-promised-4.0.2.tgz#0464f2bd256568d4a59bce84269c0548f6879f1a" dependencies: bluebird "^3.3.1" methods "^1.1.1" -supertest@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" +supertest@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" dependencies: methods "~1.1.2" - superagent "^3.0.0" + superagent "3.8.2" supports-color@3.1.2: version "3.1.2" From 81096fda70d0f32a6dec1ce64b434c2e7a04872f Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 11:52:03 -0400 Subject: [PATCH 32/68] attempt to isolate test failure --- scripts/functional_tests.js | 4 ++-- x-pack/scripts/functional_tests.js | 16 ++++++++-------- x-pack/test/functional/config.js | 13 +++++++------ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 9d73923704ce6..88b89f84bcd17 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -19,7 +19,7 @@ require('../src/setup_node_env'); require('@kbn/test').runTestsCli([ - require.resolve('../test/functional/config.js'), + // require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), - require.resolve('../test/plugin_functional/config.js'), + // require.resolve('../test/plugin_functional/config.js'), ]); diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 1b05203e4dd09..742582e9b48b6 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -6,13 +6,13 @@ require('@kbn/plugin-helpers').babelRegister(); require('@kbn/test').runTestsCli([ - require.resolve('../test/reporting/configs/chromium_api.js'), - require.resolve('../test/reporting/configs/chromium_functional.js'), - require.resolve('../test/reporting/configs/phantom_api.js'), - require.resolve('../test/reporting/configs/phantom_functional.js'), + // require.resolve('../test/reporting/configs/chromium_api.js'), + // require.resolve('../test/reporting/configs/chromium_functional.js'), + // require.resolve('../test/reporting/configs/phantom_api.js'), + // require.resolve('../test/reporting/configs/phantom_functional.js'), require.resolve('../test/functional/config.js'), - require.resolve('../test/api_integration/config.js'), - require.resolve('../test/saml_api_integration/config.js'), - require.resolve('../test/rbac_api_integration/config.js'), - require.resolve('../test/spaces_api_integration/config.js'), + // require.resolve('../test/api_integration/config.js'), + // require.resolve('../test/saml_api_integration/config.js'), + // require.resolve('../test/rbac_api_integration/config.js'), + // require.resolve('../test/spaces_api_integration/config.js'), ]); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 77c0aee49b83e..1d2010c7f38f0 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -59,13 +59,13 @@ export default async function ({ readConfigFile }) { return { // list paths to the files that contain your plugins tests testFiles: [ - resolve(__dirname, './apps/graph'), - resolve(__dirname, './apps/monitoring'), - resolve(__dirname, './apps/watcher'), + // resolve(__dirname, './apps/graph'), + // resolve(__dirname, './apps/monitoring'), + // resolve(__dirname, './apps/watcher'), resolve(__dirname, './apps/dashboard_mode'), - resolve(__dirname, './apps/security'), - resolve(__dirname, './apps/logstash'), - resolve(__dirname, './apps/grok_debugger'), + // resolve(__dirname, './apps/security'), + // resolve(__dirname, './apps/logstash'), + // resolve(__dirname, './apps/grok_debugger'), ], // define the name and providers for services that should be @@ -133,6 +133,7 @@ export default async function ({ readConfigFile }) { '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', '--xpack.xpack_main.telemetry.enabled=false', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions + '--logging.verbose=true', ], }, From ec718838de85e6fbed3cc5c9de12c6e6a5427a70 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 12:50:26 -0400 Subject: [PATCH 33/68] [Spaces] - copy edits (#22457) --- .../edit_role/components/edit_role_page.tsx | 23 +++++++++---- .../index_privilege_form.test.tsx.snap | 2 +- .../privileges/es/index_privilege_form.tsx | 2 +- .../impacted_spaces_flyout.test.tsx.snap | 2 +- .../privilege_space_form.test.tsx.snap | 2 +- .../space_aware_privilege_form.test.tsx.snap | 26 +++++++++----- .../space_selector.test.tsx.snap | 1 - .../kibana/impacted_spaces_flyout.tsx | 4 +-- .../kibana/privilege_callout_warning.tsx | 6 +++- .../kibana/privilege_space_form.tsx | 2 +- .../kibana/privilege_space_table.tsx | 2 +- .../kibana/space_aware_privilege_form.tsx | 34 +++++++------------ .../privileges/kibana/space_selector.tsx | 1 - .../components/manage_spaces_button.tsx | 2 +- x-pack/plugins/spaces/public/lib/constants.ts | 2 ++ .../plugins/spaces/public/register_feature.js | 3 +- .../space_identifier.test.js.snap | 16 +++++++-- .../edit_space/manage_space_page.js | 4 +-- .../management/edit_space/space_identifier.js | 14 +++++--- .../views/management/lib/validate_space.js | 10 +++--- .../management/lib/validate_space.test.js | 14 ++++---- .../spaces_grid/spaces_grid_page.js | 8 +++-- .../spaces_description.test.tsx.snap | 2 +- .../components/spaces_description.tsx | 6 ++-- .../__snapshots__/space_selector.test.js.snap | 2 +- .../views/space_selector/space_selector.js | 2 +- .../spaces/server/lib/create_default_space.js | 4 +-- .../server/lib/create_default_space.test.js | 2 +- 28 files changed, 116 insertions(+), 82 deletions(-) diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx index d4497582a7511..ee1de5d99e191 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; import { get } from 'lodash'; -import React, { ChangeEvent, Component, HTMLProps } from 'react'; +import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; import { toastNotifications } from 'ui/notify'; import { Space } from '../../../../../../spaces/common/model/space'; import { IndexPrivilege } from '../../../../../common/model/index_privilege'; @@ -66,18 +66,29 @@ export class EditRolePage extends Component { } public render() { + const description = this.props.spacesEnabled + ? `Set privileges on your Elasticsearch data and control access to your Kibana spaces.` + : `Set privileges on your Elasticsearch data and control access to Kibana.`; + return ( {this.getFormTitle()} + + + {description} + {isReservedRole(this.props.role) && ( - -

- Reserved roles are built-in and cannot be removed or modified. -

- + + + +

+ Reserved roles are built-in and cannot be removed or modified. +

+
+
)} diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap index d7426919a1f3b..c8a8f284b9bad 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap @@ -162,7 +162,7 @@ exports[`it renders without crashing 1`] = ` diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx index 4072d3b2f9008..ccf5bd42722e0 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx @@ -171,7 +171,7 @@ export class IndexPrivilegeForm extends Component { renders without crashing 1`] = ` onClick={[Function]} type="button" > - See summary of all spaces privileges + View summary of spaces privileges diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap index afce653a9c7f4..ad9c209ec5255 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap @@ -19,7 +19,7 @@ exports[` renders without crashing 1`] = ` fullWidth={false} hasEmptyLabelSpace={false} isInvalid={false} - label="Space(s)" + label="Spaces" > renders without crashing 1`] = ` - Specifies the lowest permission level for all spaces, unless a custom privilege is specified. + Specify the minimum actions users can perform in your spaces.

} fullWidth={false} gutterSize="l" title={

- Minimum privilege + Minimum privileges for all spaces

} titleSize="xs" @@ -63,7 +63,7 @@ exports[` renders without crashing 1`] = ` describedByIds={Array []} fullWidth={false} hasEmptyLabelSpace={true} - helpText="No access" + helpText="No access to spaces" > renders without crashing 1`] = ` size="xs" >

- Space privileges + Higher privileges for individual spaces

renders without crashing 1`] = ` size="s" >

- Customize permission levels per space. If a space is not customized, its permissions will default to the minimum privilege specified above. -

-

- You can bulk-create space privileges though they will be saved individually upon saving the role. + Grant more privileges on a per space basis. For example, if the privileges are + + + read + + for all spaces, you can set the privileges to + + all + + + for an individual space.

+ {
- See summary of all spaces privileges + View summary of spaces privileges
{flyout} @@ -105,7 +105,7 @@ export class ImpactedSpacesFlyout extends Component { > -

Summary of all space privileges

+

Summary of space privileges

diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx index 6641f43074cbe..0b5666f391573 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx @@ -77,7 +77,11 @@ export class PrivilegeCalloutWarning extends Component { + The minimal possible privilege is read. +
+ } /> ); } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx index 5cd139c0f42e3..cf7bcf8287b9d 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx @@ -41,7 +41,7 @@ export class PrivilegeSpaceForm extends Component { { search={{ box: { incremental: true, - placeholder: 'Filter...', + placeholder: 'Filter', }, onChange: (search: any) => { this.setState({ diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx index af76691b060f5..060f89726bb72 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx @@ -79,25 +79,23 @@ export class SpaceAwarePrivilegeForm extends Component { const basePrivilege = assignedPrivileges.global.length > 0 ? assignedPrivileges.global[0] : NO_PRIVILEGE_VALUE; - const description = ( -

- Specifies the lowest permission level for all spaces, unless a custom privilege is - specified. -

- ); + const description =

Specify the minimum actions users can perform in your spaces.

; let helptext; if (basePrivilege === NO_PRIVILEGE_VALUE) { - helptext = 'No access'; + helptext = 'No access to spaces'; } else if (basePrivilege === 'all') { - helptext = 'View, edit, and share all objects and apps within all spaces'; + helptext = 'View, edit, and share objects and apps within all spaces'; } else if (basePrivilege === 'read') { - helptext = 'View only mode'; + helptext = 'View objects and apps within all spaces'; } return ( - Minimum privilege} description={description}> + Minimum privileges for all spaces} + description={description} + > { return ( -

Space privileges

+

Higher privileges for individual spaces

{ color={'subdued'} >

- Customize permission levels per space. If a space is not customized, its permissions - will default to the minimum privilege specified above. + Grant more privileges on a per space basis. For example, if the privileges are{' '} + read for all spaces, you can set the privileges to all{' '} + for an individual space.

- {basePrivilege !== 'all' && - this.props.editable && ( -

- You can bulk-create space privileges though they will be saved individually upon - saving the role. -

- )}
- + {(basePrivilege !== NO_PRIVILEGE_VALUE || isReservedRole(this.props.role)) && ( { return ( { onClick={this.navigateToManageSpaces} style={this.props.style} > - Manage Spaces + Manage spaces ); } diff --git a/x-pack/plugins/spaces/public/lib/constants.ts b/x-pack/plugins/spaces/public/lib/constants.ts index 59ac4b3aa64c3..93f21f0e46629 100644 --- a/x-pack/plugins/spaces/public/lib/constants.ts +++ b/x-pack/plugins/spaces/public/lib/constants.ts @@ -6,4 +6,6 @@ import chrome from 'ui/chrome'; +export const SPACES_FEATURE_DESCRIPTION = `Organize your dashboards and other saved objects into meaningful categories.`; + export const MANAGE_SPACES_URL = chrome.addBasePath(`/app/kibana#/management/spaces/list`); diff --git a/x-pack/plugins/spaces/public/register_feature.js b/x-pack/plugins/spaces/public/register_feature.js index 09b896c9caa35..1a02313aa331d 100644 --- a/x-pack/plugins/spaces/public/register_feature.js +++ b/x-pack/plugins/spaces/public/register_feature.js @@ -7,12 +7,13 @@ import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import { SPACES_FEATURE_DESCRIPTION } from './lib/constants'; FeatureCatalogueRegistryProvider.register(() => { return { id: 'spaces', title: 'Spaces', - description: 'Organize your dashboards, visualizations, and other saved objects', + description: SPACES_FEATURE_DESCRIPTION, icon: 'spacesApp', path: '/app/kibana#/management/spaces/list', showOnHomePage: true, diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap index db250113e044d..a88906a10237e 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap +++ b/x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/space_identifier.test.js.snap @@ -8,13 +8,23 @@ exports[`renders without crashing 1`] = ` hasEmptyLabelSpace={false} helpText={

- Links within Kibana will include this space identifier + If the identifier is + + engineering + + , the Kibana URL is +
+ https://my-kibana.example + + /s/engineering/ + + app/kibana.

} isInvalid={false} label={

- Space Identifier + URL identifier diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js index 606c7d9ad1c7a..a6afad557cc3e 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js +++ b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js @@ -311,9 +311,9 @@ export class ManageSpacePage extends Component { } action - .then(result => { + .then(() => { this.props.spacesNavState.refreshSpacesList(); - toastNotifications.addSuccess(`Saved '${result.data.name}'`); + toastNotifications.addSuccess(`'${name}' was saved`); window.location.hash = `#/management/spaces/list`; }) .catch(error => { diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js index 9fcd7460114b0..4af9003678db1 100644 --- a/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js +++ b/x-pack/plugins/spaces/public/views/management/edit_space/space_identifier.js @@ -29,11 +29,15 @@ export class SpaceIdentifier extends Component { this.textFieldRef = ref} @@ -45,15 +49,15 @@ export class SpaceIdentifier extends Component { getLabel = () => { if (!this.props.editable) { - return (

Space Identifier

); + return (

URL identifier

); } const editLinkText = this.state.editing ? `[stop editing]` : `[edit]`; - return (

Space Identifier {editLinkText}

); + return (

URL identifier {editLinkText}

); }; getHelpText = () => { - return (

Links within Kibana will include this space identifier

); + return (

If the identifier is engineering, the Kibana URL is
https://my-kibana.example/s/engineering/app/kibana.

); }; onEditClick = () => { diff --git a/x-pack/plugins/spaces/public/views/management/lib/validate_space.js b/x-pack/plugins/spaces/public/views/management/lib/validate_space.js index 56d22ca39aad5..1aa98aac8e579 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/validate_space.js +++ b/x-pack/plugins/spaces/public/views/management/lib/validate_space.js @@ -23,7 +23,7 @@ export class SpaceValidator { if (!this._shouldValidate) return valid(); if (!space.name) { - return invalid(`Please provide a space name`); + return invalid(`Name is required`); } if (space.name.length > 1024) { @@ -43,17 +43,17 @@ export class SpaceValidator { return valid(); } - validateSpaceIdentifier(space) { + validateURLIdentifier(space) { if (!this._shouldValidate) return valid(); if (isReservedSpace(space)) return valid(); if (!space.id) { - return invalid(`Space Identifier is required`); + return invalid(`URL identifier is required`); } if (!isValidSpaceIdentifier(space.id)) { - return invalid('Space Identifier only allows a-z, 0-9, "_", and the "-" character'); + return invalid('URL identifier can only contain a-z, 0-9, and the characters "_" and "-"'); } return valid(); @@ -62,7 +62,7 @@ export class SpaceValidator { validateForSave(space) { const { isInvalid: isNameInvalid } = this.validateSpaceName(space); const { isInvalid: isDescriptionInvalid } = this.validateSpaceDescription(space); - const { isInvalid: isIdentifierInvalid } = this.validateSpaceIdentifier(space); + const { isInvalid: isIdentifierInvalid } = this.validateURLIdentifier(space); if (isNameInvalid || isDescriptionInvalid || isIdentifierInvalid) { return invalid(); diff --git a/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js b/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js index 5adbb2ec5090e..dac3e417a371c 100644 --- a/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js +++ b/x-pack/plugins/spaces/public/views/management/lib/validate_space.test.js @@ -26,7 +26,7 @@ describe('validateSpaceName', () => { name: '' }; - expect(validator.validateSpaceName(space)).toEqual({ isInvalid: true, error: `Please provide a space name` }); + expect(validator.validateSpaceName(space)).toEqual({ isInvalid: true, error: `Name is required` }); }); test('it cannot exceed 1024 characters', () => { @@ -55,13 +55,13 @@ describe('validateSpaceDescription', () => { }); }); -describe('validateSpaceIdentifier', () => { +describe('validateURLIdentifier', () => { test('it does not validate reserved spaces', () => { const space = { _reserved: true }; - expect(validator.validateSpaceIdentifier(space)).toEqual({ isInvalid: false }); + expect(validator.validateURLIdentifier(space)).toEqual({ isInvalid: false }); }); test('it requires a non-empty value', () => { @@ -69,7 +69,7 @@ describe('validateSpaceIdentifier', () => { id: '' }; - expect(validator.validateSpaceIdentifier(space)).toEqual({ isInvalid: true, error: `Space Identifier is required` }); + expect(validator.validateURLIdentifier(space)).toEqual({ isInvalid: true, error: `URL identifier is required` }); }); test('it requires a valid Space Identifier', () => { @@ -77,8 +77,8 @@ describe('validateSpaceIdentifier', () => { id: 'invalid identifier' }; - expect(validator.validateSpaceIdentifier(space)) - .toEqual({ isInvalid: true, error: 'Space Identifier only allows a-z, 0-9, "_", and the "-" character' }); + expect(validator.validateURLIdentifier(space)) + .toEqual({ isInvalid: true, error: 'URL identifier can only contain a-z, 0-9, and the characters "_" and "-"' }); }); test('it allows a valid Space Identifier', () => { @@ -86,6 +86,6 @@ describe('validateSpaceIdentifier', () => { id: '01-valid-context-01' }; - expect(validator.validateSpaceIdentifier(space)).toEqual({ isInvalid: false }); + expect(validator.validateURLIdentifier(space)).toEqual({ isInvalid: false }); }); }); diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js index 8a5c3ecd9ae81..93e2dbe722bf7 100644 --- a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js +++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.js @@ -54,7 +54,11 @@ export class SpacesGridPage extends Component { onSelectionChange: this.onSelectionChange }} pagination={true} - search={true} + search={{ + box: { + placeholder: 'Search' + } + }} loading={this.state.loading} message={this.state.loading ? "loading..." : undefined} /> @@ -76,7 +80,7 @@ export class SpacesGridPage extends Component { } return ( - { window.location.hash = `#/management/spaces/create`; }}>Create new space + { window.location.hash = `#/management/spaces/create`; }}>Create space ); } diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap b/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap index 5f439351549c1..b46ae0481840a 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap +++ b/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap @@ -12,7 +12,7 @@ exports[`SpacesDescription renders without crashing 1`] = ` grow={true} >

- Use Spaces within Kibana to organize your Dashboards, Visualizations, and other saved objects. + Organize your dashboards and other saved objects into meaningful categories.

{ @@ -18,10 +19,7 @@ export const SpacesDescription: SFC = () => { return ( -

- Use Spaces within Kibana to organize your Dashboards, Visualizations, and other saved - objects. -

+

{SPACES_FEATURE_DESCRIPTION}

diff --git a/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap b/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap index 3f58530d7139b..21662970fe240 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap +++ b/x-pack/plugins/spaces/public/views/space_selector/__snapshots__/space_selector.test.js.snap @@ -73,7 +73,7 @@ exports[`it renders without crashing 1`] = ` className="euiText euiText--extraSmall" >

- You can change your space at anytime from within Kibana. + You can change your space at anytime.

diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js index f90d050fcf860..3508bdefe3b0b 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.js +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.js @@ -90,7 +90,7 @@ export class SpaceSelector extends Component { {this.getSearchField()} -

You can change your space at anytime from within Kibana.

+

You can change your space at anytime.

diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.js b/x-pack/plugins/spaces/server/lib/create_default_space.js index fc19ee325d14f..949e1fd8bbf2a 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.js +++ b/x-pack/plugins/spaces/server/lib/create_default_space.js @@ -25,8 +25,8 @@ export async function createDefaultSpace(server) { }; await savedObjectsRepository.create('space', { - name: 'Default Space', - description: 'This is your Default Space!', + name: 'Default', + description: 'This is your default space!', _reserved: true }, options); } diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.test.js b/x-pack/plugins/spaces/server/lib/create_default_space.test.js index 1d5a243315e92..874c5023b197f 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.test.js +++ b/x-pack/plugins/spaces/server/lib/create_default_space.test.js @@ -78,7 +78,7 @@ test(`it creates the default space when one does not exist`, async () => { expect(repository.create).toHaveBeenCalledTimes(1); expect(repository.create).toHaveBeenCalledWith( 'space', - { "_reserved": true, "description": "This is your Default Space!", "name": "Default Space" }, + { "_reserved": true, "description": "This is your default space!", "name": "Default" }, { "id": "default" } ); }); From 4d83cfde56009b7e683a5dd7b5d7256ac32d575b Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 6 Sep 2018 20:34:26 +0300 Subject: [PATCH 34/68] [Tools] Fix line breaks in default JSON serializer (#22653) * [Tools] Fix line breaks in default JSON serializer * Add test for not escaped line breaks --- src/dev/i18n/__snapshots__/utils.test.js.snap | 7 ++++++- src/dev/i18n/utils.js | 3 +-- src/dev/i18n/utils.test.js | 13 ++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/dev/i18n/__snapshots__/utils.test.js.snap b/src/dev/i18n/__snapshots__/utils.test.js.snap index 85e61058072b1..2a2f196d3f13f 100644 --- a/src/dev/i18n/__snapshots__/utils.test.js.snap +++ b/src/dev/i18n/__snapshots__/utils.test.js.snap @@ -1,3 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`i18n utils should escape linebreaks 1`] = `"Text with\\\\n\\\\n\\\\nline-breaks and \\\\n\\\\n\\\\n \\\\n\\\\n\\\\n "`; +exports[`i18n utils should not escape linebreaks 1`] = ` +"Text + with + line-breaks +" +`; diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index af01c4721f14a..658c1cbe67177 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -30,7 +30,6 @@ import { promisify } from 'util'; const ESCAPE_LINE_BREAK_REGEX = /(? { test('should remove escaped linebreak', () => { expect(formatJSString('Test\\\n str\\\ning')).toEqual('Test string'); }); - - test('should escape linebreaks', () => { + test('should not escape linebreaks', () => { expect( - formatJSString(`Text with - - -line-breaks and \n\n - \n\n - `) + formatJSString(`Text \n with + line-breaks +`) ).toMatchSnapshot(); }); - test('should detect i18n translate function call', () => { let source = i18nTranslateSources[0]; let expressionStatementNode = [...traverseNodes(parse(source).program.body)].find(node => From 557bbcd26d83c6e68e46f546d15a745a8cb2fc73 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 14:04:33 -0400 Subject: [PATCH 35/68] don't render space selector on login screen --- .../spaces/public/views/nav_control/nav_control.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control.js b/x-pack/plugins/spaces/public/views/nav_control/nav_control.js index 9dfcaf8ede84a..e0d3cfa90f483 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control.js +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control.js @@ -30,11 +30,19 @@ module.controller('spacesNavController', ($scope, $http, chrome, activeSpace) => spacesManager = new SpacesManager($http, chrome); - render(, domNode); + let mounted = false; + + $scope.$parent.$watch('isVisible', function (isVisible) { + if (isVisible && !mounted) { + render(, domNode); + mounted = true; + } + }); // unmount react on controller destroy $scope.$on('$destroy', () => { unmountComponentAtNode(domNode); + mounted = false; }); }); From b6c5f115e69422a851a45631654ff7d8002e38bb Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 17:56:45 -0400 Subject: [PATCH 36/68] run all tests again --- x-pack/scripts/functional_tests.js | 16 ++++++++-------- x-pack/test/functional/config.js | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 742582e9b48b6..1b05203e4dd09 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -6,13 +6,13 @@ require('@kbn/plugin-helpers').babelRegister(); require('@kbn/test').runTestsCli([ - // require.resolve('../test/reporting/configs/chromium_api.js'), - // require.resolve('../test/reporting/configs/chromium_functional.js'), - // require.resolve('../test/reporting/configs/phantom_api.js'), - // require.resolve('../test/reporting/configs/phantom_functional.js'), + require.resolve('../test/reporting/configs/chromium_api.js'), + require.resolve('../test/reporting/configs/chromium_functional.js'), + require.resolve('../test/reporting/configs/phantom_api.js'), + require.resolve('../test/reporting/configs/phantom_functional.js'), require.resolve('../test/functional/config.js'), - // require.resolve('../test/api_integration/config.js'), - // require.resolve('../test/saml_api_integration/config.js'), - // require.resolve('../test/rbac_api_integration/config.js'), - // require.resolve('../test/spaces_api_integration/config.js'), + require.resolve('../test/api_integration/config.js'), + require.resolve('../test/saml_api_integration/config.js'), + require.resolve('../test/rbac_api_integration/config.js'), + require.resolve('../test/spaces_api_integration/config.js'), ]); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 1d2010c7f38f0..23fda2b632263 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -59,13 +59,13 @@ export default async function ({ readConfigFile }) { return { // list paths to the files that contain your plugins tests testFiles: [ - // resolve(__dirname, './apps/graph'), - // resolve(__dirname, './apps/monitoring'), - // resolve(__dirname, './apps/watcher'), + resolve(__dirname, './apps/graph'), + resolve(__dirname, './apps/monitoring'), + resolve(__dirname, './apps/watcher'), resolve(__dirname, './apps/dashboard_mode'), - // resolve(__dirname, './apps/security'), - // resolve(__dirname, './apps/logstash'), - // resolve(__dirname, './apps/grok_debugger'), + resolve(__dirname, './apps/security'), + resolve(__dirname, './apps/logstash'), + resolve(__dirname, './apps/grok_debugger'), ], // define the name and providers for services that should be From ce6bd301788fdd6900570486fb4139f4d1859153 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 18:05:32 -0400 Subject: [PATCH 37/68] run all oss tests again --- scripts/functional_tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 88b89f84bcd17..9d73923704ce6 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -19,7 +19,7 @@ require('../src/setup_node_env'); require('@kbn/test').runTestsCli([ - // require.resolve('../test/functional/config.js'), + require.resolve('../test/functional/config.js'), require.resolve('../test/api_integration/config.js'), - // require.resolve('../test/plugin_functional/config.js'), + require.resolve('../test/plugin_functional/config.js'), ]); From 03553dedbc12090d1b99505036da1a8ab106803f Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 6 Sep 2018 18:31:47 -0500 Subject: [PATCH 38/68] [ci] Run ES snapshot when possible (#22663) For instances where we would otherwise be running from source in a tracked elastic/elasticsearch branch, let's use a snapshot instead. This will eliminate some gradle issues we are experiencing in master. --- src/dev/ci_setup/git_setup.sh | 1 + tasks/config/run.js | 11 ++++++----- test/scripts/jenkins_cloud.sh | 2 +- test/scripts/jenkins_selenium.sh | 1 + test/scripts/jenkins_unit.sh | 2 +- test/scripts/jenkins_xpack.sh | 4 ++-- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/dev/ci_setup/git_setup.sh b/src/dev/ci_setup/git_setup.sh index b5e6902e2f258..d0e2f3ffd87e4 100755 --- a/src/dev/ci_setup/git_setup.sh +++ b/src/dev/ci_setup/git_setup.sh @@ -70,6 +70,7 @@ function checkout_sibling { cloneBranch="$PR_TARGET_BRANCH" if clone_target_is_valid ; then + export TEST_ES_FROM=snapshot return 0 fi diff --git a/tasks/config/run.js b/tasks/config/run.js index 5c17bd408dbd5..6c037e702c47e 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -60,6 +60,7 @@ module.exports = function (grunt) { '--server.port=5610', ]; + const esFrom = process.env.TEST_ES_FROM || 'source'; return { // used by the test and jenkins:unit tasks // runs the eslint script to check for linting errors @@ -165,7 +166,7 @@ module.exports = function (grunt) { args: [ 'scripts/functional_tests', '--config', 'test/api_integration/config.js', - '--esFrom', 'source', + '--esFrom', esFrom, '--bail', '--debug', ], @@ -177,7 +178,7 @@ module.exports = function (grunt) { 'scripts/functional_tests', '--config', 'test/server_integration/http/ssl/config.js', '--config', 'test/server_integration/http/ssl_redirect/config.js', - '--esFrom', 'source', + '--esFrom', esFrom, '--bail', '--debug', '--kibana-install-dir', `./build/oss/kibana-${PKG_VERSION}-${process.platform}-x86_64`, @@ -189,7 +190,7 @@ module.exports = function (grunt) { args: [ 'scripts/functional_tests', '--config', 'test/plugin_functional/config.js', - '--esFrom', 'source', + '--esFrom', esFrom, '--bail', '--debug', '--kibana-install-dir', `./build/oss/kibana-${PKG_VERSION}-${process.platform}-x86_64`, @@ -203,7 +204,7 @@ module.exports = function (grunt) { args: [ 'scripts/functional_tests', '--config', 'test/functional/config.js', - '--esFrom', 'source', + '--esFrom', esFrom, '--bail', '--debug', '--', @@ -216,7 +217,7 @@ module.exports = function (grunt) { args: [ 'scripts/functional_tests', '--config', 'test/functional/config.js', - '--esFrom', 'source', + '--esFrom', esFrom, '--bail', '--debug', '--kibana-install-dir', `./build/oss/kibana-${PKG_VERSION}-${process.platform}-x86_64`, diff --git a/test/scripts/jenkins_cloud.sh b/test/scripts/jenkins_cloud.sh index 1020fbe924290..25f683661232b 100755 --- a/test/scripts/jenkins_cloud.sh +++ b/test/scripts/jenkins_cloud.sh @@ -4,7 +4,7 @@ # # The cloud instance setup is done in the elastic/elastic-stack-testing framework, # where the following environment variables are set pointing to the cloud instance. -# +# # export TEST_KIBANA_HOSTNAME # export TEST_KIBANA_PROTOCOL= # export TEST_KIBANA_PORT= diff --git a/test/scripts/jenkins_selenium.sh b/test/scripts/jenkins_selenium.sh index 7d6b26a59c485..e95be0073fe63 100755 --- a/test/scripts/jenkins_selenium.sh +++ b/test/scripts/jenkins_selenium.sh @@ -7,4 +7,5 @@ source "$(dirname $0)/../../src/dev/ci_setup/java_setup.sh" node scripts/build --release --debug --oss; +export TEST_ES_FROM=${TEST_ES_FROM:-source} xvfb-run "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:selenium --from=source; diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 0ce41c45110fd..4364facc45224 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -5,5 +5,5 @@ source "$(dirname $0)/../../src/dev/ci_setup/setup.sh" source "$(dirname $0)/../../src/dev/ci_setup/git_setup.sh" source "$(dirname $0)/../../src/dev/ci_setup/java_setup.sh" -export TEST_ES_FROM=source +export TEST_ES_FROM=${TEST_ES_FROM:-source} xvfb-run "$(FORCE_COLOR=0 yarn bin)/grunt" jenkins:unit --from=source; diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index b1837d03c8522..6172db83a5de5 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -34,9 +34,9 @@ installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 - +export TEST_ES_FROM=${TEST_ES_FROM:-source} echo " -> Running functional and api tests" cd "$XPACK_DIR" -xvfb-run node scripts/functional_tests --debug --bail --kibana-install-dir "$installDir" --esFrom=source +xvfb-run node scripts/functional_tests --debug --bail --kibana-install-dir "$installDir" echo "" echo "" From df74a824a5fec02f158d54dbb8dda989968fe3c1 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Thu, 6 Sep 2018 19:40:29 -0400 Subject: [PATCH 39/68] update role api tests --- .../api_integration/apis/security/roles.js | 50 ++++++------------- x-pack/test/functional/config.js | 1 - 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/x-pack/test/api_integration/apis/security/roles.js b/x-pack/test/api_integration/apis/security/roles.js index f77ea88bde2c8..7e05e11944a0c 100644 --- a/x-pack/test/api_integration/apis/security/roles.js +++ b/x-pack/test/api_integration/apis/security/roles.js @@ -32,7 +32,7 @@ export default function ({ getService }) { { field_security: { grant: ['*'], - except: [ 'geo.*' ] + except: ['geo.*'] }, names: ['logstash-*'], privileges: ['read', 'view_index_metadata'], @@ -41,14 +41,10 @@ export default function ({ getService }) { ], run_as: ['watcher_user'], }, - kibana: [ - { - privileges: ['all'], - }, - { - privileges: ['read'], - }, - ], + kibana: { + global: ['all', 'read'], + space: {} + } }) .expect(204); @@ -62,7 +58,7 @@ export default function ({ getService }) { privileges: ['read', 'view_index_metadata'], field_security: { grant: ['*'], - except: [ 'geo.*' ] + except: ['geo.*'] }, query: `{ "match": { "geo.src": "CN" } }`, }, @@ -70,12 +66,7 @@ export default function ({ getService }) { applications: [ { application: 'kibana-.kibana', - privileges: ['all'], - resources: ['*'], - }, - { - application: 'kibana-.kibana', - privileges: ['read'], + privileges: ['all', 'read'], resources: ['*'], } ], @@ -102,8 +93,8 @@ export default function ({ getService }) { names: ['beats-*'], privileges: ['write'], field_security: { - grant: [ 'request.*' ], - except: [ 'response.*' ] + grant: ['request.*'], + except: ['response.*'] }, query: `{ "match": { "host.name": "localhost" } }`, }, @@ -139,7 +130,7 @@ export default function ({ getService }) { { field_security: { grant: ['*'], - except: [ 'geo.*' ] + except: ['geo.*'] }, names: ['logstash-*'], privileges: ['read', 'view_index_metadata'], @@ -148,14 +139,10 @@ export default function ({ getService }) { ], run_as: ['watcher_user'], }, - kibana: [ - { - privileges: ['all'], - }, - { - privileges: ['read'], - }, - ], + kibana: { + global: ['all', 'read'], + space: {} + } }) .expect(204); @@ -169,7 +156,7 @@ export default function ({ getService }) { privileges: ['read', 'view_index_metadata'], field_security: { grant: ['*'], - except: [ 'geo.*' ] + except: ['geo.*'] }, query: `{ "match": { "geo.src": "CN" } }`, }, @@ -177,12 +164,7 @@ export default function ({ getService }) { applications: [ { application: 'kibana-.kibana', - privileges: ['all'], - resources: ['*'], - }, - { - application: 'kibana-.kibana', - privileges: ['read'], + privileges: ['all', 'read'], resources: ['*'], }, { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 23fda2b632263..77c0aee49b83e 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -133,7 +133,6 @@ export default async function ({ readConfigFile }) { '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', '--xpack.xpack_main.telemetry.enabled=false', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions - '--logging.verbose=true', ], }, From bdd1d530cb3022e2319aa7f90153267e94d343b4 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Fri, 7 Sep 2018 08:40:03 +0300 Subject: [PATCH 40/68] Get rid of `__tests__` folders in the core. (#22662) --- .../{__tests__ => }/__fixtures__/config.yml | 0 .../__fixtures__/config_flat.yml | 0 .../__fixtures__/en_var_ref_config.yml | 0 .../{__tests__ => }/__fixtures__/one.yml | 0 .../{__tests__ => }/__fixtures__/two.yml | 0 .../config/{__tests__ => }/__mocks__/env.ts | 2 +- .../__snapshots__/config_service.test.ts.snap | 0 .../__snapshots__/env.test.ts.snap | 0 .../__snapshots__/read_config.test.ts.snap | 0 .../config/{__tests__ => }/apply_argv.test.ts | 2 +- .../{__tests__ => }/config_service.test.ts | 9 ++++----- .../ensure_deep_object.test.ts | 2 +- .../server/config/{__tests__ => }/env.test.ts | 4 ++-- src/core/server/config/index.ts | 1 - .../raw_config_service.test.ts | 4 ++-- .../{__tests__ => }/read_config.test.ts | 2 +- .../__snapshots__/index.test.ts.snap | 0 .../{__tests__ => }/index.test.ts | 2 +- .../{__tests__ => }/schema_error.test.ts | 2 +- .../__snapshots__/any_type.test.ts.snap | 0 .../__snapshots__/array_type.test.ts.snap | 0 .../__snapshots__/boolean_type.test.ts.snap | 0 .../__snapshots__/byte_size_type.test.ts.snap | 0 .../conditional_type.test.ts.snap | 0 .../__snapshots__/duration_type.test.ts.snap | 0 .../__snapshots__/literal_type.test.ts.snap | 0 .../__snapshots__/map_of_type.test.ts.snap | 0 .../__snapshots__/maybe_type.test.ts.snap | 0 .../__snapshots__/number_type.test.ts.snap | 0 .../__snapshots__/object_type.test.ts.snap | 0 .../__snapshots__/one_of_type.test.ts.snap | 0 .../__snapshots__/string_type.test.ts.snap | 0 .../types/{__tests__ => }/any_type.test.ts | 2 +- .../types/{__tests__ => }/array_type.test.ts | 2 +- .../{__tests__ => }/boolean_type.test.ts | 2 +- .../{__tests__ => }/byte_size_type.test.ts | 4 ++-- .../{__tests__ => }/conditional_type.test.ts | 2 +- .../{__tests__ => }/duration_type.test.ts | 2 +- .../{__tests__ => }/literal_type.test.ts | 2 +- .../types/{__tests__ => }/map_of_type.test.ts | 2 +- .../types/{__tests__ => }/maybe_type.test.ts | 2 +- .../types/{__tests__ => }/number_type.test.ts | 2 +- .../types/{__tests__ => }/object_type.test.ts | 2 +- .../types/{__tests__ => }/one_of_type.test.ts | 2 +- .../types/{__tests__ => }/string_type.test.ts | 2 +- .../__snapshots__/http_config.test.ts.snap | 0 .../__snapshots__/http_server.test.ts.snap | 0 .../__snapshots__/http_service.test.ts.snap | 0 .../https_redirect_server.test.ts.snap | 0 .../http/{__tests__ => }/http_config.test.ts | 2 +- .../http/{__tests__ => }/http_server.test.ts | 9 ++++----- .../http/{__tests__ => }/http_service.test.ts | 8 +++----- .../https_redirect_server.test.ts | 8 ++++---- src/core/server/index.test.ts | 2 +- .../__snapshots__/legacy_service.test.ts.snap | 0 ...gacy_object_to_config_adapter.test.ts.snap | 0 .../legacy_object_to_config_adapter.test.ts | 2 +- .../legacy_platform_proxy.test.ts | 2 +- .../{__tests__ => }/legacy_service.test.ts | 20 +++++++++---------- .../legacy_appender.test.ts.snap | 0 .../{__tests__ => }/legacy_appender.test.ts | 10 +++++----- .../__snapshots__/logging_config.test.ts.snap | 0 .../logging_service.test.ts.snap | 0 .../{__tests__ => }/appenders.test.ts | 12 +++++------ .../buffer_appender.test.ts | 2 +- .../console_appender.test.ts | 2 +- .../{__tests__ => file}/file_appender.test.ts | 2 +- .../__snapshots__/json_layout.test.ts.snap | 0 .../__snapshots__/pattern_layout.test.ts.snap | 0 .../{__tests__ => }/json_layout.test.ts | 6 +++--- .../layouts/{__tests__ => }/layouts.test.ts | 6 +++--- .../{__tests__ => }/pattern_layout.test.ts | 8 ++++---- .../logging/{__tests__ => }/log_level.test.ts | 2 +- .../logging/{__tests__ => }/logger.test.ts | 8 ++++---- .../{__tests__ => }/logger_adapter.test.ts | 4 ++-- .../{__tests__ => }/logging_config.test.ts | 2 +- .../{__tests__ => }/logging_service.test.ts | 3 +-- .../__snapshots__/index.test.ts.snap | 0 .../server/root/{__tests__ => }/index.test.ts | 14 ++++++------- .../__snapshots__/get.test.ts.snap | 0 src/core/utils/{__tests__ => }/get.test.ts | 2 +- src/core/utils/{__tests__ => }/url.test.ts | 2 +- 82 files changed, 95 insertions(+), 101 deletions(-) rename src/core/server/config/{__tests__ => }/__fixtures__/config.yml (100%) rename src/core/server/config/{__tests__ => }/__fixtures__/config_flat.yml (100%) rename src/core/server/config/{__tests__ => }/__fixtures__/en_var_ref_config.yml (100%) rename src/core/server/config/{__tests__ => }/__fixtures__/one.yml (100%) rename src/core/server/config/{__tests__ => }/__fixtures__/two.yml (100%) rename src/core/server/config/{__tests__ => }/__mocks__/env.ts (97%) rename src/core/server/config/{__tests__ => }/__snapshots__/config_service.test.ts.snap (100%) rename src/core/server/config/{__tests__ => }/__snapshots__/env.test.ts.snap (100%) rename src/core/server/config/{__tests__ => }/__snapshots__/read_config.test.ts.snap (100%) rename src/core/server/config/{__tests__ => }/apply_argv.test.ts (97%) rename src/core/server/config/{__tests__ => }/config_service.test.ts (97%) rename src/core/server/config/{__tests__ => }/ensure_deep_object.test.ts (98%) rename src/core/server/config/{__tests__ => }/env.test.ts (97%) rename src/core/server/config/{__tests__ => }/raw_config_service.test.ts (98%) rename src/core/server/config/{__tests__ => }/read_config.test.ts (98%) rename src/core/server/config/schema/byte_size_value/{__tests__ => }/__snapshots__/index.test.ts.snap (100%) rename src/core/server/config/schema/byte_size_value/{__tests__ => }/index.test.ts (99%) rename src/core/server/config/schema/errors/{__tests__ => }/schema_error.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/any_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/array_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/boolean_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/byte_size_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/conditional_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/duration_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/literal_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/map_of_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/maybe_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/number_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/object_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/one_of_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/__snapshots__/string_type.test.ts.snap (100%) rename src/core/server/config/schema/types/{__tests__ => }/any_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/array_type.test.ts (99%) rename src/core/server/config/schema/types/{__tests__ => }/boolean_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/byte_size_type.test.ts (97%) rename src/core/server/config/schema/types/{__tests__ => }/conditional_type.test.ts (99%) rename src/core/server/config/schema/types/{__tests__ => }/duration_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/literal_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/map_of_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/maybe_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/number_type.test.ts (98%) rename src/core/server/config/schema/types/{__tests__ => }/object_type.test.ts (99%) rename src/core/server/config/schema/types/{__tests__ => }/one_of_type.test.ts (99%) rename src/core/server/config/schema/types/{__tests__ => }/string_type.test.ts (99%) rename src/core/server/http/{__tests__ => }/__snapshots__/http_config.test.ts.snap (100%) rename src/core/server/http/{__tests__ => }/__snapshots__/http_server.test.ts.snap (100%) rename src/core/server/http/{__tests__ => }/__snapshots__/http_service.test.ts.snap (100%) rename src/core/server/http/{__tests__ => }/__snapshots__/https_redirect_server.test.ts.snap (100%) rename src/core/server/http/{__tests__ => }/http_config.test.ts (99%) rename src/core/server/http/{__tests__ => }/http_server.test.ts (98%) rename src/core/server/http/{__tests__ => }/http_service.test.ts (95%) rename src/core/server/http/{__tests__ => }/https_redirect_server.test.ts (92%) rename src/core/server/legacy_compat/{__tests__ => }/__snapshots__/legacy_service.test.ts.snap (100%) rename src/core/server/legacy_compat/config/{__tests__ => }/__snapshots__/legacy_object_to_config_adapter.test.ts.snap (100%) rename src/core/server/legacy_compat/config/{__tests__ => }/legacy_object_to_config_adapter.test.ts (98%) rename src/core/server/legacy_compat/{__tests__ => }/legacy_platform_proxy.test.ts (98%) rename src/core/server/legacy_compat/{__tests__ => }/legacy_service.test.ts (95%) rename src/core/server/legacy_compat/logging/appenders/{__tests__ => }/__snapshots__/legacy_appender.test.ts.snap (100%) rename src/core/server/legacy_compat/logging/appenders/{__tests__ => }/legacy_appender.test.ts (93%) rename src/core/server/logging/{__tests__ => }/__snapshots__/logging_config.test.ts.snap (100%) rename src/core/server/logging/{__tests__ => }/__snapshots__/logging_service.test.ts.snap (100%) rename src/core/server/logging/appenders/{__tests__ => }/appenders.test.ts (88%) rename src/core/server/logging/appenders/{__tests__ => buffer}/buffer_appender.test.ts (97%) rename src/core/server/logging/appenders/{__tests__ => console}/console_appender.test.ts (97%) rename src/core/server/logging/appenders/{__tests__ => file}/file_appender.test.ts (98%) rename src/core/server/logging/layouts/{__tests__ => }/__snapshots__/json_layout.test.ts.snap (100%) rename src/core/server/logging/layouts/{__tests__ => }/__snapshots__/pattern_layout.test.ts.snap (100%) rename src/core/server/logging/layouts/{__tests__ => }/json_layout.test.ts (94%) rename src/core/server/logging/layouts/{__tests__ => }/layouts.test.ts (93%) rename src/core/server/logging/layouts/{__tests__ => }/pattern_layout.test.ts (93%) rename src/core/server/logging/{__tests__ => }/log_level.test.ts (98%) rename src/core/server/logging/{__tests__ => }/logger.test.ts (98%) rename src/core/server/logging/{__tests__ => }/logger_adapter.test.ts (97%) rename src/core/server/logging/{__tests__ => }/logging_config.test.ts (98%) rename src/core/server/logging/{__tests__ => }/logging_service.test.ts (98%) rename src/core/server/root/{__tests__ => }/__snapshots__/index.test.ts.snap (100%) rename src/core/server/root/{__tests__ => }/index.test.ts (95%) rename src/core/utils/{__tests__ => }/__snapshots__/get.test.ts.snap (100%) rename src/core/utils/{__tests__ => }/get.test.ts (97%) rename src/core/utils/{__tests__ => }/url.test.ts (98%) diff --git a/src/core/server/config/__tests__/__fixtures__/config.yml b/src/core/server/config/__fixtures__/config.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/config.yml rename to src/core/server/config/__fixtures__/config.yml diff --git a/src/core/server/config/__tests__/__fixtures__/config_flat.yml b/src/core/server/config/__fixtures__/config_flat.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/config_flat.yml rename to src/core/server/config/__fixtures__/config_flat.yml diff --git a/src/core/server/config/__tests__/__fixtures__/en_var_ref_config.yml b/src/core/server/config/__fixtures__/en_var_ref_config.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/en_var_ref_config.yml rename to src/core/server/config/__fixtures__/en_var_ref_config.yml diff --git a/src/core/server/config/__tests__/__fixtures__/one.yml b/src/core/server/config/__fixtures__/one.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/one.yml rename to src/core/server/config/__fixtures__/one.yml diff --git a/src/core/server/config/__tests__/__fixtures__/two.yml b/src/core/server/config/__fixtures__/two.yml similarity index 100% rename from src/core/server/config/__tests__/__fixtures__/two.yml rename to src/core/server/config/__fixtures__/two.yml diff --git a/src/core/server/config/__tests__/__mocks__/env.ts b/src/core/server/config/__mocks__/env.ts similarity index 97% rename from src/core/server/config/__tests__/__mocks__/env.ts rename to src/core/server/config/__mocks__/env.ts index e90c33f19ee49..dec62978a292f 100644 --- a/src/core/server/config/__tests__/__mocks__/env.ts +++ b/src/core/server/config/__mocks__/env.ts @@ -19,7 +19,7 @@ // Test helpers to simplify mocking environment options. -import { EnvOptions } from '../../env'; +import { EnvOptions } from '../env'; type DeepPartial = { [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial diff --git a/src/core/server/config/__tests__/__snapshots__/config_service.test.ts.snap b/src/core/server/config/__snapshots__/config_service.test.ts.snap similarity index 100% rename from src/core/server/config/__tests__/__snapshots__/config_service.test.ts.snap rename to src/core/server/config/__snapshots__/config_service.test.ts.snap diff --git a/src/core/server/config/__tests__/__snapshots__/env.test.ts.snap b/src/core/server/config/__snapshots__/env.test.ts.snap similarity index 100% rename from src/core/server/config/__tests__/__snapshots__/env.test.ts.snap rename to src/core/server/config/__snapshots__/env.test.ts.snap diff --git a/src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap b/src/core/server/config/__snapshots__/read_config.test.ts.snap similarity index 100% rename from src/core/server/config/__tests__/__snapshots__/read_config.test.ts.snap rename to src/core/server/config/__snapshots__/read_config.test.ts.snap diff --git a/src/core/server/config/__tests__/apply_argv.test.ts b/src/core/server/config/apply_argv.test.ts similarity index 97% rename from src/core/server/config/__tests__/apply_argv.test.ts rename to src/core/server/config/apply_argv.test.ts index b3d2f3749271a..80aa3d9f74a40 100644 --- a/src/core/server/config/__tests__/apply_argv.test.ts +++ b/src/core/server/config/apply_argv.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Config, ObjectToConfigAdapter } from '..'; +import { Config, ObjectToConfigAdapter } from '.'; /** * Overrides some config values with ones from argv. diff --git a/src/core/server/config/__tests__/config_service.test.ts b/src/core/server/config/config_service.test.ts similarity index 97% rename from src/core/server/config/__tests__/config_service.test.ts rename to src/core/server/config/config_service.test.ts index c41ae3ccd8b75..22598b2a971d0 100644 --- a/src/core/server/config/__tests__/config_service.test.ts +++ b/src/core/server/config/config_service.test.ts @@ -23,13 +23,12 @@ import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); -jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); +jest.mock('../../../utils/package_json', () => ({ pkg: mockPackage })); -import { schema, Type, TypeOf } from '../schema'; +import { schema, Type, TypeOf } from './schema'; -import { ConfigService, ObjectToConfigAdapter } from '..'; -import { logger } from '../../logging/__mocks__'; -import { Env } from '../env'; +import { ConfigService, Env, ObjectToConfigAdapter } from '.'; +import { logger } from '../logging/__mocks__'; import { getEnvOptions } from './__mocks__/env'; const emptyArgv = getEnvOptions(); diff --git a/src/core/server/config/__tests__/ensure_deep_object.test.ts b/src/core/server/config/ensure_deep_object.test.ts similarity index 98% rename from src/core/server/config/__tests__/ensure_deep_object.test.ts rename to src/core/server/config/ensure_deep_object.test.ts index 40c0732266073..5a520fbeef316 100644 --- a/src/core/server/config/__tests__/ensure_deep_object.test.ts +++ b/src/core/server/config/ensure_deep_object.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ensureDeepObject } from '../ensure_deep_object'; +import { ensureDeepObject } from './ensure_deep_object'; test('flat object', () => { const obj = { diff --git a/src/core/server/config/__tests__/env.test.ts b/src/core/server/config/env.test.ts similarity index 97% rename from src/core/server/config/__tests__/env.test.ts rename to src/core/server/config/env.test.ts index 381273a1f8ffb..56ff576fd8f31 100644 --- a/src/core/server/config/__tests__/env.test.ts +++ b/src/core/server/config/env.test.ts @@ -30,9 +30,9 @@ jest.mock('path', () => ({ })); const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); -jest.mock('../../../../utils/package_json', () => ({ pkg: mockPackage })); +jest.mock('../../../utils/package_json', () => ({ pkg: mockPackage })); -import { Env } from '../env'; +import { Env } from '.'; import { getEnvOptions } from './__mocks__/env'; test('correctly creates default environment in dev mode.', () => { diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index c63a8d6aa7c04..bbddec03a0f41 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -24,4 +24,3 @@ export { Config, ConfigPath } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { Env, CliArgs } from './env'; export { ConfigWithSchema } from './config_with_schema'; -export { getConfigFromFiles } from './read_config'; diff --git a/src/core/server/config/__tests__/raw_config_service.test.ts b/src/core/server/config/raw_config_service.test.ts similarity index 98% rename from src/core/server/config/__tests__/raw_config_service.test.ts rename to src/core/server/config/raw_config_service.test.ts index 66cc31bc77774..eb5c212a31eb9 100644 --- a/src/core/server/config/__tests__/raw_config_service.test.ts +++ b/src/core/server/config/raw_config_service.test.ts @@ -19,12 +19,12 @@ const mockGetConfigFromFiles = jest.fn(); -jest.mock('../read_config', () => ({ +jest.mock('./read_config', () => ({ getConfigFromFiles: mockGetConfigFromFiles, })); import { first } from 'rxjs/operators'; -import { RawConfigService } from '../raw_config_service'; +import { RawConfigService } from '.'; const configFile = '/config/kibana.yml'; const anotherConfigFile = '/config/kibana.dev.yml'; diff --git a/src/core/server/config/__tests__/read_config.test.ts b/src/core/server/config/read_config.test.ts similarity index 98% rename from src/core/server/config/__tests__/read_config.test.ts rename to src/core/server/config/read_config.test.ts index b9aa3871b1794..46b75f28eb987 100644 --- a/src/core/server/config/__tests__/read_config.test.ts +++ b/src/core/server/config/read_config.test.ts @@ -18,7 +18,7 @@ */ import { relative, resolve } from 'path'; -import { getConfigFromFiles } from '../read_config'; +import { getConfigFromFiles } from './read_config'; const fixtureFile = (name: string) => `${__dirname}/__fixtures__/${name}`; diff --git a/src/core/server/config/schema/byte_size_value/__tests__/__snapshots__/index.test.ts.snap b/src/core/server/config/schema/byte_size_value/__snapshots__/index.test.ts.snap similarity index 100% rename from src/core/server/config/schema/byte_size_value/__tests__/__snapshots__/index.test.ts.snap rename to src/core/server/config/schema/byte_size_value/__snapshots__/index.test.ts.snap diff --git a/src/core/server/config/schema/byte_size_value/__tests__/index.test.ts b/src/core/server/config/schema/byte_size_value/index.test.ts similarity index 99% rename from src/core/server/config/schema/byte_size_value/__tests__/index.test.ts rename to src/core/server/config/schema/byte_size_value/index.test.ts index ece8769248152..46ed96c83dd1f 100644 --- a/src/core/server/config/schema/byte_size_value/__tests__/index.test.ts +++ b/src/core/server/config/schema/byte_size_value/index.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ByteSizeValue } from '../'; +import { ByteSizeValue } from '.'; describe('parsing units', () => { test('bytes', () => { diff --git a/src/core/server/config/schema/errors/__tests__/schema_error.test.ts b/src/core/server/config/schema/errors/schema_error.test.ts similarity index 98% rename from src/core/server/config/schema/errors/__tests__/schema_error.test.ts rename to src/core/server/config/schema/errors/schema_error.test.ts index 15ce626621b58..0f632b781e9a6 100644 --- a/src/core/server/config/schema/errors/__tests__/schema_error.test.ts +++ b/src/core/server/config/schema/errors/schema_error.test.ts @@ -18,7 +18,7 @@ */ import { relative } from 'path'; -import { SchemaError } from '..'; +import { SchemaError } from '.'; /** * Make all paths in stacktrace relative. diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/any_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/any_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/any_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/array_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/array_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/array_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/array_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/boolean_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/boolean_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/boolean_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/boolean_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/byte_size_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/byte_size_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/byte_size_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/byte_size_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/conditional_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/conditional_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/conditional_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/conditional_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/duration_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/duration_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/duration_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/duration_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/literal_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/literal_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/literal_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/literal_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/map_of_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/map_of_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/map_of_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/map_of_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/maybe_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/maybe_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/maybe_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/maybe_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/number_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/number_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/number_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/number_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/object_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/object_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/object_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/object_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/one_of_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/one_of_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/one_of_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/one_of_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/__snapshots__/string_type.test.ts.snap b/src/core/server/config/schema/types/__snapshots__/string_type.test.ts.snap similarity index 100% rename from src/core/server/config/schema/types/__tests__/__snapshots__/string_type.test.ts.snap rename to src/core/server/config/schema/types/__snapshots__/string_type.test.ts.snap diff --git a/src/core/server/config/schema/types/__tests__/any_type.test.ts b/src/core/server/config/schema/types/any_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/any_type.test.ts rename to src/core/server/config/schema/types/any_type.test.ts index 6f39f3deab5fd..4d68c860ba13d 100644 --- a/src/core/server/config/schema/types/__tests__/any_type.test.ts +++ b/src/core/server/config/schema/types/any_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('works for any value', () => { expect(schema.any().validate(true)).toBe(true); diff --git a/src/core/server/config/schema/types/__tests__/array_type.test.ts b/src/core/server/config/schema/types/array_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/array_type.test.ts rename to src/core/server/config/schema/types/array_type.test.ts index f1fb124a95ede..c6943e0d1b5f3 100644 --- a/src/core/server/config/schema/types/__tests__/array_type.test.ts +++ b/src/core/server/config/schema/types/array_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value if it matches the type', () => { const type = schema.arrayOf(schema.string()); diff --git a/src/core/server/config/schema/types/__tests__/boolean_type.test.ts b/src/core/server/config/schema/types/boolean_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/boolean_type.test.ts rename to src/core/server/config/schema/types/boolean_type.test.ts index bfd4259af387e..d6e274f05e3ff 100644 --- a/src/core/server/config/schema/types/__tests__/boolean_type.test.ts +++ b/src/core/server/config/schema/types/boolean_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value by default', () => { expect(schema.boolean().validate(true)).toBe(true); diff --git a/src/core/server/config/schema/types/__tests__/byte_size_type.test.ts b/src/core/server/config/schema/types/byte_size_type.test.ts similarity index 97% rename from src/core/server/config/schema/types/__tests__/byte_size_type.test.ts rename to src/core/server/config/schema/types/byte_size_type.test.ts index 786b996ae5687..67eae1e7c382a 100644 --- a/src/core/server/config/schema/types/__tests__/byte_size_type.test.ts +++ b/src/core/server/config/schema/types/byte_size_type.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { schema } from '../..'; -import { ByteSizeValue } from '../../byte_size_value'; +import { schema } from '..'; +import { ByteSizeValue } from '../byte_size_value'; const { byteSize } = schema; diff --git a/src/core/server/config/schema/types/__tests__/conditional_type.test.ts b/src/core/server/config/schema/types/conditional_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/conditional_type.test.ts rename to src/core/server/config/schema/types/conditional_type.test.ts index 112ee874afa7b..a72c3463e00cb 100644 --- a/src/core/server/config/schema/types/__tests__/conditional_type.test.ts +++ b/src/core/server/config/schema/types/conditional_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('required by default', () => { const type = schema.conditional( diff --git a/src/core/server/config/schema/types/__tests__/duration_type.test.ts b/src/core/server/config/schema/types/duration_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/duration_type.test.ts rename to src/core/server/config/schema/types/duration_type.test.ts index 0c1d7e4dd8e50..9a21afc6cf40f 100644 --- a/src/core/server/config/schema/types/__tests__/duration_type.test.ts +++ b/src/core/server/config/schema/types/duration_type.test.ts @@ -18,7 +18,7 @@ */ import { duration as momentDuration } from 'moment'; -import { schema } from '../..'; +import { schema } from '..'; const { duration } = schema; diff --git a/src/core/server/config/schema/types/__tests__/literal_type.test.ts b/src/core/server/config/schema/types/literal_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/literal_type.test.ts rename to src/core/server/config/schema/types/literal_type.test.ts index 4d590200c1ccf..5ee0ac4edff68 100644 --- a/src/core/server/config/schema/types/__tests__/literal_type.test.ts +++ b/src/core/server/config/schema/types/literal_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; const { literal } = schema; diff --git a/src/core/server/config/schema/types/__tests__/map_of_type.test.ts b/src/core/server/config/schema/types/map_of_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/map_of_type.test.ts rename to src/core/server/config/schema/types/map_of_type.test.ts index ed4e12f162c59..1b72d39fcec26 100644 --- a/src/core/server/config/schema/types/__tests__/map_of_type.test.ts +++ b/src/core/server/config/schema/types/map_of_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('handles object as input', () => { const type = schema.mapOf(schema.string(), schema.string()); diff --git a/src/core/server/config/schema/types/__tests__/maybe_type.test.ts b/src/core/server/config/schema/types/maybe_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/maybe_type.test.ts rename to src/core/server/config/schema/types/maybe_type.test.ts index 950987763baf1..b29f504c03b32 100644 --- a/src/core/server/config/schema/types/__tests__/maybe_type.test.ts +++ b/src/core/server/config/schema/types/maybe_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value if specified', () => { const type = schema.maybe(schema.string()); diff --git a/src/core/server/config/schema/types/__tests__/number_type.test.ts b/src/core/server/config/schema/types/number_type.test.ts similarity index 98% rename from src/core/server/config/schema/types/__tests__/number_type.test.ts rename to src/core/server/config/schema/types/number_type.test.ts index dd6be2631d28c..b85d5113563eb 100644 --- a/src/core/server/config/schema/types/__tests__/number_type.test.ts +++ b/src/core/server/config/schema/types/number_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value by default', () => { expect(schema.number().validate(4)).toBe(4); diff --git a/src/core/server/config/schema/types/__tests__/object_type.test.ts b/src/core/server/config/schema/types/object_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/object_type.test.ts rename to src/core/server/config/schema/types/object_type.test.ts index ec54528c292a0..e0eaabadb8ef5 100644 --- a/src/core/server/config/schema/types/__tests__/object_type.test.ts +++ b/src/core/server/config/schema/types/object_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value by default', () => { const type = schema.object({ diff --git a/src/core/server/config/schema/types/__tests__/one_of_type.test.ts b/src/core/server/config/schema/types/one_of_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/one_of_type.test.ts rename to src/core/server/config/schema/types/one_of_type.test.ts index e2f0f9688544a..72119e761590b 100644 --- a/src/core/server/config/schema/types/__tests__/one_of_type.test.ts +++ b/src/core/server/config/schema/types/one_of_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('handles string', () => { expect(schema.oneOf([schema.string()]).validate('test')).toBe('test'); diff --git a/src/core/server/config/schema/types/__tests__/string_type.test.ts b/src/core/server/config/schema/types/string_type.test.ts similarity index 99% rename from src/core/server/config/schema/types/__tests__/string_type.test.ts rename to src/core/server/config/schema/types/string_type.test.ts index f9415a0ac2506..193d85d290731 100644 --- a/src/core/server/config/schema/types/__tests__/string_type.test.ts +++ b/src/core/server/config/schema/types/string_type.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { schema } from '../..'; +import { schema } from '..'; test('returns value is string and defined', () => { expect(schema.string().validate('test')).toBe('test'); diff --git a/src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap similarity index 100% rename from src/core/server/http/__tests__/__snapshots__/http_config.test.ts.snap rename to src/core/server/http/__snapshots__/http_config.test.ts.snap diff --git a/src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap b/src/core/server/http/__snapshots__/http_server.test.ts.snap similarity index 100% rename from src/core/server/http/__tests__/__snapshots__/http_server.test.ts.snap rename to src/core/server/http/__snapshots__/http_server.test.ts.snap diff --git a/src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap b/src/core/server/http/__snapshots__/http_service.test.ts.snap similarity index 100% rename from src/core/server/http/__tests__/__snapshots__/http_service.test.ts.snap rename to src/core/server/http/__snapshots__/http_service.test.ts.snap diff --git a/src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap b/src/core/server/http/__snapshots__/https_redirect_server.test.ts.snap similarity index 100% rename from src/core/server/http/__tests__/__snapshots__/https_redirect_server.test.ts.snap rename to src/core/server/http/__snapshots__/https_redirect_server.test.ts.snap diff --git a/src/core/server/http/__tests__/http_config.test.ts b/src/core/server/http/http_config.test.ts similarity index 99% rename from src/core/server/http/__tests__/http_config.test.ts rename to src/core/server/http/http_config.test.ts index 45bd8962fc0df..54d28ef921fcf 100644 --- a/src/core/server/http/__tests__/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { HttpConfig } from '../http_config'; +import { HttpConfig } from '.'; test('has defaults for config', () => { const httpSchema = HttpConfig.schema; diff --git a/src/core/server/http/__tests__/http_server.test.ts b/src/core/server/http/http_server.test.ts similarity index 98% rename from src/core/server/http/__tests__/http_server.test.ts rename to src/core/server/http/http_server.test.ts index 42f93a13e1c80..704a6ddf97aba 100644 --- a/src/core/server/http/__tests__/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -26,11 +26,10 @@ jest.mock('fs', () => ({ import Chance from 'chance'; import supertest from 'supertest'; -import { ByteSizeValue } from '../../config/schema'; -import { logger } from '../../logging/__mocks__'; -import { HttpConfig } from '../http_config'; -import { HttpServer } from '../http_server'; -import { Router } from '../router'; +import { HttpConfig, Router } from '.'; +import { ByteSizeValue } from '../config/schema'; +import { logger } from '../logging/__mocks__'; +import { HttpServer } from './http_server'; const chance = new Chance(); diff --git a/src/core/server/http/__tests__/http_service.test.ts b/src/core/server/http/http_service.test.ts similarity index 95% rename from src/core/server/http/__tests__/http_service.test.ts rename to src/core/server/http/http_service.test.ts index 1c6d259848117..a42ac26745c60 100644 --- a/src/core/server/http/__tests__/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -19,16 +19,14 @@ const mockHttpServer = jest.fn(); -jest.mock('../http_server', () => ({ +jest.mock('./http_server', () => ({ HttpServer: mockHttpServer, })); import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { logger } from '../../logging/__mocks__'; -import { HttpConfig } from '../http_config'; -import { HttpService } from '../http_service'; -import { Router } from '../router'; +import { HttpConfig, HttpService, Router } from '.'; +import { logger } from '../logging/__mocks__'; beforeEach(() => { logger.mockClear(); diff --git a/src/core/server/http/__tests__/https_redirect_server.test.ts b/src/core/server/http/https_redirect_server.test.ts similarity index 92% rename from src/core/server/http/__tests__/https_redirect_server.test.ts rename to src/core/server/http/https_redirect_server.test.ts index c92691a679ef0..6d9443335a62b 100644 --- a/src/core/server/http/__tests__/https_redirect_server.test.ts +++ b/src/core/server/http/https_redirect_server.test.ts @@ -25,10 +25,10 @@ import Chance from 'chance'; import { Server } from 'http'; import supertest from 'supertest'; -import { ByteSizeValue } from '../../config/schema'; -import { logger } from '../../logging/__mocks__'; -import { HttpConfig } from '../http_config'; -import { HttpsRedirectServer } from '../https_redirect_server'; +import { HttpConfig } from '.'; +import { ByteSizeValue } from '../config/schema'; +import { logger } from '../logging/__mocks__'; +import { HttpsRedirectServer } from './https_redirect_server'; const chance = new Chance(); diff --git a/src/core/server/index.test.ts b/src/core/server/index.test.ts index 8a83d8d500b81..d6bd19d36ce3c 100644 --- a/src/core/server/index.test.ts +++ b/src/core/server/index.test.ts @@ -30,7 +30,7 @@ jest.mock('./legacy_compat/legacy_service', () => ({ import { BehaviorSubject } from 'rxjs'; import { Server } from '.'; import { Env } from './config'; -import { getEnvOptions } from './config/__tests__/__mocks__/env'; +import { getEnvOptions } from './config/__mocks__/env'; import { logger } from './logging/__mocks__'; const mockConfigService = { atPath: jest.fn(), getUnusedPaths: jest.fn().mockReturnValue([]) }; diff --git a/src/core/server/legacy_compat/__tests__/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap similarity index 100% rename from src/core/server/legacy_compat/__tests__/__snapshots__/legacy_service.test.ts.snap rename to src/core/server/legacy_compat/__snapshots__/legacy_service.test.ts.snap diff --git a/src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy_compat/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap similarity index 100% rename from src/core/server/legacy_compat/config/__tests__/__snapshots__/legacy_object_to_config_adapter.test.ts.snap rename to src/core/server/legacy_compat/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap diff --git a/src/core/server/legacy_compat/config/__tests__/legacy_object_to_config_adapter.test.ts b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.test.ts similarity index 98% rename from src/core/server/legacy_compat/config/__tests__/legacy_object_to_config_adapter.test.ts rename to src/core/server/legacy_compat/config/legacy_object_to_config_adapter.test.ts index b465a55be4243..afa3cf03fe9a9 100644 --- a/src/core/server/legacy_compat/config/__tests__/legacy_object_to_config_adapter.test.ts +++ b/src/core/server/legacy_compat/config/legacy_object_to_config_adapter.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyObjectToConfigAdapter } from '../legacy_object_to_config_adapter'; +import { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter'; describe('#get', () => { test('correctly handles paths that do not exist.', () => { diff --git a/src/core/server/legacy_compat/__tests__/legacy_platform_proxy.test.ts b/src/core/server/legacy_compat/legacy_platform_proxy.test.ts similarity index 98% rename from src/core/server/legacy_compat/__tests__/legacy_platform_proxy.test.ts rename to src/core/server/legacy_compat/legacy_platform_proxy.test.ts index 8330bbb8d74db..cc7436ce32170 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_platform_proxy.test.ts +++ b/src/core/server/legacy_compat/legacy_platform_proxy.test.ts @@ -19,7 +19,7 @@ import { Server } from 'net'; -import { LegacyPlatformProxy } from '../legacy_platform_proxy'; +import { LegacyPlatformProxy } from './legacy_platform_proxy'; let server: jest.Mocked; let proxy: LegacyPlatformProxy; diff --git a/src/core/server/legacy_compat/__tests__/legacy_service.test.ts b/src/core/server/legacy_compat/legacy_service.test.ts similarity index 95% rename from src/core/server/legacy_compat/__tests__/legacy_service.test.ts rename to src/core/server/legacy_compat/legacy_service.test.ts index dc16709861084..70c71e3b4b0b8 100644 --- a/src/core/server/legacy_compat/__tests__/legacy_service.test.ts +++ b/src/core/server/legacy_compat/legacy_service.test.ts @@ -19,20 +19,20 @@ import { BehaviorSubject, Subject, throwError } from 'rxjs'; -jest.mock('../legacy_platform_proxy'); -jest.mock('../../../../server/kbn_server'); -jest.mock('../../../../cli/cluster/cluster_manager'); +jest.mock('./legacy_platform_proxy'); +jest.mock('../../../server/kbn_server'); +jest.mock('../../../cli/cluster/cluster_manager'); import { first } from 'rxjs/operators'; +import { LegacyService } from '.'; // @ts-ignore: implicit any for JS file -import MockClusterManager from '../../../../cli/cluster/cluster_manager'; +import MockClusterManager from '../../../cli/cluster/cluster_manager'; // @ts-ignore: implicit any for JS file -import MockKbnServer from '../../../../server/kbn_server'; -import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config'; -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; -import { logger } from '../../logging/__mocks__'; -import { LegacyPlatformProxy } from '../legacy_platform_proxy'; -import { LegacyService } from '../legacy_service'; +import MockKbnServer from '../../../server/kbn_server'; +import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; +import { logger } from '../logging/__mocks__'; +import { LegacyPlatformProxy } from './legacy_platform_proxy'; const MockLegacyPlatformProxy: jest.Mock = LegacyPlatformProxy as any; diff --git a/src/core/server/legacy_compat/logging/appenders/__tests__/__snapshots__/legacy_appender.test.ts.snap b/src/core/server/legacy_compat/logging/appenders/__snapshots__/legacy_appender.test.ts.snap similarity index 100% rename from src/core/server/legacy_compat/logging/appenders/__tests__/__snapshots__/legacy_appender.test.ts.snap rename to src/core/server/legacy_compat/logging/appenders/__snapshots__/legacy_appender.test.ts.snap diff --git a/src/core/server/legacy_compat/logging/appenders/__tests__/legacy_appender.test.ts b/src/core/server/legacy_compat/logging/appenders/legacy_appender.test.ts similarity index 93% rename from src/core/server/legacy_compat/logging/appenders/__tests__/legacy_appender.test.ts rename to src/core/server/legacy_compat/logging/appenders/legacy_appender.test.ts index 5f62a853666d9..adc5dcae3ec9d 100644 --- a/src/core/server/legacy_compat/logging/appenders/__tests__/legacy_appender.test.ts +++ b/src/core/server/legacy_compat/logging/appenders/legacy_appender.test.ts @@ -17,12 +17,12 @@ * under the License. */ -jest.mock('../../legacy_logging_server'); +jest.mock('../legacy_logging_server'); -import { LogLevel } from '../../../../logging/log_level'; -import { LogRecord } from '../../../../logging/log_record'; -import { LegacyLoggingServer } from '../../legacy_logging_server'; -import { LegacyAppender } from '../legacy_appender'; +import { LogLevel } from '../../../logging/log_level'; +import { LogRecord } from '../../../logging/log_record'; +import { LegacyLoggingServer } from '../legacy_logging_server'; +import { LegacyAppender } from './legacy_appender'; afterEach(() => (LegacyLoggingServer as any).mockClear()); diff --git a/src/core/server/logging/__tests__/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap similarity index 100% rename from src/core/server/logging/__tests__/__snapshots__/logging_config.test.ts.snap rename to src/core/server/logging/__snapshots__/logging_config.test.ts.snap diff --git a/src/core/server/logging/__tests__/__snapshots__/logging_service.test.ts.snap b/src/core/server/logging/__snapshots__/logging_service.test.ts.snap similarity index 100% rename from src/core/server/logging/__tests__/__snapshots__/logging_service.test.ts.snap rename to src/core/server/logging/__snapshots__/logging_service.test.ts.snap diff --git a/src/core/server/logging/appenders/__tests__/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts similarity index 88% rename from src/core/server/logging/appenders/__tests__/appenders.test.ts rename to src/core/server/logging/appenders/appenders.test.ts index f141de991f453..2103f9d8187b2 100644 --- a/src/core/server/logging/appenders/__tests__/appenders.test.ts +++ b/src/core/server/logging/appenders/appenders.test.ts @@ -18,8 +18,8 @@ */ const mockCreateLayout = jest.fn(); -jest.mock('../../layouts/layouts', () => { - const { schema } = require('../../../config/schema'); +jest.mock('../layouts/layouts', () => { + const { schema } = require('../../config/schema'); return { Layouts: { configSchema: schema.object({ kind: schema.literal('mock') }), @@ -28,10 +28,10 @@ jest.mock('../../layouts/layouts', () => { }; }); -import { LegacyAppender } from '../../../legacy_compat/logging/appenders/legacy_appender'; -import { Appenders } from '../appenders'; -import { ConsoleAppender } from '../console/console_appender'; -import { FileAppender } from '../file/file_appender'; +import { LegacyAppender } from '../../legacy_compat/logging/appenders/legacy_appender'; +import { Appenders } from './appenders'; +import { ConsoleAppender } from './console/console_appender'; +import { FileAppender } from './file/file_appender'; beforeEach(() => { mockCreateLayout.mockReset(); diff --git a/src/core/server/logging/appenders/__tests__/buffer_appender.test.ts b/src/core/server/logging/appenders/buffer/buffer_appender.test.ts similarity index 97% rename from src/core/server/logging/appenders/__tests__/buffer_appender.test.ts rename to src/core/server/logging/appenders/buffer/buffer_appender.test.ts index cdf2714f44e29..453a29271c582 100644 --- a/src/core/server/logging/appenders/__tests__/buffer_appender.test.ts +++ b/src/core/server/logging/appenders/buffer/buffer_appender.test.ts @@ -19,7 +19,7 @@ import { LogLevel } from '../../log_level'; import { LogRecord } from '../../log_record'; -import { BufferAppender } from '../buffer/buffer_appender'; +import { BufferAppender } from './buffer_appender'; test('`flush()` does not return any record buffered at the beginning.', () => { const appender = new BufferAppender(); diff --git a/src/core/server/logging/appenders/__tests__/console_appender.test.ts b/src/core/server/logging/appenders/console/console_appender.test.ts similarity index 97% rename from src/core/server/logging/appenders/__tests__/console_appender.test.ts rename to src/core/server/logging/appenders/console/console_appender.test.ts index 35128bd6ba1fd..fc885e5f58a11 100644 --- a/src/core/server/logging/appenders/__tests__/console_appender.test.ts +++ b/src/core/server/logging/appenders/console/console_appender.test.ts @@ -30,7 +30,7 @@ jest.mock('../../layouts/layouts', () => { import { LogLevel } from '../../log_level'; import { LogRecord } from '../../log_record'; -import { ConsoleAppender } from '../console/console_appender'; +import { ConsoleAppender } from './console_appender'; test('`configSchema` creates correct schema.', () => { const appenderSchema = ConsoleAppender.configSchema; diff --git a/src/core/server/logging/appenders/__tests__/file_appender.test.ts b/src/core/server/logging/appenders/file/file_appender.test.ts similarity index 98% rename from src/core/server/logging/appenders/__tests__/file_appender.test.ts rename to src/core/server/logging/appenders/file/file_appender.test.ts index 69b4980dff1f0..cc8f0196bff7c 100644 --- a/src/core/server/logging/appenders/__tests__/file_appender.test.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.ts @@ -33,7 +33,7 @@ jest.mock('fs', () => ({ createWriteStream: mockCreateWriteStream })); import { LogLevel } from '../../log_level'; import { LogRecord } from '../../log_record'; -import { FileAppender } from '../file/file_appender'; +import { FileAppender } from './file_appender'; const tickMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/core/server/logging/layouts/__tests__/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap similarity index 100% rename from src/core/server/logging/layouts/__tests__/__snapshots__/json_layout.test.ts.snap rename to src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap diff --git a/src/core/server/logging/layouts/__tests__/__snapshots__/pattern_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap similarity index 100% rename from src/core/server/logging/layouts/__tests__/__snapshots__/pattern_layout.test.ts.snap rename to src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap diff --git a/src/core/server/logging/layouts/__tests__/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts similarity index 94% rename from src/core/server/logging/layouts/__tests__/json_layout.test.ts rename to src/core/server/logging/layouts/json_layout.test.ts index ec94d023b2d64..49b8ddef07a63 100644 --- a/src/core/server/logging/layouts/__tests__/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { LogLevel } from '../../log_level'; -import { LogRecord } from '../../log_record'; -import { JsonLayout } from '../json_layout'; +import { LogLevel } from '../log_level'; +import { LogRecord } from '../log_record'; +import { JsonLayout } from './json_layout'; const records: LogRecord[] = [ { diff --git a/src/core/server/logging/layouts/__tests__/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts similarity index 93% rename from src/core/server/logging/layouts/__tests__/layouts.test.ts rename to src/core/server/logging/layouts/layouts.test.ts index ca70710233fee..aa1c54c846bc6 100644 --- a/src/core/server/logging/layouts/__tests__/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { JsonLayout } from '../json_layout'; -import { Layouts } from '../layouts'; -import { PatternLayout } from '../pattern_layout'; +import { JsonLayout } from './json_layout'; +import { Layouts } from './layouts'; +import { PatternLayout } from './pattern_layout'; test('`configSchema` creates correct schema for `pattern` layout.', () => { const layoutsSchema = Layouts.configSchema; diff --git a/src/core/server/logging/layouts/__tests__/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts similarity index 93% rename from src/core/server/logging/layouts/__tests__/pattern_layout.test.ts rename to src/core/server/logging/layouts/pattern_layout.test.ts index 4e6ddf2c097ed..ae8b39b9cc99a 100644 --- a/src/core/server/logging/layouts/__tests__/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { stripAnsiSnapshotSerializer } from '../../../../test_helpers/strip_ansi_snapshot_serializer'; -import { LogLevel } from '../../log_level'; -import { LogRecord } from '../../log_record'; -import { PatternLayout } from '../pattern_layout'; +import { stripAnsiSnapshotSerializer } from '../../../test_helpers/strip_ansi_snapshot_serializer'; +import { LogLevel } from '../log_level'; +import { LogRecord } from '../log_record'; +import { PatternLayout } from './pattern_layout'; const records: LogRecord[] = [ { diff --git a/src/core/server/logging/__tests__/log_level.test.ts b/src/core/server/logging/log_level.test.ts similarity index 98% rename from src/core/server/logging/__tests__/log_level.test.ts rename to src/core/server/logging/log_level.test.ts index 43de344b34cff..1f86cf21037a6 100644 --- a/src/core/server/logging/__tests__/log_level.test.ts +++ b/src/core/server/logging/log_level.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LogLevel } from '../log_level'; +import { LogLevel } from './log_level'; const allLogLevels = [ LogLevel.Off, diff --git a/src/core/server/logging/__tests__/logger.test.ts b/src/core/server/logging/logger.test.ts similarity index 98% rename from src/core/server/logging/__tests__/logger.test.ts rename to src/core/server/logging/logger.test.ts index 2dc16178fb47b..61eaa4912185b 100644 --- a/src/core/server/logging/__tests__/logger.test.ts +++ b/src/core/server/logging/logger.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { Appender } from '../appenders/appenders'; -import { LogLevel } from '../log_level'; -import { BaseLogger } from '../logger'; -import { LoggingConfig } from '../logging_config'; +import { LoggingConfig } from '.'; +import { Appender } from './appenders/appenders'; +import { LogLevel } from './log_level'; +import { BaseLogger } from './logger'; const context = LoggingConfig.getLoggerContext(['context', 'parent', 'child']); let appenderMocks: Appender[]; diff --git a/src/core/server/logging/__tests__/logger_adapter.test.ts b/src/core/server/logging/logger_adapter.test.ts similarity index 97% rename from src/core/server/logging/__tests__/logger_adapter.test.ts rename to src/core/server/logging/logger_adapter.test.ts index 25a9c01b108d6..075e8f4d47ffe 100644 --- a/src/core/server/logging/__tests__/logger_adapter.test.ts +++ b/src/core/server/logging/logger_adapter.test.ts @@ -17,8 +17,8 @@ * under the License. */ -import { Logger } from '../logger'; -import { LoggerAdapter } from '../logger_adapter'; +import { Logger } from '.'; +import { LoggerAdapter } from './logger_adapter'; test('proxies all method calls to the internal logger.', () => { const internalLogger: Logger = { diff --git a/src/core/server/logging/__tests__/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts similarity index 98% rename from src/core/server/logging/__tests__/logging_config.test.ts rename to src/core/server/logging/logging_config.test.ts index 2f1f1d9f2f7c0..f21b5aaf3c1a7 100644 --- a/src/core/server/logging/__tests__/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LoggingConfig } from '../logging_config'; +import { LoggingConfig } from '.'; test('`schema` creates correct schema with defaults.', () => { const loggingConfigSchema = LoggingConfig.schema; diff --git a/src/core/server/logging/__tests__/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts similarity index 98% rename from src/core/server/logging/__tests__/logging_service.test.ts rename to src/core/server/logging/logging_service.test.ts index eb452376d6ccc..b6aeb88b50052 100644 --- a/src/core/server/logging/__tests__/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -32,8 +32,7 @@ jest.spyOn(global, 'Date').mockImplementation(() => timestamp); import { createWriteStream } from 'fs'; const mockCreateWriteStream = createWriteStream as jest.Mock; -import { LoggingConfig } from '../logging_config'; -import { LoggingService } from '../logging_service'; +import { LoggingConfig, LoggingService } from '.'; let service: LoggingService; beforeEach(() => (service = new LoggingService())); diff --git a/src/core/server/root/__tests__/__snapshots__/index.test.ts.snap b/src/core/server/root/__snapshots__/index.test.ts.snap similarity index 100% rename from src/core/server/root/__tests__/__snapshots__/index.test.ts.snap rename to src/core/server/root/__snapshots__/index.test.ts.snap diff --git a/src/core/server/root/__tests__/index.test.ts b/src/core/server/root/index.test.ts similarity index 95% rename from src/core/server/root/__tests__/index.test.ts rename to src/core/server/root/index.test.ts index 851e7c9dca85e..97308ef484f4f 100644 --- a/src/core/server/root/__tests__/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -18,24 +18,24 @@ */ const mockLoggingService = { asLoggerFactory: jest.fn(), upgrade: jest.fn(), stop: jest.fn() }; -jest.mock('../../logging', () => ({ +jest.mock('../logging', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); const mockConfigService = { atPath: jest.fn() }; -jest.mock('../../config/config_service', () => ({ +jest.mock('../config/config_service', () => ({ ConfigService: jest.fn(() => mockConfigService), })); const mockServer = { start: jest.fn(), stop: jest.fn() }; -jest.mock('../../', () => ({ Server: jest.fn(() => mockServer) })); +jest.mock('../', () => ({ Server: jest.fn(() => mockServer) })); import { BehaviorSubject } from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { Root } from '../'; -import { Config, Env } from '../../config'; -import { getEnvOptions } from '../../config/__tests__/__mocks__/env'; -import { logger } from '../../logging/__mocks__'; +import { Root } from '.'; +import { Config, Env } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; +import { logger } from '../logging/__mocks__'; const env = new Env('.', getEnvOptions()); const config$ = new BehaviorSubject({} as Config); diff --git a/src/core/utils/__tests__/__snapshots__/get.test.ts.snap b/src/core/utils/__snapshots__/get.test.ts.snap similarity index 100% rename from src/core/utils/__tests__/__snapshots__/get.test.ts.snap rename to src/core/utils/__snapshots__/get.test.ts.snap diff --git a/src/core/utils/__tests__/get.test.ts b/src/core/utils/get.test.ts similarity index 97% rename from src/core/utils/__tests__/get.test.ts rename to src/core/utils/get.test.ts index a93ad6f6d708e..f409638b5d491 100644 --- a/src/core/utils/__tests__/get.test.ts +++ b/src/core/utils/get.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { get } from '../get'; +import { get } from './get'; const obj = { bar: { diff --git a/src/core/utils/__tests__/url.test.ts b/src/core/utils/url.test.ts similarity index 98% rename from src/core/utils/__tests__/url.test.ts rename to src/core/utils/url.test.ts index 6ff3a75d6e725..f890b2ec13db3 100644 --- a/src/core/utils/__tests__/url.test.ts +++ b/src/core/utils/url.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { modifyUrl } from '../url'; +import { modifyUrl } from './url'; describe('modifyUrl()', () => { test('throws an error with invalid input', () => { From 76408be650953b3889176f9ec0d86b9d84c6d074 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 7 Sep 2018 10:03:28 +0200 Subject: [PATCH 41/68] [ML] Anomaly Explorer Charts jest tests. (#22759) After the migration to React in #22622, this PR adds more test coverage based on jest to the Anomaly Explorer charts. This is done as preparation for some further refactoring (#22626) and the integration of the support for charts for the rare detector (#21163). The tests use mock data based on a standard multi-metric job using the farequote dataset. Besided the tests this PR includes the following changes: - In explore_series.js the path is no longer a concatenanted string but now using a template literal - The exploreSeries() function is no longer calling window.open by itself, it just returns the link because of the above, renamed exploreSeries() to getExploreSeriesLink() and moved it to chart_utils.js - explorer_charts_container_service.js is no longer requiring jQuery itself, it now receiving the required element as a factory argument. Further work on this is planned to get rid of jQuery. --- .../messagebar/messagebar_service.js | 2 +- .../__mocks__/mock_anomaly_chart_records.json | 68 ++ .../__mocks__/mock_anomaly_record.json | 33 + .../__mocks__/mock_chart_data.js | 26 + .../__mocks__/mock_detectors_by_job.json | 11 + .../__mocks__/mock_job_config.json | 88 +++ .../mock_series_config_filebeat.json | 57 ++ .../mock_series_promises_response.json | 231 +++++++ ...explorer_chart_config_builder.test.js.snap | 52 ++ .../explorer_chart_tooltip.test.js.snap | 24 + .../explorer_charts_container.test.js.snap | 54 ++ ...orer_charts_container_service.test.js.snap | 616 ++++++++++++++++++ .../explorer_charts/explore_series.js | 74 --- .../explorer_charts/explorer_chart.test.js | 69 +- .../explorer_chart_config_builder.test.js | 27 + .../explorer_chart_tooltip.test.js | 28 + .../explorer_charts_container.js | 5 +- .../explorer_charts_container.test.js | 76 +++ .../explorer_charts_container_directive.js | 5 +- .../explorer_charts_container_service.js | 6 +- .../explorer_charts_container_service.test.js | 127 ++++ .../watcher/create_watch_service.js | 3 +- .../public/services/ml_api_service/filters.js | 2 +- .../public/services/ml_api_service/index.js | 2 +- .../ml/public/services/ml_api_service/jobs.js | 2 +- .../public/services/ml_api_service/results.js | 2 +- .../ml/public/services/results_service.js | 8 +- .../ml/public/util/chart_config_builder.js | 2 +- x-pack/plugins/ml/public/util/chart_utils.js | 61 ++ .../ml/public/util/chart_utils.test.js | 63 ++ 30 files changed, 1664 insertions(+), 160 deletions(-) create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container.test.js.snap create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap delete mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explore_series.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_config_builder.test.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_chart_tooltip.test.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container.test.js create mode 100644 x-pack/plugins/ml/public/explorer/explorer_charts/explorer_charts_container_service.test.js create mode 100644 x-pack/plugins/ml/public/util/chart_utils.test.js diff --git a/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js b/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js index 3bbf7e79c683e..4395be2a9fb4b 100644 --- a/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js +++ b/x-pack/plugins/ml/public/components/messagebar/messagebar_service.js @@ -6,7 +6,7 @@ import { notify } from 'ui/notify'; -import { MLRequestFailure } from 'plugins/ml/util/ml_error'; +import { MLRequestFailure } from '../../util/ml_error'; const messages = []; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json new file mode 100644 index 0000000000000..3f106d399f852 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_chart_records.json @@ -0,0 +1,68 @@ +[ + { + "job_id": "mock-job-id", + "result_type": "record", + "probability": 1.6552181439816634e-32, + "record_score": 98.56065708456248, + "initial_record_score": 98.56065708456248, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1486656000000, + "partition_field_name": "airline", + "partition_field_value": "AAL", + "function": "mean", + "function_description": "mean", + "typical": [ + 99.81123207526203 + ], + "actual": [ + 242.3568918440077 + ], + "field_name": "responsetime", + "influencers": [ + { + "influencer_field_name": "airline", + "influencer_field_values": [ + "AAL" + ] + } + ], + "airline": [ + "AAL" + ] + }, + { + "job_id": "mock-job-id", + "result_type": "record", + "probability": 2.6276047868032343e-28, + "record_score": 96.93718, + "initial_record_score": 92.70812367638732, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1486656900000, + "partition_field_name": "airline", + "partition_field_value": "AAL", + "function": "mean", + "function_description": "mean", + "typical": [ + 100.02884159032787 + ], + "actual": [ + 282.02533259111306 + ], + "field_name": "responsetime", + "influencers": [ + { + "influencer_field_name": "airline", + "influencer_field_values": [ + "AAL" + ] + } + ], + "airline": [ + "AAL" + ] + } +] diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json new file mode 100644 index 0000000000000..ccc13b6a815a4 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_anomaly_record.json @@ -0,0 +1,33 @@ +{ + "job_id": "mock-job-id", + "result_type": "record", + "probability": 0.000374234162864467, + "record_score": 1.3677172011743646, + "initial_record_score": 1.3677172011743646, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1486743300000, + "partition_field_name": "airline", + "partition_field_value": "JAL", + "function": "mean", + "function_description": "mean", + "typical": [ + 499.9850000350266 + ], + "actual": [ + 511.4997161865235 + ], + "field_name": "responsetime", + "influencers": [ + { + "influencer_field_name": "airline", + "influencer_field_values": [ + "JAL" + ] + } + ], + "airline": [ + "JAL" + ] +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js new file mode 100644 index 0000000000000..e150335114a20 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_chart_data.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const chartData = [ + { + date: new Date('2017-02-23T08:00:00.000Z'), + value: 228243469, anomalyScore: 63.32916, numberOfCauses: 1, + actual: [228243469], typical: [133107.7703441773] + }, + { date: new Date('2017-02-23T09:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T10:00:00.000Z'), value: null }, + { date: new Date('2017-02-23T11:00:00.000Z'), value: null }, + { + date: new Date('2017-02-23T12:00:00.000Z'), + value: 625736376, anomalyScore: 97.32085, numberOfCauses: 1, + actual: [625736376], typical: [132830.424736973] + }, + { + date: new Date('2017-02-23T13:00:00.000Z'), + value: 201039318, anomalyScore: 59.83488, numberOfCauses: 1, + actual: [201039318], typical: [132739.5267403542] + } +]; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json new file mode 100644 index 0000000000000..f45a2d5b8f2b9 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_detectors_by_job.json @@ -0,0 +1,11 @@ +{ + "mock-job-id": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "partition_field_name": "airline", + "detector_index": 0 + } + ] +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json new file mode 100644 index 0000000000000..2750ad84a8308 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_job_config.json @@ -0,0 +1,88 @@ +{ + "job_id": "mock-job-id", + "job_type": "anomaly_detector", + "job_version": "7.0.0-alpha1", + "description": "", + "create_time": 1532692299663, + "finished_time": 1532692304364, + "established_model_memory": 560894, + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "mean(responsetime)", + "function": "mean", + "field_name": "responsetime", + "partition_field_name": "airline", + "detector_index": 0 + } + ], + "influencers": [ + "airline" + ] + }, + "analysis_limits": { + "model_memory_limit": "13mb", + "categorization_examples_limit": 4 + }, + "data_description": { + "time_field": "@timestamp", + "time_format": "epoch_ms" + }, + "model_snapshot_retention_days": 1, + "custom_settings": { + "created_by": "multi-metric-wizard" + }, + "model_snapshot_id": "1532692303", + "model_snapshot_min_version": "6.4.0", + "results_index_name": "shared", + "data_counts": { + "job_id": "mock-job-id", + "processed_record_count": 86274, + "processed_field_count": 172548, + "input_bytes": 6744642, + "input_field_count": 172548, + "invalid_date_count": 0, + "missing_field_count": 0, + "out_of_order_timestamp_count": 0, + "empty_bucket_count": 0, + "sparse_bucket_count": 0, + "bucket_count": 479, + "earliest_record_timestamp": 1486425600000, + "latest_record_timestamp": 1486857594000, + "last_data_time": 1532692303844, + "input_record_count": 86274 + }, + "model_size_stats": { + "job_id": "mock-job-id", + "result_type": "model_size_stats", + "model_bytes": 560894, + "total_by_field_count": 21, + "total_over_field_count": 0, + "total_partition_field_count": 20, + "bucket_allocation_failures_count": 0, + "memory_status": "ok", + "log_time": 1532692303000, + "timestamp": 1486855800000 + }, + "datafeed_config": { + "datafeed_id": "datafeed-mock-job-id", + "job_id": "mock-job-id", + "query_delay": "86658ms", + "indices": [ + "farequote-2017" + ], + "types": [], + "query": { + "match_all": { + "boost": 1 + } + }, + "scroll_size": 1000, + "chunking_config": { + "mode": "auto" + }, + "state": "stopped" + }, + "state": "closed" +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json new file mode 100644 index 0000000000000..b2c974e737e48 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_config_filebeat.json @@ -0,0 +1,57 @@ +{ + "jobId": "population-03", + "detectorIndex": 0, + "metricFunction": "sum", + "timeField": "@timestamp", + "interval": "1h", + "datafeedConfig": { + "datafeed_id": "datafeed-population-03", + "job_id": "population-03", + "query_delay": "60s", + "frequency": "600s", + "indices": [ + "filebeat-7.0.0*" + ], + "types": [ + "doc" + ], + "query": { + "match_all": { + "boost": 1 + } + }, + "scroll_size": 1000, + "chunking_config": { + "mode": "auto" + }, + "state": "stopped" + }, + "metricFieldName": "nginx.access.body_sent.bytes", + "functionDescription": "sum", + "bucketSpanSeconds": 3600, + "detectorLabel": "high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03)", + "fieldName": "nginx.access.body_sent.bytes", + "entityFields": [ + { + "fieldName": "nginx.access.remote_ip", + "fieldValue": "72.57.0.53", + "$$hashKey": "object:813" + } + ], + "infoTooltip": { + "jobId": "population-03", + "aggregationInterval": "1h", + "chartFunction": "sum nginx.access.body_sent.bytes", + "entityFields": [ + { + "fieldName": "nginx.access.remote_ip", + "fieldValue": "72.57.0.53" + } + ] + }, + "loading": false, + "plotEarliest": 1487534400000, + "plotLatest": 1488168000000, + "selectedEarliest": 1487808000000, + "selectedLatest": 1487894399999 +} diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json new file mode 100644 index 0000000000000..b2d51e7de713b --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__mocks__/mock_series_promises_response.json @@ -0,0 +1,231 @@ +[ + [ + { + "success": true, + "results": { + "1486611900000": 95.61584963117328, + "1486612800000": 99.34646708170573, + "1486613700000": 92.54502330106847, + "1486614600000": 98.87258768081665, + "1486615500000": 102.82824816022601, + "1486616400000": 96.7391939163208, + "1486617300000": 99.72634760538737, + "1486618200000": 101.08556365966797, + "1486619100000": 84.60266517190372, + "1486620000000": 105.24246263504028, + "1486620900000": 91.86086603800456, + "1486621800000": 94.5369130452474, + "1486622700000": 97.63843189586292, + "1486623600000": 93.79290502211627, + "1486624500000": 108.91006604362937, + "1486625400000": 107.46900049845378, + "1486626300000": 100.03502061631944, + "1486627200000": 92.0638559129503, + "1486628100000": 96.06356851678146, + "1486629000000": 109.89569989372703, + "1486629900000": 96.09498441786994, + "1486630800000": 105.05972120496962, + "1486631700000": 94.53041982650757, + "1486632600000": 103.37048240329908, + "1486633500000": 105.2058048248291, + "1486634400000": 102.06169471740722, + "1486635300000": 101.4836499955919, + "1486636200000": 96.34219177246094, + "1486637100000": 102.81613063812256, + "1486638000000": 96.09064518321644, + "1486638900000": 104.8488635012978, + "1486639800000": 93.45240384056454, + "1486640700000": 102.28834065524015, + "1486641600000": 104.54204668317523, + "1486642500000": 99.85492063823499, + "1486643400000": 97.12778260972765, + "1486644300000": 103.99638447008635, + "1486645200000": 95.34676822863128, + "1486646100000": 97.04620517383923, + "1486647000000": 104.2849609375, + "1486647900000": 97.88982413796818, + "1486648800000": 99.03312370300293, + "1486649700000": 105.5509593963623, + "1486650600000": 100.49496881585372, + "1486651500000": 99.06059494018555, + "1486652400000": 90.58293914794922, + "1486653300000": 92.8633090655009, + "1486654200000": 96.12510445004418, + "1486655100000": 100.4840145111084, + "1486656000000": 242.3568918440077, + "1486656900000": 282.02533259111294, + "1486657800000": 100.15823459625244, + "1486658700000": 97.5446532754337, + "1486659600000": 99.53840043809679, + "1486660500000": 101.24810005636776, + "1486661400000": 101.11400771141052, + "1486662300000": 100.70463662398488, + "1486663200000": 110.70174247340152, + "1486664100000": 96.51030629475912, + "1486665000000": 103.92840491400824, + "1486665900000": 98.29448418868215, + "1486666800000": 98.0272060394287, + "1486667700000": 99.63833363850911, + "1486668600000": 105.18764642568735, + "1486669500000": 97.8544118669298, + "1486670400000": 97.99196343672902, + "1486671300000": 106.30481338500977, + "1486672200000": 99.88215498490767, + "1486673100000": 93.50493303934734, + "1486674000000": 101.2538422175816, + "1486674900000": 102.07398986816406, + "1486675800000": 102.66583075890175, + "1486676700000": 108.5278158748851, + "1486677600000": 103.91436131795247, + "1486678500000": 98.55452414119945, + "1486679400000": 88.25028387705485, + "1486680300000": 93.57433591570172, + "1486681200000": 96.70550713172325, + "1486682100000": 98.14921424502418, + "1486683000000": 96.99264602661133, + "1486683900000": 88.23578810691833, + "1486684800000": 106.89157305265728, + "1486685700000": 101.07822271493765, + "1486686600000": 101.77820564718807, + "1486687500000": 102.84660829816546, + "1486688400000": 103.91598869772518, + "1486689300000": 104.73469270978656, + "1486690200000": 97.01155325082632, + "1486691100000": 104.97890539730297, + "1486692000000": 99.66440022786459, + "1486692900000": 99.64117607703575, + "1486693800000": 87.37038326263428, + "1486694700000": 105.95191955566406, + "1486695600000": 104.33271111382379, + "1486696500000": 101.93921706255745, + "1486697400000": 101.11774004422702, + "1486698300000": 101.70929403866039, + "1486699200000": 102.61243908221905, + "1486700100000": 99.16273922390408, + "1486701000000": 105.98729952643899, + "1486701900000": 114.16951904296874, + "1486702800000": 98.25128769874573, + "1486703700000": 94.25434192858245, + "1486704600000": 99.7759528526893, + "1486705500000": 113.10429502788342, + "1486706400000": 97.95185834711248, + "1486707300000": 114.46214866638184, + "1486708200000": 105.51880025863647, + "1486709100000": 99.89148930140904, + "1486710000000": 90.5253866369074, + "1486710900000": 103.66612243652344, + "1486711800000": 103.97851837158203, + "1486712700000": 92.76053659539474, + "1486713600000": 99.99461364746094 + } + }, + { + "success": true, + "records": [ + { + "job_id": "mock-job-id", + "result_type": "record", + "probability": 1.6552181439816634e-32, + "record_score": 98.56065708456248, + "initial_record_score": 98.56065708456248, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1486656000000, + "partition_field_name": "airline", + "partition_field_value": "AAL", + "function": "mean", + "function_description": "mean", + "typical": [ + 99.81123207526203 + ], + "actual": [ + 242.3568918440077 + ], + "field_name": "responsetime", + "influencers": [ + { + "influencer_field_name": "airline", + "influencer_field_values": [ + "AAL" + ] + } + ], + "airline": [ + "AAL" + ] + }, + { + "job_id": "mock-job-id", + "result_type": "record", + "probability": 2.6276047868032343e-28, + "record_score": 96.93718, + "initial_record_score": 92.70812367638732, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1486656900000, + "partition_field_name": "airline", + "partition_field_value": "AAL", + "function": "mean", + "function_description": "mean", + "typical": [ + 100.02884159032787 + ], + "actual": [ + 282.02533259111306 + ], + "field_name": "responsetime", + "influencers": [ + { + "influencer_field_name": "airline", + "influencer_field_values": [ + "AAL" + ] + } + ], + "airline": [ + "AAL" + ] + }, + { + "job_id": "mock-job-id", + "result_type": "record", + "probability": 0.013283203854072794, + "record_score": 0.02716009, + "initial_record_score": 0.6110770406098681, + "bucket_span": 900, + "detector_index": 0, + "is_interim": false, + "timestamp": 1486619100000, + "partition_field_name": "airline", + "partition_field_value": "AAL", + "function": "mean", + "function_description": "mean", + "typical": [ + 99.79426367092864 + ], + "actual": [ + 84.60266517190372 + ], + "field_name": "responsetime", + "influencers": [ + { + "influencer_field_name": "airline", + "influencer_field_values": [ + "AAL" + ] + } + ], + "airline": [ + "AAL" + ] + } + ] + }, + { + "success": true, + "events": {} + } + ] +] diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap new file mode 100644 index 0000000000000..f899ee14003b7 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildConfig get dataConfig for anomaly record 1`] = ` +Object { + "bucketSpanSeconds": 900, + "datafeedConfig": Object { + "chunking_config": Object { + "mode": "auto", + }, + "datafeed_id": "datafeed-mock-job-id", + "indices": Array [ + "farequote-2017", + ], + "job_id": "mock-job-id", + "query": Object { + "match_all": Object { + "boost": 1, + }, + }, + "query_delay": "86658ms", + "scroll_size": 1000, + "state": "stopped", + "types": Array [], + }, + "detectorIndex": 0, + "detectorLabel": "mean(responsetime)", + "entityFields": Array [ + Object { + "fieldName": "airline", + "fieldValue": "JAL", + }, + ], + "fieldName": "responsetime", + "functionDescription": "mean", + "infoTooltip": Object { + "aggregationInterval": "15m", + "chartFunction": "avg responsetime", + "entityFields": Array [ + Object { + "fieldName": "airline", + "fieldValue": "JAL", + }, + ], + "jobId": "mock-job-id", + }, + "interval": "15m", + "jobId": "mock-job-id", + "metricFieldName": "responsetime", + "metricFunction": "avg", + "timeField": "@timestamp", +} +`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap new file mode 100644 index 0000000000000..c602bc0373c51 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_chart_tooltip.test.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExplorerChartTooltip Render tooltip based on infoTooltip data. 1`] = ` +
+ job ID: + mock-job-id +
+ aggregation interval: + 15m +
+ chart function: + avg responsetime + +
+ airline + : + JAL +
+
+`; diff --git a/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container.test.js.snap b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container.test.js.snap new file mode 100644 index 0000000000000..087558cfa4ed4 --- /dev/null +++ b/x-pack/plugins/ml/public/explorer/explorer_charts/__snapshots__/explorer_charts_container.test.js.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExplorerChartsContainer Initialization with chart data 1`] = ` +
+
+ + high_sum(nginx.access.body_sent.bytes) over nginx.access.remote_ip (population-03) + - + + + nginx.access.remote_ip + + 72.57.0.53 + +
+ + } + position="left" + size="s" + type="questionInCircle" + /> + + View +